Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ee6bc0e
fix(failover): live-probe targets, rate-limit auto moves, clean old c…
Lftobs Oct 8, 2026
850bf5b
fix(ssh): run the railpack config generator on remote hosts
Lftobs Oct 8, 2026
34a8638
fix(logs): access-log ingress routes and target runtime logs at the d…
Lftobs Oct 8, 2026
31cbea0
fix: bug-sheet fixes, test-suite repair, and Let's Encrypt certs for …
Lftobs Oct 9, 2026
61266c5
Merge branch 'bugs-fix' of github.com:Lftobs/dequel into bugs-fix
Lftobs Oct 9, 2026
b7581b7
fix(databases): use real certificates and server
Lftobs Oct 9, 2026
211d0f6
docs(changelog): include gateway, failover and database fixes in v0.4.0
Lftobs Oct 9, 2026
91e928b
docs: clarify database exposure and MySQL access
Lftobs Oct 9, 2026
e70a76c
refactor(config): separate fixed values from
Lftobs Oct 9, 2026
7b6e78c
docs(changelog): include config refactor and database docs in v0.4.0
Lftobs Oct 9, 2026
41b07b7
Create encryption-migration.ts
Lftobs Oct 9, 2026
7416cd0
docs(changelog): include encryption migration in v0.4.0
Lftobs Oct 9, 2026
81ea4b0
fix(api): remove redundant zombie reaper
Lftobs Oct 11, 2026
0dce87e
fix(docs): make landing page and docs nav mobile responsive
Lftobs Oct 11, 2026
6ebfde7
fix: harden deployment and configuration workflows
Lftobs Oct 11, 2026
87b1486
docs(changelog): include deployment hardening fixes in v0.4.0
Lftobs Oct 11, 2026
eb0124a
docs: rewrite for accuracy, add missing feature pages and UI screenshots
Lftobs Oct 11, 2026
d5797ac
Update index.ts
Lftobs Oct 11, 2026
cdfb812
Merge remote-tracking branch 'origin/dev' into bugs-fix
Lftobs Oct 11, 2026
776ff48
Merge pull request #62 from Lftobs/bugs-fix
Lftobs Oct 11, 2026
579fbe9
Merge branch 'dev' into feat/blume-docs
Lftobs Oct 11, 2026
1ac9913
Merge pull request #67 from Lftobs/feat/blume-docs
Lftobs Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ bun run sync-versions # Syncs VERSION → sub-package.json files
- No emojis in code or UI unless explicitly requested
- Functional components with hooks (React)
- Tailwind CSS for styling (both web and docs)
- All new UI created (both web and docs) must be mobile responsive across all viewport sizes (mobile, tablet, and desktop) with no horizontal overflow or broken navigation

## Adding a Doc Page

Expand Down
51 changes: 51 additions & 0 deletions apps/api/src/api/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Elysia } from "elysia";
import { fail } from "./response";

const BYPASS_PATHS = new Set([
"/api/auth/login",
"/api/auth/logout",
"/api/auth/refresh",
"/api/auth/me",
"/api/health",
"/api/github/callback",
"/api/github/webhook",
"/api/agents/register",
"/api/agents/p2p-sync",
]);

export const authMiddleware = (app: Elysia) =>
app.onBeforeHandle(async ({ request, set, path }) => {
if (BYPASS_PATHS.has(path)) return;

const cookie = request.headers.get("cookie") || "";
const match = cookie.match(/(?:^|;\s*)dequel_session=([^;]+)/);
if (match) {
const { verifyAccessToken } = await import("../utils/auth");
const payload = await verifyAccessToken(match[1]);
if (payload) return;
}

const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.slice(7);
if (token) {
const { validateApiKey } = await import("../db/repo");
const { canPerformMethod, parsePermissions, requiredPermissionForMethod } = await import(
"../utils/permissions"
);
const key = await validateApiKey(token);
if (!key) {
set.status = 401;
return fail("Invalid API key");
}
if (!canPerformMethod(parsePermissions(key.permissions), request.method)) {
set.status = 403;
return fail(`API key is missing the ${requiredPermissionForMethod(request.method)} scope`);
}
return;
}
}

set.status = 401;
return fail("Authentication required");
});
65 changes: 54 additions & 11 deletions apps/api/src/api/databases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
deleteDatabase,
getDatabaseById,
getProjectById,
getServerById,
listAllDatabases,
listDatabases,
updateDatabaseSettings,
updateDatabaseStatus,
} from "../../db/repo";
import { loadCaddyCert } from "../../tunnels/cert-store";
import type { Database } from "../../types";
import { config } from "../../utils/config";
import { removeDbGatewayRoute, syncDbGatewayRoute } from "../../utils/db-caddy-route";
import { resolveServerIp } from "../../utils/dns";
import { probeTcp } from "../../utils/tcp-probe";
import { created, fail, ok } from "../response";
import { executeDatabaseQuery, getDatabaseTables } from "./query";

Expand Down Expand Up @@ -60,6 +64,14 @@ const isNonLoopbackIp = (value: string): boolean => {
return !value.startsWith("127.");
};

const resolveDbExternalHost = async (serverId: string | null): Promise<string> => {
if (serverId) {
const server = await getServerById(serverId);
if (server && isNonLoopbackIp(server.host)) return server.host;
}
return resolveServerIp();
};

const createManagedDatabase = async (body: any, projectId: string | null, set: any) => {
if (projectId && !(await getProjectById(projectId))) {
set.status = 404;
Expand All @@ -76,6 +88,7 @@ const createManagedDatabase = async (body: any, projectId: string | null, set: a
return fail("A database with this name already exists");
}
const dbRecord = await createDbRecord({ ...validation.input, projectId });
void syncDbGatewayRoute(dbRecord);
const { provisionDatabase } = await import("../../databases/manager");
provisionDatabase(dbRecord).catch((err: Error) => console.error("DB provision failed", err));
return created(sanitizeDatabase(dbRecord));
Expand Down Expand Up @@ -118,27 +131,50 @@ export const databasesRoutes = new Elysia()
? `${dbRecord.internalHost}.${baseDomain}`
: null;
if (gatewayHost) {
const reachable = await probeTcp(gatewayHost, 443);
const hasRealCert = config.caddyDataDir
? (await loadCaddyCert(config.caddyDataDir, gatewayHost)) !== null
: false;
return ok({
username: dbRecord.username,
password: dbRecord.password,
internalConnectionString: dbRecord.connectionString,
externalConnectionString: buildGatewayConnectionString(dbRecord, gatewayHost),
externalConnectionString: buildGatewayConnectionString(dbRecord, gatewayHost, hasRealCert),
externalHost: gatewayHost,
externalPort: 443,
externalReachable: reachable,
warning: reachable
? null
: `Gateway endpoint ${gatewayHost}:443 is not reachable from the network — check the gateway service and DNS.`,
});
}
const externalHost = dbRecord.publicAccess && dbRecord.externalPort ? await resolveServerIp() : null;
const externalHost =
dbRecord.publicAccess && dbRecord.externalPort ? await resolveDbExternalHost(dbRecord.serverId ?? null) : null;
const usableHost = externalHost && isNonLoopbackIp(externalHost) ? externalHost : null;
const externalConnectionString = usableHost
? buildConnectionString(dbRecord, usableHost, dbRecord.externalPort!)
: null;
if (usableHost && dbRecord.externalPort) {
const reachable = await probeTcp(usableHost, dbRecord.externalPort);
return ok({
username: dbRecord.username,
password: dbRecord.password,
internalConnectionString: dbRecord.connectionString,
externalConnectionString: buildConnectionString(dbRecord, usableHost, dbRecord.externalPort),
externalHost: usableHost,
externalPort: dbRecord.externalPort,
externalReachable: reachable,
warning: reachable
? null
: `Public endpoint ${usableHost}:${dbRecord.externalPort} is not reachable — open the port in your cloud security group or firewall to allow external clients.`,
});
}
return ok({
username: dbRecord.username,
password: dbRecord.password,
internalConnectionString: dbRecord.connectionString,
externalConnectionString,
externalHost: usableHost,
externalPort: usableHost ? dbRecord.externalPort : null,
externalConnectionString: null,
externalHost: null,
externalPort: null,
externalReachable: null,
warning: null,
});
})
.post("/databases/:id/start", async ({ params: { id }, set }) => {
Expand Down Expand Up @@ -235,7 +271,12 @@ export const databasesRoutes = new Elysia()
backupSchedule: body?.backupSchedule,
backupRetention: body?.backupRetention,
});
return ok(sanitizeDatabase(updated!));
if (!updated) {
set.status = 404;
return fail("Database not found");
}
void syncDbGatewayRoute(updated);
return ok(sanitizeDatabase(updated));
})
.delete("/databases/:id", async ({ params: { id }, set }) => {
const dbRecord = await findDatabase(id, set);
Expand All @@ -259,6 +300,7 @@ export const databasesRoutes = new Elysia()
return fail("Database resources could not be deleted; cleanup will be retried automatically");
}
await deleteDatabase(id);
void removeDbGatewayRoute(current);
return ok(null, "Database deleted");
});
});
Expand All @@ -272,12 +314,13 @@ const buildConnectionString = (dbRecord: DatabaseRecord, host: string, port: num
return `${protocol}://${dbRecord.username}:${dbRecord.password}@${host}:${port}/${dbRecord.databaseName}`;
};

const buildGatewayConnectionString = (dbRecord: DatabaseRecord, host: string) => {
const buildGatewayConnectionString = (dbRecord: DatabaseRecord, host: string, hasRealCert: boolean) => {
const auth = `${dbRecord.username}:${dbRecord.password}`;
const path = `/${dbRecord.databaseName}`;
if (dbRecord.type === "redis") return `rediss://:${dbRecord.password}@${host}:443`;
if (dbRecord.type === "mongodb") {
return `mongodb://${auth}@${host}:443${path}?authSource=admin&tls=true&tlsAllowInvalidCertificates=true`;
const tls = hasRealCert ? "tls=true" : "tls=true&tlsAllowInvalidCertificates=true";
return `mongodb://${auth}@${host}:443${path}?authSource=admin&${tls}`;
}
return `postgresql://${auth}@${host}:443${path}?sslmode=require`;
};
43 changes: 25 additions & 18 deletions apps/api/src/api/deployments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { executorFor } from "../../executors/dispatch";
import { orchestrator } from "../../orchestrator";
import { summarizeDeploymentError } from "../../orchestrator/deployment-errors";
import { logBus } from "../../orchestrator/log-bus";
import { readRuntimeLogs, runtimeLogFollowerArgs } from "../../orchestrator/runtime-logs";
import { config } from "../../utils/config";
import { terminateWithEscalation } from "../../utils/process-exec";
import { isPrivateGitUrl } from "../../utils/validate";
Expand Down Expand Up @@ -142,9 +143,17 @@ export const deploymentsRoutes = new Elysia()
set.status = 400;
return fail("archive file is required for upload source");
}
const safeName = basename(file.name || "project.zip").replace(/[^a-zA-Z0-9._-]/g, "_");
if (![".zip", ".tar", ".tar.gz", ".tgz"].some((ext) => safeName.toLowerCase().endsWith(ext))) {
set.status = 400;
return fail("Unsupported archive format. Use .zip, .tar, .tar.gz, or .tgz");
}
if (file.size === 0) {
set.status = 400;
return fail("Archive file is empty");
}
const uploadsDir = join(config.workspaceRoot, "uploads");
await mkdir(uploadsDir, { recursive: true });
const safeName = basename(file.name || "project.zip").replace(/[^a-zA-Z0-9._-]/g, "_");
const uploadPath = join(uploadsDir, `${Date.now()}-${safeName}`);
const bytes = new Uint8Array(await file.arrayBuffer());
await writeFile(uploadPath, bytes);
Expand Down Expand Up @@ -330,20 +339,8 @@ export const deploymentsRoutes = new Elysia()
set.status = 404;
return fail("Deployment not found");
}
const { run } = await import("../../orchestrator/runtime");
const containerName = deployment.containerName || `deploy-${id}`;
try {
const output = await run("docker", ["logs", "--tail", "200", containerName]);
const lines = output
.split("\n")
.filter(Boolean)
.map((line, i) => ({
sequence: i + 1,
message: line,
timestamp: new Date().toISOString(),
stage: "runtime" as const,
}));
return ok(lines);
return ok(await readRuntimeLogs(deployment));
} catch {
return ok([]);
}
Expand All @@ -355,7 +352,6 @@ export const deploymentsRoutes = new Elysia()
return fail("Deployment not found");
}
const encoder = new TextEncoder();
const containerName = deployment.containerName || `deploy-${id}`;
let closed = false;
let childProcess: import("node:child_process").ChildProcess | null = null;
const stop = () => {
Expand All @@ -375,16 +371,27 @@ export const deploymentsRoutes = new Elysia()
controller.enqueue(encoder.encode(`event: ${eventName}\ndata: ${JSON.stringify(payload)}\n\n`));
};
const { spawn } = await import("node:child_process");
const isPosix = process.platform !== "win32";
const child = spawn("docker", ["logs", "--tail", "100", "--follow", containerName], {
detached: isPosix,
const child = spawn("docker", await runtimeLogFollowerArgs(deployment), {
stdio: ["ignore", "pipe", "pipe"],
});
childProcess = child;
if (closed) {
terminateWithEscalation(child, 2000);
try {
controller.close();
} catch {}
return;
}
child.on("error", (err) => {
send("log", {
sequence: 0,
message: `docker logs failed: ${err.message}`,
timestamp: new Date().toISOString(),
stage: "runtime",
});
send("close", { reason: "spawn failed" });
stop();
});
let seq = 0;
child.stdout.on("data", (chunk: Buffer) => {
if (closed) return;
Expand Down
7 changes: 5 additions & 2 deletions apps/api/src/api/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
setGithubIntegration,
} from "../../db/repo";
import { orchestrator } from "../../orchestrator";
import { config } from "../../utils/config";
import { fail, ok } from "../response";

const validateToken = async (token: string): Promise<boolean> => {
Expand Down Expand Up @@ -249,7 +248,11 @@ export const githubRoutes = new Elysia({ prefix: "/github" })
}
const webhookUrl = `${publicUrl(request).origin}/api/github/webhook`;
const integration = await getGithubIntegration();
const secret = integration?.webhookSecret || config.githubWebhookSecret;
const secret = integration?.webhookSecret || "";
if (!secret) {
set.status = 400;
return fail("Set a webhook secret in Settings before registering a webhook");
}

try {
const hooks = await fetchGitHub(`/repos/${params.owner}/${params.repo}/hooks`, token);
Expand Down
47 changes: 3 additions & 44 deletions apps/api/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Elysia } from "elysia";
import { fail } from "./response";
import { fixdiagRoutes } from "../fixdiag/routes";
import { agentRoutes } from "./agents";
import { alertsRoutes } from "./alerts";
import { apiKeysRoutes } from "./api-keys";
import { authRoutes } from "./auth";
import { authMiddleware } from "./auth-middleware";
import { backupRoutes } from "./backups";
import { databasesRoutes } from "./databases";
import { deploymentsRoutes } from "./deployments";
Expand All @@ -14,59 +15,17 @@ import { healthRoutes } from "./health";
import { projectsRoutes } from "./projects";
import { projectStatusRoutes } from "./projects/status";
import { prometheusRoutes } from "./prometheus";
import { fail } from "./response";
import { routesRoutes } from "./routes";
import { scalingRoutes } from "./scaling";
import { serverInfoRoutes } from "./server-info";
import { serversRoutes } from "./servers";
import { settingsRoutes } from "./settings";
import { llmSettingsRoutes } from "./settings/llm";
import { fixdiagRoutes } from "../fixdiag/routes";
import { sharedEnvLinksRoutes, sharedEnvVarsRoutes } from "./shared-env-vars";
import { sshKeysRoutes } from "./ssh-keys";
import { volumesRoutes } from "./volumes";

const BYPASS_PATHS = new Set([
"/api/auth/login",
"/api/auth/logout",
"/api/auth/refresh",
"/api/auth/me",
"/api/health",
"/api/github/callback",
"/api/github/webhook",
"/api/agents/register",
"/api/agents/p2p-sync",
]);

const authMiddleware = (app: Elysia) =>
app.onBeforeHandle(async ({ request, set, path }) => {
if (BYPASS_PATHS.has(path)) return;

const cookie = request.headers.get("cookie") || "";
const match = cookie.match(/(?:^|;\s*)dequel_session=([^;]+)/);
if (match) {
const { verifyAccessToken } = await import("../utils/auth");
const payload = await verifyAccessToken(match[1]);
if (payload) return;
set.status = 401;
return fail("Invalid session");
}

const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.slice(7);
if (token) {
const { validateApiKey } = await import("../db/repo");
const key = await validateApiKey(token);
if (key) return;
set.status = 401;
return fail("Invalid API key");
}
}

set.status = 401;
return fail("Authentication required");
});

const INTERNAL_ERROR =
/Failed query:|params:|getaddrinfo|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|EHOSTUNREACH|timeout exceeded|node:internal|Cannot read propert|is not a function|is not a constructor|Unexpected token/i;

Expand Down
Loading
Loading