docs(deploy): document user-owned session cutover
This commit is contained in:
@@ -3,6 +3,11 @@ import path from "node:path";
|
||||
export interface AppConfig {
|
||||
host: string; port: number; harnessDir: string; thtBin: string; piBin: string;
|
||||
authMode: "none" | "mock" | "upstream";
|
||||
sessionStorage: {
|
||||
mode: "local" | "postgres";
|
||||
host?: string; port?: number; database?: string; runtimeUser?: string;
|
||||
runtimePasswordFile?: string; sslmode?: "verify-ca" | "verify-full"; sslrootcert?: string;
|
||||
};
|
||||
defaults: { provider?: string; model?: string; thinking?: string };
|
||||
maxPiProcesses: number;
|
||||
settingsFile: string;
|
||||
@@ -20,6 +25,43 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
if (env.THOTH_PUBLIC_EXPOSURE === "true" && authMode !== "upstream") {
|
||||
throw new Error("public exposure requires AUTH_MODE=upstream behind a trusted proxy");
|
||||
}
|
||||
const sessionStorageMode = env.THT_SESSION_STORAGE ?? "local";
|
||||
if (sessionStorageMode !== "local" && sessionStorageMode !== "postgres") {
|
||||
throw new Error("session storage configuration is invalid");
|
||||
}
|
||||
if (sessionStorageMode === "local" && env.THOTH_PUBLIC_EXPOSURE === "true") {
|
||||
throw new Error("local session storage requires loopback-only deployment");
|
||||
}
|
||||
const sessionStorage: AppConfig["sessionStorage"] = { mode: sessionStorageMode };
|
||||
if (sessionStorageMode === "postgres") {
|
||||
const host = env.THT_SESSION_DB_HOST;
|
||||
const database = env.THT_SESSION_DB_NAME;
|
||||
const runtimeUser = env.THT_SESSION_RUNTIME_USER;
|
||||
const runtimePasswordFile = env.THT_SESSION_RUNTIME_PASSWORD_FILE;
|
||||
const sslmode = env.THT_SESSION_DB_SSLMODE;
|
||||
const sslrootcert = env.THT_SESSION_DB_SSLROOTCERT;
|
||||
const port = Number(env.THT_SESSION_DB_PORT ?? 5432);
|
||||
if (
|
||||
authMode !== "upstream"
|
||||
|| !host || !database || !runtimeUser
|
||||
|| !runtimePasswordFile || !path.isAbsolute(runtimePasswordFile)
|
||||
|| (sslmode !== "verify-ca" && sslmode !== "verify-full")
|
||||
|| !sslrootcert || !path.isAbsolute(sslrootcert)
|
||||
|| !Number.isInteger(port) || port < 1 || port > 65535
|
||||
) {
|
||||
if (authMode !== "upstream") {
|
||||
throw new Error("server session storage requires AUTH_MODE=upstream");
|
||||
}
|
||||
throw new Error("server session storage configuration is invalid");
|
||||
}
|
||||
sessionStorage.host = host;
|
||||
sessionStorage.port = port;
|
||||
sessionStorage.database = database;
|
||||
sessionStorage.runtimeUser = runtimeUser;
|
||||
sessionStorage.runtimePasswordFile = runtimePasswordFile;
|
||||
sessionStorage.sslmode = sslmode;
|
||||
sessionStorage.sslrootcert = sslrootcert;
|
||||
}
|
||||
const modelApiKeyFile = env.THT_MODEL_API_KEY_FILE;
|
||||
if (modelApiKeyFile !== undefined && (
|
||||
modelApiKeyFile.trim() !== modelApiKeyFile
|
||||
@@ -48,6 +90,7 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
thtBin: env.THT_BIN ?? "tht",
|
||||
piBin: env.PI_BIN ?? "pi",
|
||||
authMode: authMode as AppConfig["authMode"],
|
||||
sessionStorage,
|
||||
defaults: { provider: env.PI_PROVIDER, model: env.PI_MODEL, thinking: env.PI_THINKING },
|
||||
maxPiProcesses: Number(env.MAX_PI_PROCESSES ?? 4),
|
||||
settingsFile: env.SETTINGS_FILE ?? "data/settings.json",
|
||||
|
||||
@@ -44,9 +44,60 @@ test("loadConfig accepts an authenticated upstream trust boundary", () => {
|
||||
expect(loadConfig({
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
AUTH_MODE: "upstream",
|
||||
THT_SESSION_STORAGE: "postgres",
|
||||
THT_SESSION_DB_HOST: "db.internal",
|
||||
THT_SESSION_DB_NAME: "thoth",
|
||||
THT_SESSION_RUNTIME_USER: "thoth_sessions_app",
|
||||
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password",
|
||||
THT_SESSION_DB_SSLMODE: "verify-full",
|
||||
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem",
|
||||
}).authMode).toBe("upstream");
|
||||
});
|
||||
|
||||
test("loadConfig requires direct PostgreSQL TLS inputs for the public server session store", () => {
|
||||
const env = {
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
AUTH_MODE: "upstream",
|
||||
THT_SESSION_STORAGE: "postgres",
|
||||
THT_SESSION_DB_HOST: "db.internal",
|
||||
THT_SESSION_DB_NAME: "thoth",
|
||||
THT_SESSION_RUNTIME_USER: "thoth_sessions_app",
|
||||
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password",
|
||||
THT_SESSION_DB_SSLMODE: "verify-full",
|
||||
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem",
|
||||
};
|
||||
|
||||
expect(loadConfig(env).sessionStorage).toMatchObject({
|
||||
mode: "postgres",
|
||||
host: "db.internal",
|
||||
port: 5432,
|
||||
database: "thoth",
|
||||
runtimeUser: "thoth_sessions_app",
|
||||
runtimePasswordFile: "/run/secrets/session_runtime_password",
|
||||
sslmode: "verify-full",
|
||||
sslrootcert: "/run/secrets/session_ca.pem",
|
||||
});
|
||||
for (const required of [
|
||||
"THT_SESSION_DB_HOST", "THT_SESSION_DB_NAME", "THT_SESSION_RUNTIME_USER",
|
||||
"THT_SESSION_RUNTIME_PASSWORD_FILE", "THT_SESSION_DB_SSLMODE", "THT_SESSION_DB_SSLROOTCERT",
|
||||
]) {
|
||||
const missing = { ...env, [required]: undefined };
|
||||
expect(() => loadConfig(missing)).toThrow(/server session storage configuration is invalid/);
|
||||
}
|
||||
});
|
||||
|
||||
test("loadConfig rejects public local storage and server storage without upstream auth", () => {
|
||||
expect(() => loadConfig({
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
AUTH_MODE: "upstream",
|
||||
THT_SESSION_STORAGE: "local",
|
||||
})).toThrow(/local session storage requires loopback-only deployment/);
|
||||
expect(() => loadConfig({
|
||||
THT_SESSION_STORAGE: "postgres",
|
||||
AUTH_MODE: "none",
|
||||
})).toThrow(/server session storage requires AUTH_MODE=upstream/);
|
||||
});
|
||||
|
||||
test("loadConfig accepts only an absolute generic model key file", () => {
|
||||
expect(loadConfig({ THT_MODEL_API_KEY_FILE: "/run/secrets/model_api_key" }).modelApiKeyFile)
|
||||
.toBe("/run/secrets/model_api_key");
|
||||
|
||||
@@ -15,6 +15,13 @@ test("GET /health remains available to container probes in upstream auth mode",
|
||||
THT_HARNESS_DIR: "/tmp/h",
|
||||
AUTH_MODE: "upstream",
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
THT_SESSION_STORAGE: "postgres",
|
||||
THT_SESSION_DB_HOST: "db.internal",
|
||||
THT_SESSION_DB_NAME: "thoth",
|
||||
THT_SESSION_RUNTIME_USER: "thoth_sessions_app",
|
||||
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password",
|
||||
THT_SESSION_DB_SSLMODE: "verify-full",
|
||||
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem",
|
||||
}));
|
||||
const res = await app.inject({ method: "GET", url: "/health" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user