feat(docker): add shared-runtime self-hosted deployment
This commit is contained in:
1 parent
e900e4b615
commit
b83e99a9ac
21 files changed
+940
-116
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { resolveSelfHostedConfig } from "./self-hosted-config.mjs";
|
||||
import { loadSelfHostedEnvironment } from "./self-hosted-secrets.mjs";
|
||||
|
||||
const readProjectFile = (path) => readFileSync(resolve(import.meta.dir, "..", path), "utf8");
|
||||
|
||||
describe("self-hosted runtime configuration", () => {
|
||||
test("uses the portable single-volume defaults", () => {
|
||||
const config = resolveSelfHostedConfig({}, "/opt/edgeever");
|
||||
expect(config.dataDirectory).toBe("/opt/edgeever/.edgeever-data");
|
||||
expect(config.databaseFile).toBe("/opt/edgeever/.edgeever-data/edgeever.sqlite");
|
||||
expect(config.resourcesDirectory).toBe("/opt/edgeever/.edgeever-data/resources");
|
||||
expect(config.port).toBe(8787);
|
||||
expect(config.storageBackend).toBe("local");
|
||||
});
|
||||
|
||||
test("loads Docker secrets without retaining the file indirection", async () => {
|
||||
const directory = await mkdtemp(`${tmpdir()}/edgeever-docker-secret-`);
|
||||
const secretPath = resolve(directory, "password");
|
||||
try {
|
||||
await writeFile(secretPath, "secret-from-file\n");
|
||||
const environment = await loadSelfHostedEnvironment({ EDGE_EVER_AUTH_PASSWORD_FILE: secretPath });
|
||||
expect(environment.EDGE_EVER_AUTH_PASSWORD).toBe("secret-from-file");
|
||||
expect(environment.EDGE_EVER_AUTH_PASSWORD_FILE).toBeUndefined();
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects invalid ports and storage backends", () => {
|
||||
expect(() => resolveSelfHostedConfig({ EDGE_EVER_PORT: "0" })).toThrow("EDGE_EVER_PORT");
|
||||
expect(() => resolveSelfHostedConfig({ EDGE_EVER_STORAGE_BACKEND: "r2" })).toThrow(
|
||||
"either local or s3",
|
||||
);
|
||||
expect(() => resolveSelfHostedConfig({ EDGE_EVER_STORAGE_BACKEND: "s3" })).toThrow(
|
||||
"EDGE_EVER_S3_BUCKET",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Docker release contract", () => {
|
||||
test("runs the production image as a non-root user with one persistent volume", () => {
|
||||
const dockerfile = readProjectFile("Dockerfile");
|
||||
expect(dockerfile).toContain("FROM oven/bun:1.3.14-alpine AS runtime");
|
||||
expect(dockerfile).toContain("COPY patches patches");
|
||||
expect(dockerfile).toContain("COPY docs docs");
|
||||
expect(dockerfile).toContain("--filter @edgeever/web");
|
||||
expect(dockerfile).toContain("--production --filter edgeever");
|
||||
expect(dockerfile).toContain("USER bun");
|
||||
expect(dockerfile).toContain('VOLUME ["/data"]');
|
||||
expect(dockerfile).toContain("HEALTHCHECK");
|
||||
});
|
||||
|
||||
test("keeps authentication explicit in Compose", () => {
|
||||
const compose = readProjectFile("compose.yaml");
|
||||
expect(compose).toContain("EDGE_EVER_AUTH_PASSWORD: \"${EDGE_EVER_AUTH_PASSWORD:?");
|
||||
expect(compose).toContain("edgeever-data:/data");
|
||||
expect(compose).toContain("no-new-privileges:true");
|
||||
});
|
||||
|
||||
test("keeps both runtime entrypoints on the shared application", () => {
|
||||
const application = readProjectFile("apps/api/src/index.ts");
|
||||
const selfHosted = readProjectFile("scripts/self-hosted-server.mjs");
|
||||
expect(application).toContain("export const fetchEdgeEverApp");
|
||||
expect(application).toContain("storage: createCloudflareStorageAdapter(env)");
|
||||
expect(selfHosted).toContain("fetchEdgeEverApp(request, env, executionContext)");
|
||||
expect(selfHosted).not.toContain("worker.fetch(");
|
||||
});
|
||||
|
||||
test("gates official image publishing and release auditing", () => {
|
||||
const workflow = readProjectFile(".github/workflows/docker-image.yml");
|
||||
expect(workflow.match(/github\.repository == 'tianma-if\/edgeever'/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
expect(workflow).toContain("release_tag");
|
||||
expect(workflow).toContain("docker logout ghcr.io");
|
||||
expect(workflow).toContain("docker buildx imagetools inspect");
|
||||
});
|
||||
});
|
||||
+28
-4
@@ -21,9 +21,10 @@ const DEFAULT_REPOSITORY = "tianma-if/edgeever";
|
||||
const VERSION_BUMPS = new Set(["patch", "minor", "major"]);
|
||||
const POLL_INTERVAL_MS = 10_000;
|
||||
const RUN_DISCOVERY_TIMEOUT_MS = 60_000;
|
||||
const RELEASE_WORKFLOWS = {
|
||||
export const RELEASE_WORKFLOWS = {
|
||||
desktop: "desktop-build.yml",
|
||||
mobile: "mobile-build.yml",
|
||||
docker: "docker-image.yml",
|
||||
demo: "deploy-demo.yml",
|
||||
};
|
||||
|
||||
@@ -947,7 +948,7 @@ const releaseMain = async (options) => {
|
||||
console.log(`[release] Draft created: ${draftUrl}`);
|
||||
}
|
||||
|
||||
const [desktopRunId, mobileRunId] = await Promise.all([
|
||||
const [desktopRunId, mobileRunId, dockerRunId] = await Promise.all([
|
||||
dispatchReleaseWorkflow({
|
||||
repository: options.repository,
|
||||
workflow: RELEASE_WORKFLOWS.desktop,
|
||||
@@ -960,6 +961,12 @@ const releaseMain = async (options) => {
|
||||
tag,
|
||||
headSha: releaseSha,
|
||||
}),
|
||||
dispatchReleaseWorkflow({
|
||||
repository: options.repository,
|
||||
workflow: RELEASE_WORKFLOWS.docker,
|
||||
tag,
|
||||
headSha: releaseSha,
|
||||
}),
|
||||
]);
|
||||
await Promise.all([
|
||||
waitForRun({
|
||||
@@ -972,6 +979,11 @@ const releaseMain = async (options) => {
|
||||
runId: mobileRunId,
|
||||
label: "Draft Android assets",
|
||||
}),
|
||||
waitForRun({
|
||||
repository: options.repository,
|
||||
runId: dockerRunId,
|
||||
label: "Draft Docker image",
|
||||
}),
|
||||
]);
|
||||
|
||||
const draft = ghJson([
|
||||
@@ -1010,7 +1022,7 @@ const releaseMain = async (options) => {
|
||||
], { capture: true });
|
||||
console.log(`[release] published: ${releaseUrl}`);
|
||||
|
||||
const [desktopAudit, mobileAudit] = await Promise.all([
|
||||
const [desktopAudit, mobileAudit, dockerAudit] = await Promise.all([
|
||||
findReleaseRun({
|
||||
repository: options.repository,
|
||||
workflow: RELEASE_WORKFLOWS.desktop,
|
||||
@@ -1025,6 +1037,13 @@ const releaseMain = async (options) => {
|
||||
headSha: releaseSha,
|
||||
publishedAfter: publishedAt,
|
||||
}),
|
||||
findReleaseRun({
|
||||
repository: options.repository,
|
||||
workflow: RELEASE_WORKFLOWS.docker,
|
||||
tag,
|
||||
headSha: releaseSha,
|
||||
publishedAfter: publishedAt,
|
||||
}),
|
||||
]);
|
||||
const demoRuns = listWorkflowRuns({
|
||||
repository: options.repository,
|
||||
@@ -1052,6 +1071,11 @@ const releaseMain = async (options) => {
|
||||
runId: mobileAudit.databaseId,
|
||||
label: "Published Android asset audit",
|
||||
}),
|
||||
waitForRun({
|
||||
repository: options.repository,
|
||||
runId: dockerAudit.databaseId,
|
||||
label: "Published Docker image audit",
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
run("gh", [
|
||||
@@ -1072,7 +1096,7 @@ const releaseMain = async (options) => {
|
||||
"--repo",
|
||||
options.repository,
|
||||
"--body",
|
||||
`Released in [${tag}](${releaseUrl}).\n\nRequired local validations, Draft asset preparation, and post-publication native asset audits passed.`,
|
||||
`Released in [${tag}](${releaseUrl}).\n\nRequired local validations, Draft asset and image preparation, and post-publication audits passed.`,
|
||||
]);
|
||||
run("gh", [
|
||||
"issue",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildReleaseTitle,
|
||||
nextVersion,
|
||||
parseReleaseArgs,
|
||||
RELEASE_WORKFLOWS,
|
||||
RELEASE_VALIDATIONS,
|
||||
resolveReleaseVersion,
|
||||
reusedAssetMatches,
|
||||
@@ -13,6 +14,10 @@ import {
|
||||
} from "./release.mjs";
|
||||
|
||||
describe("release automation", () => {
|
||||
test("prepares and audits the official Docker image with every formal release", () => {
|
||||
expect(RELEASE_WORKFLOWS.docker).toBe("docker-image.yml");
|
||||
});
|
||||
|
||||
test("runs the complete project regression suite before release", () => {
|
||||
expect(RELEASE_VALIDATIONS).toContainEqual({
|
||||
label: "Project regression tests",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const SUPPORTED_STORAGE_BACKENDS = new Set(["local", "s3"]);
|
||||
|
||||
const parseInteger = (value, fallback, name, minimum, maximum) => {
|
||||
const normalized = value?.trim();
|
||||
if (!normalized) return fallback;
|
||||
|
||||
const parsed = Number(normalized);
|
||||
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
|
||||
throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const resolveSelfHostedConfig = (environment = process.env, projectRoot = process.cwd()) => {
|
||||
const dataDirectory = resolve(environment.EDGE_EVER_DATA_DIR ?? join(projectRoot, ".edgeever-data"));
|
||||
const storageBackend = (environment.EDGE_EVER_STORAGE_BACKEND ?? "local").trim().toLowerCase();
|
||||
|
||||
if (!SUPPORTED_STORAGE_BACKENDS.has(storageBackend)) {
|
||||
throw new Error("EDGE_EVER_STORAGE_BACKEND must be either local or s3");
|
||||
}
|
||||
if (storageBackend === "s3" && !environment.EDGE_EVER_S3_BUCKET?.trim()) {
|
||||
throw new Error("EDGE_EVER_S3_BUCKET is required when EDGE_EVER_STORAGE_BACKEND=s3");
|
||||
}
|
||||
|
||||
return {
|
||||
dataDirectory,
|
||||
databaseFile: resolve(environment.EDGE_EVER_SQLITE_FILE ?? join(dataDirectory, "edgeever.sqlite")),
|
||||
resourcesDirectory: resolve(environment.EDGE_EVER_RESOURCES_DIR ?? join(dataDirectory, "resources")),
|
||||
webDirectory: resolve(environment.EDGE_EVER_WEB_DIR ?? join(projectRoot, "apps/web/dist")),
|
||||
hostname: environment.EDGE_EVER_HOST?.trim() || "0.0.0.0",
|
||||
port: parseInteger(environment.PORT ?? environment.EDGE_EVER_PORT, 8787, "EDGE_EVER_PORT", 1, 65_535),
|
||||
idleTimeout: parseInteger(
|
||||
environment.EDGE_EVER_IDLE_TIMEOUT_SECONDS,
|
||||
120,
|
||||
"EDGE_EVER_IDLE_TIMEOUT_SECONDS",
|
||||
10,
|
||||
255,
|
||||
),
|
||||
storageBackend,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const FILE_SECRET_NAMES = [
|
||||
"EDGE_EVER_AUTH_PASSWORD",
|
||||
"EDGE_EVER_AUTH_PASSWORD_HASH",
|
||||
"EDGE_EVER_STORAGE_ENCRYPTION_KEY",
|
||||
"EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY",
|
||||
"EDGE_EVER_S3_ACCESS_KEY_ID",
|
||||
"EDGE_EVER_S3_SECRET_ACCESS_KEY",
|
||||
];
|
||||
|
||||
export const loadSelfHostedEnvironment = async (environment = process.env) => {
|
||||
const resolved = { ...environment };
|
||||
|
||||
for (const name of FILE_SECRET_NAMES) {
|
||||
const fileName = `${name}_FILE`;
|
||||
const filePath = environment[fileName]?.trim();
|
||||
if (!filePath) continue;
|
||||
if (environment[name] !== undefined) {
|
||||
throw new Error(`Set either ${name} or ${fileName}, not both`);
|
||||
}
|
||||
|
||||
resolved[name] = (await readFile(filePath, "utf8")).replace(/[\r\n]+$/, "");
|
||||
delete resolved[fileName];
|
||||
}
|
||||
|
||||
return resolved;
|
||||
};
|
||||
@@ -2,25 +2,23 @@ import { Database } from "bun:sqlite";
|
||||
import { mkdir, readdir, readFile } from "node:fs/promises";
|
||||
import { dirname, extname, join, normalize, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import worker from "../apps/api/src/index.ts";
|
||||
import { hasBootstrapCredential } from "../apps/api/src/auth-bootstrap.ts";
|
||||
import { isUnauthenticatedAccessEnabled } from "../apps/api/src/auth-state.ts";
|
||||
import { fetchEdgeEverApp } from "../apps/api/src/index.ts";
|
||||
import { createSelfHostedStorageAdapter } from "../apps/api/src/self-hosted-storage-adapter.ts";
|
||||
import { createS3CompatibleStorageAdapter } from "../apps/api/src/s3-compatible-storage-adapter.ts";
|
||||
import { resolveSelfHostedConfig } from "./self-hosted-config.mjs";
|
||||
import { loadSelfHostedEnvironment } from "./self-hosted-secrets.mjs";
|
||||
|
||||
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const dataDirectory = resolve(process.env.EDGE_EVER_DATA_DIR ?? join(projectRoot, ".edgeever-data"));
|
||||
const databaseFile = resolve(process.env.EDGE_EVER_SQLITE_FILE ?? join(dataDirectory, "edgeever.sqlite"));
|
||||
const resourcesDirectory = resolve(process.env.EDGE_EVER_RESOURCES_DIR ?? join(dataDirectory, "resources"));
|
||||
const webDirectory = resolve(process.env.EDGE_EVER_WEB_DIR ?? join(projectRoot, "apps/web/dist"));
|
||||
const port = Number(process.env.PORT ?? process.env.EDGE_EVER_PORT ?? 8787);
|
||||
const configuredIdleTimeout = Number(process.env.EDGE_EVER_IDLE_TIMEOUT_SECONDS ?? 120);
|
||||
const idleTimeout = Number.isFinite(configuredIdleTimeout)
|
||||
? Math.min(255, Math.max(10, configuredIdleTimeout))
|
||||
: 120;
|
||||
const runtimeEnvironment = await loadSelfHostedEnvironment(process.env);
|
||||
const config = resolveSelfHostedConfig(runtimeEnvironment, projectRoot);
|
||||
const { dataDirectory, databaseFile, resourcesDirectory, webDirectory } = config;
|
||||
|
||||
await mkdir(dataDirectory, { recursive: true });
|
||||
await mkdir(resourcesDirectory, { recursive: true });
|
||||
const sqlite = new Database(databaseFile, { create: true });
|
||||
sqlite.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
|
||||
sqlite.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;");
|
||||
|
||||
const migrationFiles = (await readdir(join(projectRoot, "migrations")))
|
||||
.filter((name) => name.endsWith(".sql"))
|
||||
@@ -50,40 +48,47 @@ for (const name of migrationFiles) {
|
||||
console.log(`[self-hosted] applied migration ${name}`);
|
||||
}
|
||||
|
||||
const storageBackend = process.env.EDGE_EVER_STORAGE_BACKEND ?? "local";
|
||||
const storage = storageBackend === "s3"
|
||||
const existingUser = sqlite.query("SELECT id FROM users WHERE is_disabled = 0 LIMIT 1").get();
|
||||
if (
|
||||
!existingUser
|
||||
&& !hasBootstrapCredential(runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD, runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD_HASH)
|
||||
&& !isUnauthenticatedAccessEnabled(runtimeEnvironment.EDGE_EVER_ALLOW_UNAUTHENTICATED)
|
||||
) {
|
||||
sqlite.close();
|
||||
throw new Error(
|
||||
"Authentication is not configured. Set EDGE_EVER_AUTH_PASSWORD (recommended), "
|
||||
+ "EDGE_EVER_AUTH_PASSWORD_HASH, or explicitly set EDGE_EVER_ALLOW_UNAUTHENTICATED=true.",
|
||||
);
|
||||
}
|
||||
|
||||
const storage = config.storageBackend === "s3"
|
||||
? createS3CompatibleStorageAdapter(sqlite, {
|
||||
bucket: process.env.EDGE_EVER_S3_BUCKET ?? "",
|
||||
region: process.env.EDGE_EVER_S3_REGION,
|
||||
endpoint: process.env.EDGE_EVER_S3_ENDPOINT,
|
||||
accessKeyId: process.env.EDGE_EVER_S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.EDGE_EVER_S3_SECRET_ACCESS_KEY,
|
||||
forcePathStyle: process.env.EDGE_EVER_S3_FORCE_PATH_STYLE
|
||||
? process.env.EDGE_EVER_S3_FORCE_PATH_STYLE === "true"
|
||||
bucket: runtimeEnvironment.EDGE_EVER_S3_BUCKET ?? "",
|
||||
region: runtimeEnvironment.EDGE_EVER_S3_REGION,
|
||||
endpoint: runtimeEnvironment.EDGE_EVER_S3_ENDPOINT,
|
||||
accessKeyId: runtimeEnvironment.EDGE_EVER_S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: runtimeEnvironment.EDGE_EVER_S3_SECRET_ACCESS_KEY,
|
||||
forcePathStyle: runtimeEnvironment.EDGE_EVER_S3_FORCE_PATH_STYLE
|
||||
? runtimeEnvironment.EDGE_EVER_S3_FORCE_PATH_STYLE === "true"
|
||||
: undefined,
|
||||
})
|
||||
: createSelfHostedStorageAdapter(sqlite, resourcesDirectory);
|
||||
|
||||
if (storageBackend === "s3" && !process.env.EDGE_EVER_S3_BUCKET) {
|
||||
throw new Error("EDGE_EVER_S3_BUCKET is required when EDGE_EVER_STORAGE_BACKEND=s3");
|
||||
}
|
||||
const env = {
|
||||
DB: storage.db,
|
||||
RESOURCES: storage.resources,
|
||||
EDGE_EVER_AUTH_USERNAME: process.env.EDGE_EVER_AUTH_USERNAME ?? "admin",
|
||||
storage,
|
||||
EDGE_EVER_AUTH_USERNAME: runtimeEnvironment.EDGE_EVER_AUTH_USERNAME ?? "admin",
|
||||
EDGE_EVER_RUNTIME: "self-hosted-bun",
|
||||
EDGE_EVER_AUTH_PASSWORD: process.env.EDGE_EVER_AUTH_PASSWORD,
|
||||
EDGE_EVER_AUTH_PASSWORD_HASH: process.env.EDGE_EVER_AUTH_PASSWORD_HASH,
|
||||
EDGE_EVER_SESSION_TTL_DAYS: process.env.EDGE_EVER_SESSION_TTL_DAYS ?? "400",
|
||||
EDGE_EVER_AUTH_LOGIN_WINDOW_SECONDS: process.env.EDGE_EVER_AUTH_LOGIN_WINDOW_SECONDS,
|
||||
EDGE_EVER_AUTH_LOGIN_USERNAME_MAX_ATTEMPTS: process.env.EDGE_EVER_AUTH_LOGIN_USERNAME_MAX_ATTEMPTS,
|
||||
EDGE_EVER_AUTH_LOGIN_USERNAME_COOLDOWN_SECONDS: process.env.EDGE_EVER_AUTH_LOGIN_USERNAME_COOLDOWN_SECONDS,
|
||||
EDGE_EVER_AUTH_LOGIN_IP_MAX_ATTEMPTS: process.env.EDGE_EVER_AUTH_LOGIN_IP_MAX_ATTEMPTS,
|
||||
EDGE_EVER_AUTH_LOGIN_IP_COOLDOWN_SECONDS: process.env.EDGE_EVER_AUTH_LOGIN_IP_COOLDOWN_SECONDS,
|
||||
EDGE_EVER_STORAGE_ENCRYPTION_KEY: process.env.EDGE_EVER_STORAGE_ENCRYPTION_KEY,
|
||||
EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY: process.env.EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY,
|
||||
EDGE_EVER_DEMO_MODE: process.env.EDGE_EVER_DEMO_MODE,
|
||||
EDGE_EVER_ALLOW_UNAUTHENTICATED: process.env.EDGE_EVER_ALLOW_UNAUTHENTICATED,
|
||||
EDGE_EVER_AUTH_PASSWORD: runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD,
|
||||
EDGE_EVER_AUTH_PASSWORD_HASH: runtimeEnvironment.EDGE_EVER_AUTH_PASSWORD_HASH,
|
||||
EDGE_EVER_SESSION_TTL_DAYS: runtimeEnvironment.EDGE_EVER_SESSION_TTL_DAYS ?? "400",
|
||||
EDGE_EVER_AUTH_LOGIN_WINDOW_SECONDS: runtimeEnvironment.EDGE_EVER_AUTH_LOGIN_WINDOW_SECONDS,
|
||||
EDGE_EVER_AUTH_LOGIN_USERNAME_MAX_ATTEMPTS: runtimeEnvironment.EDGE_EVER_AUTH_LOGIN_USERNAME_MAX_ATTEMPTS,
|
||||
EDGE_EVER_AUTH_LOGIN_USERNAME_COOLDOWN_SECONDS: runtimeEnvironment.EDGE_EVER_AUTH_LOGIN_USERNAME_COOLDOWN_SECONDS,
|
||||
EDGE_EVER_AUTH_LOGIN_IP_MAX_ATTEMPTS: runtimeEnvironment.EDGE_EVER_AUTH_LOGIN_IP_MAX_ATTEMPTS,
|
||||
EDGE_EVER_AUTH_LOGIN_IP_COOLDOWN_SECONDS: runtimeEnvironment.EDGE_EVER_AUTH_LOGIN_IP_COOLDOWN_SECONDS,
|
||||
EDGE_EVER_STORAGE_ENCRYPTION_KEY: runtimeEnvironment.EDGE_EVER_STORAGE_ENCRYPTION_KEY,
|
||||
EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY: runtimeEnvironment.EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY,
|
||||
EDGE_EVER_DEMO_MODE: runtimeEnvironment.EDGE_EVER_DEMO_MODE,
|
||||
EDGE_EVER_ALLOW_UNAUTHENTICATED: runtimeEnvironment.EDGE_EVER_ALLOW_UNAUTHENTICATED,
|
||||
};
|
||||
|
||||
const executionContext = {
|
||||
@@ -126,14 +131,15 @@ const serveStatic = async (request) => {
|
||||
};
|
||||
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
hostname: config.hostname,
|
||||
port: config.port,
|
||||
// Model providers may take longer than Bun's 10-second default to emit the
|
||||
// first streaming token. Keep the connection alive within Bun's supported range.
|
||||
idleTimeout,
|
||||
idleTimeout: config.idleTimeout,
|
||||
async fetch(request) {
|
||||
const pathname = new URL(request.url).pathname;
|
||||
if (pathname.startsWith("/api/") || pathname === "/mcp" || pathname.startsWith("/mcp/")) {
|
||||
return worker.fetch(request, env, executionContext);
|
||||
return fetchEdgeEverApp(request, env, executionContext);
|
||||
}
|
||||
return serveStatic(request);
|
||||
},
|
||||
@@ -141,5 +147,25 @@ const server = Bun.serve({
|
||||
|
||||
console.log(`[self-hosted] listening on ${server.url}`);
|
||||
console.log(`[self-hosted] data directory: ${dataDirectory}`);
|
||||
console.log(`[self-hosted] storage backend: ${storageBackend}`);
|
||||
console.log(`[self-hosted] idle timeout: ${idleTimeout}s`);
|
||||
console.log(`[self-hosted] storage backend: ${config.storageBackend}`);
|
||||
console.log(`[self-hosted] idle timeout: ${config.idleTimeout}s`);
|
||||
|
||||
let shuttingDown = false;
|
||||
const shutdown = async (signal) => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log(`[self-hosted] received ${signal}; shutting down`);
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
sqlite.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
sqlite.close();
|
||||
console.log("[self-hosted] shutdown complete");
|
||||
} catch (error) {
|
||||
console.error("[self-hosted] shutdown failed", error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
};
|
||||
|
||||
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
process.once("SIGINT", () => void shutdown("SIGINT"));
|
||||
Reference in new issue
Block a user