cartethyia 2.5.7 → 2.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/dashboard/assets/{BannedPage-DE2607Tb.js → BannedPage-BKAI54IA.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-BKAI54IA.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-Duv6mfrl.js → CliToolDetailPage-CkGvxrHu.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-CkGvxrHu.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-C2PNYWzk.js → CliToolsPage-Cj8MYpeY.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-Cj8MYpeY.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-TXP9OfAM.js → CombosPage-BDXBEbWT.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-BDXBEbWT.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-B-9xnAQN.js → ConsoleLogPage-49KH4z3l.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-49KH4z3l.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-xeax9nJt.js → CustomizationPage-EHjBmNBZ.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-EHjBmNBZ.js.gz +0 -0
  13. package/dist/dashboard/assets/{HealthEventsModal-Df4jjaHJ.js → HealthEventsModal-ByUTtj7j.js} +1 -1
  14. package/dist/dashboard/assets/HealthEventsModal-ByUTtj7j.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-Dyojsm84.js → LoginPage-CYSCKT8S.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-CYSCKT8S.js.gz +0 -0
  17. package/dist/dashboard/assets/{OverviewPage-BJfg4ow6.js → OverviewPage-Bu3iqBMm.js} +1 -1
  18. package/dist/dashboard/assets/{OverviewPage-BJfg4ow6.js.gz → OverviewPage-Bu3iqBMm.js.gz} +0 -0
  19. package/dist/dashboard/assets/{ProviderDetailPage-ChCX1gmT.js → ProviderDetailPage-FHdgmgjU.js} +1 -1
  20. package/dist/dashboard/assets/ProviderDetailPage-FHdgmgjU.js.gz +0 -0
  21. package/dist/dashboard/assets/{ProvidersPage-DeWSOEUO.js → ProvidersPage-CCvYS9CF.js} +1 -1
  22. package/dist/dashboard/assets/ProvidersPage-CCvYS9CF.js.gz +0 -0
  23. package/dist/dashboard/assets/{ProxyPage-CkpWln2S.js → ProxyPage-HOl9cjxl.js} +1 -1
  24. package/dist/dashboard/assets/ProxyPage-HOl9cjxl.js.gz +0 -0
  25. package/dist/dashboard/assets/{QuotaPage-B3VOgQmt.js → QuotaPage-MUGzIpTK.js} +1 -1
  26. package/dist/dashboard/assets/QuotaPage-MUGzIpTK.js.gz +0 -0
  27. package/dist/dashboard/assets/{SettingsPage-C_klIZom.js → SettingsPage-CTdF5TCa.js} +1 -1
  28. package/dist/dashboard/assets/SettingsPage-CTdF5TCa.js.gz +0 -0
  29. package/dist/dashboard/assets/{SetupPage-BYph-InK.js → SetupPage-Bra7rkKx.js} +1 -1
  30. package/dist/dashboard/assets/SetupPage-Bra7rkKx.js.gz +0 -0
  31. package/dist/dashboard/assets/{SortableList-CvyQCYWJ.js → SortableList-BRscd-vl.js} +1 -1
  32. package/dist/dashboard/assets/SortableList-BRscd-vl.js.gz +0 -0
  33. package/dist/dashboard/assets/{StudioPage-Cm1JYilI.js → StudioPage-BeevnFtM.js} +1 -1
  34. package/dist/dashboard/assets/StudioPage-BeevnFtM.js.gz +0 -0
  35. package/dist/dashboard/assets/{UsagePage-DlFnc3_S.js → UsagePage-BWik9fpu.js} +1 -1
  36. package/dist/dashboard/assets/UsagePage-BWik9fpu.js.gz +0 -0
  37. package/dist/dashboard/assets/{entry-Bog0p925.js → entry-BqZ5vrM7.js} +3 -3
  38. package/dist/dashboard/assets/entry-BqZ5vrM7.js.gz +0 -0
  39. package/dist/dashboard/assets/{index-CZQIrdoX.js → index-9ARd3FIs.js} +2 -2
  40. package/dist/dashboard/assets/index-9ARd3FIs.js.gz +0 -0
  41. package/dist/dashboard/assets/{select-BPpSsBgL.js → select-D_BBhBEF.js} +1 -1
  42. package/dist/dashboard/assets/select-D_BBhBEF.js.gz +0 -0
  43. package/dist/dashboard/index.html +1 -1
  44. package/dist/dashboard/index.html.gz +0 -0
  45. package/dist/dashboard/precompressed-assets.json +21 -21
  46. package/package.json +3 -1
  47. package/scripts/build/aot.ts +143 -0
  48. package/scripts/build/build.ts +24 -0
  49. package/scripts/build/compile.ts +88 -0
  50. package/scripts/build/restart.ts +120 -0
  51. package/scripts/build/start.ts +25 -0
  52. package/scripts/health.ts +186 -0
  53. package/dist/dashboard/assets/BannedPage-DE2607Tb.js.gz +0 -0
  54. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js.gz +0 -0
  55. package/dist/dashboard/assets/CliToolsPage-C2PNYWzk.js.gz +0 -0
  56. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js.gz +0 -0
  57. package/dist/dashboard/assets/ConsoleLogPage-B-9xnAQN.js.gz +0 -0
  58. package/dist/dashboard/assets/CustomizationPage-xeax9nJt.js.gz +0 -0
  59. package/dist/dashboard/assets/HealthEventsModal-Df4jjaHJ.js.gz +0 -0
  60. package/dist/dashboard/assets/LoginPage-Dyojsm84.js.gz +0 -0
  61. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js.gz +0 -0
  62. package/dist/dashboard/assets/ProvidersPage-DeWSOEUO.js.gz +0 -0
  63. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js.gz +0 -0
  64. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js.gz +0 -0
  65. package/dist/dashboard/assets/SettingsPage-C_klIZom.js.gz +0 -0
  66. package/dist/dashboard/assets/SetupPage-BYph-InK.js.gz +0 -0
  67. package/dist/dashboard/assets/SortableList-CvyQCYWJ.js.gz +0 -0
  68. package/dist/dashboard/assets/StudioPage-Cm1JYilI.js.gz +0 -0
  69. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js.gz +0 -0
  70. package/dist/dashboard/assets/entry-Bog0p925.js.gz +0 -0
  71. package/dist/dashboard/assets/index-CZQIrdoX.js.gz +0 -0
  72. package/dist/dashboard/assets/select-BPpSsBgL.js.gz +0 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Compiles the standalone gateway binary from the AOT build output.
3
+ *
4
+ * Two things here are load-bearing and neither is obvious:
5
+ *
6
+ * 1. **The entry is `dist/main.js`, not `src/main.ts`.** `aot.ts` runs
7
+ * Elysia's AOT plugin, which rewrites TypeBox imports into statically wired
8
+ * mirrors in its output. Bundling the raw source instead discards that work,
9
+ * and Elysia's CommonJS build reaches TypeBox through a lazy
10
+ * `require("typebox/type")` that the bundler cannot follow — the compiled
11
+ * binary then dies at startup with `Cannot find module 'typebox/type'`,
12
+ * because a standalone executable has no node_modules to resolve against.
13
+ *
14
+ * 2. **`NODE_ENV` is set to `production` here, at build time, because Bun bakes
15
+ * it into the binary.** `bun build --compile` replaces `process.env.NODE_ENV`
16
+ * with a literal, so the value present when this runs is the value the
17
+ * binary reports forever after — a runtime `NODE_ENV=production` cannot
18
+ * change it. What depends on it is the logger: the `pino-pretty` transport
19
+ * is attached only in development, and that transport spawns a worker thread
20
+ * loading `real-require`, which a standalone executable does not carry. A
21
+ * binary built without this line therefore bundles a logger that crashes on
22
+ * first use. The migrations folder is not part of this decision —
23
+ * `resolveMigrationsFolder()` resolves `<cwd>/migrations` unconditionally.
24
+ *
25
+ * Usage: `bun run scripts/build/compile.ts [--outfile dist/cartethyia]`
26
+ */
27
+ const DEFAULT_OUTFILE = "dist/cartethyia";
28
+
29
+ /**
30
+ * The artifact path `bun build --compile` actually writes for `outfile`.
31
+ *
32
+ * Bun appends `.exe` when it compiles for Windows, so the extensionless
33
+ * `dist/cartethyia` handed to it never exists on that platform. This is the one
34
+ * place that knows where the compiled binary lands; `start-production.ts`
35
+ * resolves its target from here instead of re-deriving the name, because a
36
+ * launcher that probed the requested path reported "run bun run build first"
37
+ * immediately after a successful build.
38
+ */
39
+ export function compiledBinaryPath(outfile: string = DEFAULT_OUTFILE): string {
40
+ return process.platform === "win32" && !outfile.toLowerCase().endsWith(".exe")
41
+ ? `${outfile}.exe`
42
+ : outfile;
43
+ }
44
+
45
+ /** Reads `--outfile <path>`, falling back to a positional path or the default. */
46
+ export function resolveOutfile(argv: readonly string[]): string {
47
+ const flag = argv.indexOf("--outfile");
48
+ if (flag !== -1) {
49
+ const value = argv[flag + 1];
50
+ if (value === undefined || value.length === 0) {
51
+ throw new Error("--outfile requires a path");
52
+ }
53
+ return value;
54
+ }
55
+ return argv.find((arg) => arg.length > 0 && !arg.startsWith("-")) ?? DEFAULT_OUTFILE;
56
+ }
57
+
58
+ export async function buildBinary(
59
+ outfile: string = DEFAULT_OUTFILE,
60
+ buildFn: typeof Bun.build = Bun.build,
61
+ ): Promise<void> {
62
+ // Baked into the output; see the note above before changing it. Substituted
63
+ // through `define` rather than by assigning `process.env`, because this module
64
+ // is imported by tests in the same process — mutating the environment would
65
+ // flip `NODE_ENV` for every later suite in that process.
66
+ const result = await buildFn({
67
+ entrypoints: ["dist/main.js"],
68
+ minify: true,
69
+ target: "bun",
70
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
71
+ compile: {
72
+ outfile,
73
+ },
74
+ });
75
+
76
+ if (result.logs.length > 0) {
77
+ for (const log of result.logs) console.error(`[binary] ${log.message}`);
78
+ throw new Error(`binary build emitted ${result.logs.length} diagnostic(s)`);
79
+ }
80
+ console.log(`[binary] compiled ${compiledBinaryPath(outfile)}`);
81
+ }
82
+
83
+ if (import.meta.main) {
84
+ await buildBinary(resolveOutfile(process.argv.slice(2))).catch((error: unknown) => {
85
+ console.error("[binary] build failed:", error);
86
+ process.exit(1);
87
+ });
88
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * restart.ts — rebuild and restart the compiled gateway gracefully.
4
+ *
5
+ * Why this exists: a hard kill (`taskkill /F`, `kill -9`) truncates every
6
+ * in-flight response. On Linux the process handles SIGTERM/SIGINT and drains,
7
+ * but Windows has no deliverable catchable signal — `process.kill(pid,
8
+ * "SIGTERM")` of a console-less Bun process runs no JS handler (measured). So
9
+ * this script stops the running instance through the drain endpoint when
10
+ * `CARTETHYIA_DRAIN_TOKEN` is set (loopback + `x-drain-token`), and only falls
11
+ * back to a hard kill when no token is configured.
12
+ *
13
+ * Env:
14
+ * CARTETHYIA_DRAIN_TOKEN token for POST /admin/drain (required for graceful stop)
15
+ * PORT gateway port (default 12800)
16
+ * DRAIN_WAIT_MS how long to wait for the drain to finish (default 30000)
17
+ */
18
+ import { compiledBinaryPath } from "./compile";
19
+
20
+ function intEnv(name: string, fallback: number): number {
21
+ const raw = process.env[name];
22
+ if (raw === undefined || raw.trim() === "") return fallback;
23
+ const value = Number(raw);
24
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
25
+ }
26
+
27
+ const PORT = intEnv("PORT", 12800);
28
+ const DRAIN_WAIT_MS = intEnv("DRAIN_WAIT_MS", 30_000);
29
+
30
+ /** PIDs currently listening on `port` (best-effort, platform-specific). */
31
+ async function listeningPids(port: number): Promise<number[]> {
32
+ try {
33
+ if (process.platform === "win32") {
34
+ const proc = Bun.spawnSync(["netstat", "-ano", "-p", "TCP"]);
35
+ const out = new TextDecoder().decode(proc.stdout);
36
+ const pids = new Set<number>();
37
+ for (const line of out.split(/\r?\n/)) {
38
+ if (!line.includes(`:${port} `) || !line.includes("LISTENING")) continue;
39
+ const pid = Number(line.trim().split(/\s+/).pop());
40
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
41
+ }
42
+ return [...pids];
43
+ }
44
+ const proc = Bun.spawnSync(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
45
+ return new TextDecoder()
46
+ .decode(proc.stdout)
47
+ .split(/\s+/)
48
+ .map(Number)
49
+ .filter((n) => Number.isInteger(n) && n > 0);
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ async function waitForPortFree(port: number, timeoutMs: number): Promise<boolean> {
56
+ const deadline = Date.now() + timeoutMs;
57
+ while (Date.now() < deadline) {
58
+ if ((await listeningPids(port)).length === 0) return true;
59
+ await new Promise((r) => setTimeout(r, 250));
60
+ }
61
+ return (await listeningPids(port)).length === 0;
62
+ }
63
+
64
+ async function stopGracefully(): Promise<boolean> {
65
+ const token = process.env.CARTETHYIA_DRAIN_TOKEN?.trim();
66
+ if (token === undefined || token.length === 0) {
67
+ console.warn("[restart] CARTETHYIA_DRAIN_TOKEN unset — cannot drain gracefully");
68
+ return false;
69
+ }
70
+ try {
71
+ const response = await fetch(`http://127.0.0.1:${PORT}/admin/drain`, {
72
+ method: "POST",
73
+ headers: { "x-drain-token": token },
74
+ });
75
+ if (!response.ok) {
76
+ console.warn(`[restart] drain request returned HTTP ${response.status}`);
77
+ return false;
78
+ }
79
+ console.log("[restart] drain acknowledged; waiting for the process to exit");
80
+ return await waitForPortFree(PORT, DRAIN_WAIT_MS);
81
+ } catch (error) {
82
+ console.warn(`[restart] drain request failed: ${(error as Error).message}`);
83
+ return false;
84
+ }
85
+ }
86
+
87
+ async function hardKill(): Promise<void> {
88
+ const pids = await listeningPids(PORT);
89
+ if (pids.length === 0) return;
90
+ console.warn(`[restart] force-killing pid(s) ${pids.join(", ")} on :${PORT}`);
91
+ for (const pid of pids) {
92
+ try {
93
+ process.kill(pid, "SIGKILL");
94
+ } catch {
95
+ // Already gone.
96
+ }
97
+ }
98
+ await waitForPortFree(PORT, 5_000);
99
+ }
100
+
101
+ async function main(): Promise<number> {
102
+ const graceful = await stopGracefully();
103
+ if (!graceful) await hardKill();
104
+
105
+ console.log("[restart] building");
106
+ const build = Bun.spawnSync(["bun", "run", "build"], { stdout: "inherit", stderr: "inherit" });
107
+ if (build.exitCode !== 0) {
108
+ console.error("[restart] build failed");
109
+ return build.exitCode ?? 1;
110
+ }
111
+
112
+ const binary = compiledBinaryPath();
113
+ console.log(`[restart] starting ${binary}`);
114
+ const child = Bun.spawn([binary], { stdin: "ignore", stdout: "inherit", stderr: "inherit" });
115
+ return await child.exited;
116
+ }
117
+
118
+ if (import.meta.main) {
119
+ process.exit(await main());
120
+ }
@@ -0,0 +1,25 @@
1
+ /** Runs the compiled binary from the repository root. */
2
+ import { compiledBinaryPath } from "./compile";
3
+
4
+ /** Runs the compiled binary with inherited stdio; resolves to its exit code. */
5
+ export async function startBinary(): Promise<number> {
6
+ const binary = compiledBinaryPath();
7
+ if (!(await Bun.file(binary).exists())) {
8
+ throw new Error(`${binary} is missing; run bun run build first`);
9
+ }
10
+
11
+ process.env.NODE_ENV ??= "production";
12
+ process.env.CARTETHYIA_SERVER_MAX_BODY_BYTES ??= String(8 * 1024 * 1024 * 1024);
13
+
14
+ const child = Bun.spawn([binary], {
15
+ env: process.env,
16
+ stdin: "inherit",
17
+ stdout: "inherit",
18
+ stderr: "inherit",
19
+ });
20
+ return await child.exited;
21
+ }
22
+
23
+ if (import.meta.main) {
24
+ process.exit(await startBinary());
25
+ }
@@ -0,0 +1,186 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { Client } from "pg";
5
+ import Redis from "ioredis";
6
+ import { hasPlaceholderSecrets, readEnvFile } from "./cli/env";
7
+ import { parseServiceUrl, probeTcpService } from "./cli/services";
8
+ import { resolveDataDir, resolveDbMode } from "../src/persistence/db-mode";
9
+ import { bunVersionAtLeast, readRequiredBunVersion } from "./cli/version";
10
+
11
+ export type CheckPhase = "preflight" | "configuration" | "runtime";
12
+ export type CheckStatus = "pass" | "warn" | "fail" | "skip";
13
+ export interface CheckResult {
14
+ id: string;
15
+ phase: CheckPhase;
16
+ status: CheckStatus;
17
+ required: boolean;
18
+ summary: string;
19
+ remediation?: string;
20
+ }
21
+ export interface HealthContext {
22
+ target: "local" | "vps" | "docker";
23
+ env: Record<string, string>;
24
+ envPath: string;
25
+ port: number;
26
+ phases: readonly CheckPhase[];
27
+ }
28
+ export interface HealthDependencies {
29
+ dockerCompose: () => Promise<boolean>;
30
+ postgres: (url: string) => Promise<void>;
31
+ redis: (url: string) => Promise<void>;
32
+ tcp: typeof probeTcpService;
33
+ ready: (port: number) => Promise<string | null>;
34
+ writable: (path: string) => Promise<boolean>;
35
+ exists: (path: string) => Promise<boolean>;
36
+ }
37
+ const defaultDependencies: HealthDependencies = {
38
+ async dockerCompose() {
39
+ try {
40
+ return Bun.spawnSync(["docker", "compose", "version"], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
41
+ } catch {
42
+ return false;
43
+ }
44
+ },
45
+ async postgres(url) {
46
+ const client = new Client({ connectionString: url, connectionTimeoutMillis: 3_000 });
47
+ try {
48
+ await client.connect();
49
+ await client.query("SELECT 1");
50
+ } finally {
51
+ await client.end().catch(() => undefined);
52
+ }
53
+ },
54
+ async redis(url) {
55
+ const client = new Redis(url, { lazyConnect: true, connectTimeout: 3_000, maxRetriesPerRequest: 0 });
56
+ try {
57
+ await client.connect();
58
+ const result = await client.ping();
59
+ if (result !== "PONG") throw new Error(`Unexpected Redis response: ${result}`);
60
+ } finally {
61
+ client.disconnect();
62
+ }
63
+ },
64
+ tcp: probeTcpService,
65
+ async ready(port) {
66
+ try {
67
+ const response = await fetch(`http://127.0.0.1:${port}/health/ready`, { signal: AbortSignal.timeout(3_000) });
68
+ return response.ok ? null : `HTTP ${response.status}`;
69
+ } catch (error) {
70
+ return error instanceof Error ? error.message : "connection failed";
71
+ }
72
+ },
73
+ async writable(path) {
74
+ try {
75
+ await access(path, constants.W_OK);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ },
81
+ async exists(path) {
82
+ try {
83
+ await access(path, constants.F_OK);
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ },
89
+ };
90
+ function result(id: string, phase: CheckPhase, status: CheckStatus, summary: string, required = true, remediation?: string): CheckResult {
91
+ return { id, phase, status, required, summary, ...(remediation ? { remediation } : {}) };
92
+ }
93
+ function errorMessage(error: unknown): string {
94
+ return error instanceof Error ? error.message : String(error);
95
+ }
96
+ export async function runHealthChecks(
97
+ context: HealthContext,
98
+ dependencies: HealthDependencies = defaultDependencies,
99
+ ): Promise<CheckResult[]> {
100
+ const results: CheckResult[] = [];
101
+ const phases = new Set(context.phases);
102
+ const env = { ...(await readEnvFile(context.envPath)), ...context.env };
103
+ if (phases.has("preflight")) {
104
+ const minimum = await readRequiredBunVersion();
105
+ const bunOkay = bunVersionAtLeast(Bun.version, minimum);
106
+ results.push(result("runtime-bun", "preflight", bunOkay ? "pass" : "fail", bunOkay ? `Bun ${Bun.version} satisfies ${minimum}+` : `Bun ${Bun.version} is older than required ${minimum}`, true, bunOkay ? undefined : "Install the required Bun version"));
107
+ if (context.target === "docker") {
108
+ const available = await dependencies.dockerCompose();
109
+ results.push(result("docker-compose", "preflight", available ? "pass" : "fail", available ? "Docker Compose is available" : "Docker Compose is unavailable", true, "Install Docker Engine with the Compose plugin"));
110
+ }
111
+ }
112
+ if (phases.has("configuration")) {
113
+ if (!(await dependencies.exists(context.envPath))) {
114
+ results.push(result("env-file", "configuration", "fail", `.env not found at ${context.envPath}`, true, "Run setup or the installer"));
115
+ } else {
116
+ results.push(result("env-file", "configuration", "pass", `.env exists at ${context.envPath}`));
117
+ }
118
+ let mode: "lite" | "full" | null = null;
119
+ try {
120
+ mode = resolveDbMode(env);
121
+ results.push(result("db-mode", "configuration", "pass", `Database mode: ${mode}`));
122
+ } catch (error) {
123
+ results.push(result("db-mode", "configuration", "fail", errorMessage(error), true, "Set CARTETHYIA_DB_MODE=lite or full"));
124
+ }
125
+ const key = env.CARTETHYIA_ENCRYPTION_KEY?.trim() ?? "";
126
+ results.push(!key
127
+ ? result("encryption-key", "configuration", "fail", "CARTETHYIA_ENCRYPTION_KEY is missing")
128
+ : hasPlaceholderSecrets(key)
129
+ ? result("encryption-key", "configuration", "fail", "CARTETHYIA_ENCRYPTION_KEY is still a placeholder")
130
+ : result("encryption-key", "configuration", "pass", "Encryption key is configured"));
131
+ if (mode === "lite") {
132
+ const data = resolveDataDir(env);
133
+ const paths = [data, env.CARTETHYIA_TELEMETRY_PAYLOAD_DIR?.trim() || join(data, "telemetry-payloads"), env.CARTETHYIA_INSTALL_ID_DIR?.trim() || join(data, ".cartethyia")];
134
+ for (const path of paths) {
135
+ if (context.target === "docker" && path.startsWith("/app/")) {
136
+ results.push(result(`container-path-${path}`, "configuration", "pass", `Container state path is declared: ${path}; runtime checks verify its writability`));
137
+ continue;
138
+ }
139
+ const writable = await dependencies.writable(resolve(path));
140
+ results.push(result(`writable-${path}`, "configuration", writable ? "pass" : "fail", `${writable ? "Writable" : "Not writable"}: ${path}`));
141
+ }
142
+ }
143
+ if (mode === "full") {
144
+ const url = env.DATABASE_URL?.trim() ?? "";
145
+ if (!url) results.push(result("postgres", "configuration", "fail", "DATABASE_URL is required in Full mode"));
146
+ else if (!parseServiceUrl(url)) results.push(result("postgres", "configuration", "fail", "DATABASE_URL must include a host and port"));
147
+ else if (context.target === "docker") results.push(result("postgres", "configuration", "pass", "External DATABASE_URL is configured; container readiness verifies connectivity"));
148
+ else {
149
+ try {
150
+ await dependencies.postgres(url);
151
+ results.push(result("postgres", "configuration", "pass", "PostgreSQL authenticated SELECT 1 succeeded"));
152
+ } catch (error) {
153
+ const parsed = parseServiceUrl(url)!;
154
+ const tcp = await dependencies.tcp(parsed.host, parsed.port, 3_000);
155
+ results.push(result("postgres", "configuration", tcp.success ? "warn" : "fail", tcp.success ? "PostgreSQL port is reachable, but authenticated query failed" : "PostgreSQL is unreachable or authentication failed", true, "Verify DATABASE_URL credentials and database availability"));
156
+ }
157
+ }
158
+ }
159
+ const redis = env.REDIS_URL?.trim() ?? "";
160
+ if (!redis) results.push(result("redis", "configuration", "pass", "Redis is not configured; in-memory coordination applies"));
161
+ else if (context.target === "docker") results.push(result("redis", "configuration", "pass", "Container Redis coordination is configured; container readiness verifies connectivity"));
162
+ else {
163
+ try {
164
+ await dependencies.redis(redis);
165
+ results.push(result("redis", "configuration", "pass", "Redis PING succeeded"));
166
+ } catch (error) {
167
+ results.push(result("redis", "configuration", "fail", "Configured Redis is unavailable or authentication failed", true, "Verify REDIS_URL and Redis availability"));
168
+ }
169
+ }
170
+ }
171
+ if (phases.has("runtime")) {
172
+ if (!Number.isInteger(context.port) || context.port < 1 || context.port > 65535) {
173
+ results.push(result("app-ready", "runtime", "fail", `Invalid application port: ${context.port}`));
174
+ } else {
175
+ const failure = await dependencies.ready(context.port);
176
+ results.push(result("app-ready", "runtime", failure ? "fail" : "pass", failure ? `Application is not ready: ${failure}` : `Application is ready on port ${context.port}`));
177
+ }
178
+ }
179
+ return results;
180
+ }
181
+ export function hasRequiredFailure(results: readonly CheckResult[]): boolean {
182
+ return results.some((item) => item.required && item.status === "fail");
183
+ }
184
+ export function skippedRuntimeCheck(): CheckResult {
185
+ return result("app-ready", "runtime", "skip", "Runtime readiness was not requested", false);
186
+ }