@or3/cloud 0.1.112 → 0.1.116
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.
- package/assets/dashboard-lifecycle.mjs +59 -0
- package/dist/cli.mjs +371 -245
- package/package.json +4 -4
package/dist/cli.mjs
CHANGED
|
@@ -133,7 +133,7 @@ import { basename as basename8, resolve as resolve17 } from "node:path";
|
|
|
133
133
|
|
|
134
134
|
// src/commands/adopt.ts
|
|
135
135
|
import { chmod as chmod5, mkdir as mkdir5 } from "node:fs/promises";
|
|
136
|
-
import { basename as basename3, join as
|
|
136
|
+
import { basename as basename3, join as join17, resolve as resolve7 } from "node:path";
|
|
137
137
|
|
|
138
138
|
// src/backup/archive.ts
|
|
139
139
|
import { createReadStream as createReadStream3 } from "node:fs";
|
|
@@ -194,7 +194,7 @@ import { readFileSync } from "node:fs";
|
|
|
194
194
|
import { join, resolve } from "node:path";
|
|
195
195
|
import { fileURLToPath } from "node:url";
|
|
196
196
|
var PACKAGE_ROOT = resolve(fileURLToPath(new URL("../", import.meta.url)));
|
|
197
|
-
var PACKAGE_VERSION = "0.1.
|
|
197
|
+
var PACKAGE_VERSION = "0.1.116";
|
|
198
198
|
var IMAGE_REPOSITORY = "ghcr.io/saluana/or3-chat";
|
|
199
199
|
var ASSET_ROOT = resolve(fileURLToPath(new URL("../assets/", import.meta.url)));
|
|
200
200
|
var SHA256_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -1866,12 +1866,12 @@ END {
|
|
|
1866
1866
|
}
|
|
1867
1867
|
|
|
1868
1868
|
// src/backup/manifests.ts
|
|
1869
|
-
import {
|
|
1869
|
+
import { createHmac, randomBytes as randomBytes5, timingSafeEqual } from "node:crypto";
|
|
1870
1870
|
import { lstat as lstat3, readdir as readdir2 } from "node:fs/promises";
|
|
1871
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1871
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
1872
1872
|
|
|
1873
1873
|
// src/deployment/assets.ts
|
|
1874
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
1874
|
+
import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
|
|
1875
1875
|
import { chmod as chmod3, copyFile as copyFile2, mkdir as mkdir3, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1876
1876
|
import { join as join8 } from "node:path";
|
|
1877
1877
|
var MANAGED_ASSET_INVENTORY_VERSION = 4;
|
|
@@ -1998,11 +1998,12 @@ async function verifiedManagedAssetContents(backupPath, manifest2) {
|
|
|
1998
1998
|
throw new Error(`Backup ${manifest2.backupId} has an invalid checksum for managed asset ${name}.`);
|
|
1999
1999
|
}
|
|
2000
2000
|
const source = join8(backupPath, "managed-assets", name);
|
|
2001
|
-
const
|
|
2001
|
+
const contents = await readFile2(source);
|
|
2002
|
+
const actualSha = createHash2("sha256").update(contents).digest("hex");
|
|
2002
2003
|
if (actualSha !== expectedSha) {
|
|
2003
2004
|
throw new Error(`Backup managed asset checksum mismatch for ${name}. Expected ${expectedSha}, got ${actualSha}.`);
|
|
2004
2005
|
}
|
|
2005
|
-
assets.set(name,
|
|
2006
|
+
assets.set(name, contents);
|
|
2006
2007
|
}
|
|
2007
2008
|
return assets;
|
|
2008
2009
|
}
|
|
@@ -2405,10 +2406,236 @@ async function captureRemoteSnapshot(directory, state, env, backupDir) {
|
|
|
2405
2406
|
}
|
|
2406
2407
|
|
|
2407
2408
|
// src/backup/vault.ts
|
|
2408
|
-
import { createHash as
|
|
2409
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
|
|
2409
2410
|
import { constants } from "node:fs";
|
|
2410
|
-
import { lstat as lstat2, open as open2
|
|
2411
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2411
|
+
import { lstat as lstat2, open as open2 } from "node:fs/promises";
|
|
2412
|
+
import { isAbsolute as isAbsolute2, join as join13 } from "node:path";
|
|
2413
|
+
|
|
2414
|
+
// src/runtime/project.ts
|
|
2415
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2416
|
+
import { join as join11 } from "node:path";
|
|
2417
|
+
|
|
2418
|
+
// src/runtime/health.ts
|
|
2419
|
+
var DEEP_HEALTH_TIMEOUT_MS = 180000;
|
|
2420
|
+
var HEALTH_SCRIPT = "const fs=require('node:fs');try{const auth=process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth',storage=process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs',sync=process.env.OR3_SYNC_PROVIDER||'sqlite';if(sync!=='sqlite'||!(auth==='basic-auth'&&storage==='fs'||auth==='clerk'&&storage==='s3')||process.env.AUTH_PROVIDER&&process.env.OR3_AUTH_PROVIDER&&process.env.AUTH_PROVIDER!==process.env.OR3_AUTH_PROVIDER)throw Error('profile');fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);const paths=auth==='basic-auth'?[process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH]:[process.env.OR3_SQLITE_DB_PATH];if(paths.some(p=>!p))throw Error('database path');for(const path of paths){const fd=fs.openSync(path,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async response=>{const body=await response.json().catch(()=>({}));if(!response.ok||body.status!=='ok'||body.providers?.auth?.provider!==(process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth')||body.providers?.sync?.provider!=='sqlite'||body.providers?.storage?.provider!==(process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs'))process.exit(1)}).catch(()=>process.exit(1))";
|
|
2421
|
+
var VERIFY_DATABASES_SCRIPT = `
|
|
2422
|
+
const fs = require('node:fs');
|
|
2423
|
+
const Database = require('/app/.output/server/node_modules/better-sqlite3');
|
|
2424
|
+
const results = [];
|
|
2425
|
+
const auth = process.env.AUTH_PROVIDER || process.env.OR3_AUTH_PROVIDER || 'basic-auth';
|
|
2426
|
+
const storage = process.env.NUXT_PUBLIC_STORAGE_PROVIDER || 'fs', sync = process.env.OR3_SYNC_PROVIDER || 'sqlite';
|
|
2427
|
+
if (sync !== 'sqlite' || !(auth === 'basic-auth' && storage === 'fs' || auth === 'clerk' && storage === 's3') || process.env.AUTH_PROVIDER && process.env.OR3_AUTH_PROVIDER && process.env.AUTH_PROVIDER !== process.env.OR3_AUTH_PROVIDER) throw new Error('Unsupported auth profile');
|
|
2428
|
+
const paths = auth === 'basic-auth' ? [process.env.OR3_BASIC_AUTH_DB_PATH, process.env.OR3_SQLITE_DB_PATH] : [process.env.OR3_SQLITE_DB_PATH];
|
|
2429
|
+
if (paths.some(path => !path)) throw new Error('Missing managed database path');
|
|
2430
|
+
for (const path of paths) {
|
|
2431
|
+
const info = fs.statSync(path);
|
|
2432
|
+
if (info.uid !== 65532 || info.gid !== 65532) throw new Error(path + ' must be owned by 65532:65532');
|
|
2433
|
+
const db = new Database(path, { readonly: true, fileMustExist: true });
|
|
2434
|
+
const quickCheck = db.pragma('quick_check', { simple: true });
|
|
2435
|
+
const tables = db.prepare("select count(*) as count from sqlite_master where type = 'table'").get().count;
|
|
2436
|
+
db.close();
|
|
2437
|
+
if (quickCheck !== 'ok') throw new Error(path + ' quick_check failed: ' + quickCheck);
|
|
2438
|
+
results.push({ path, quickCheck, tables });
|
|
2439
|
+
}
|
|
2440
|
+
if (process.env.OR3_FORCE_HTTPS === 'true' && process.env.NUXT_SECURITY_PROXY_TRUST_PROXY !== 'true') {
|
|
2441
|
+
throw new Error('NUXT_SECURITY_PROXY_TRUST_PROXY must be true for a managed public deployment');
|
|
2442
|
+
}
|
|
2443
|
+
console.log(JSON.stringify(results));
|
|
2444
|
+
`;
|
|
2445
|
+
var CONTAINER_NODE = "/nodejs/bin/node";
|
|
2446
|
+
var LEGACY_CONTAINER_NODE = "/usr/local/bin/node";
|
|
2447
|
+
var CONTAINER_NODE_SHELL = `if [ -x ${CONTAINER_NODE} ]; then exec ${CONTAINER_NODE} -e "$1"; elif [ -x ${LEGACY_CONTAINER_NODE} ]; then exec ${LEGACY_CONTAINER_NODE} -e "$1"; else exit 127; fi`;
|
|
2448
|
+
function containerNodeCommand(script) {
|
|
2449
|
+
return ["sh", "-c", CONTAINER_NODE_SHELL, "or3-node", script];
|
|
2450
|
+
}
|
|
2451
|
+
async function waitForDeepHealthWithArgs(composeCommand, directory, secrets = []) {
|
|
2452
|
+
const startedAt = Date.now();
|
|
2453
|
+
const deadline = startedAt + DEEP_HEALTH_TIMEOUT_MS;
|
|
2454
|
+
let lastError = "health check did not complete";
|
|
2455
|
+
let lastProgressAt = startedAt;
|
|
2456
|
+
while (Date.now() < deadline) {
|
|
2457
|
+
const result = await run("docker", [
|
|
2458
|
+
...composeCommand,
|
|
2459
|
+
"exec",
|
|
2460
|
+
"-T",
|
|
2461
|
+
"or3",
|
|
2462
|
+
...containerNodeCommand(HEALTH_SCRIPT)
|
|
2463
|
+
], directory);
|
|
2464
|
+
if (result.ok)
|
|
2465
|
+
return;
|
|
2466
|
+
lastError = redact(result.stderr, secrets);
|
|
2467
|
+
if (Date.now() - lastProgressAt >= 15000) {
|
|
2468
|
+
console.error(`Waiting for OR3 deep health (${Math.round((Date.now() - startedAt) / 1000)}s elapsed)…`);
|
|
2469
|
+
lastProgressAt = Date.now();
|
|
2470
|
+
}
|
|
2471
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
|
|
2472
|
+
}
|
|
2473
|
+
throw new Error(`OR3 deep health did not pass within ${DEEP_HEALTH_TIMEOUT_MS / 1000} seconds. Last error: ${lastError}`);
|
|
2474
|
+
}
|
|
2475
|
+
async function waitForDeepHealth(directory, mode, secrets = []) {
|
|
2476
|
+
try {
|
|
2477
|
+
await waitForDeepHealthWithArgs(composeArgs(directory, mode), directory, secrets);
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
const captured = await captureComposeDiagnostics(directory, mode, secrets).catch((captureError) => redact(`Diagnostics capture failed: ${captureError instanceof Error ? captureError.message : String(captureError)}`, secrets));
|
|
2480
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}
|
|
2481
|
+
Captured Docker diagnostics:
|
|
2482
|
+
${captured}
|
|
2483
|
+
Diagnostics: ${diagnostics(directory, mode)}`);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
async function probeDeepHealth(directory, mode) {
|
|
2487
|
+
const result = await run("docker", [...composeArgs(directory, mode, ["exec", "-T", "or3", ...containerNodeCommand(HEALTH_SCRIPT)])], directory);
|
|
2488
|
+
if (result.ok)
|
|
2489
|
+
return "ok";
|
|
2490
|
+
const ps = await run("docker", [...composeArgs(directory, mode, ["ps", "-q", "or3"])], directory);
|
|
2491
|
+
if (ps.ok && ps.stdout.trim())
|
|
2492
|
+
return "degraded";
|
|
2493
|
+
return "unreachable";
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/runtime/project.ts
|
|
2497
|
+
function managedVaultEnabled(directory, env) {
|
|
2498
|
+
const selected = env ?? parseEnv(readFileSync5(join11(directory, ".env"), "utf8"));
|
|
2499
|
+
return selected.SSR_AUTH_ENABLED === "true" && (selected.NUXT_SECRETS_PROVIDER ?? "or3-secrets") === "or3-secrets" && readFileSync5(join11(directory, "compose.yaml"), "utf8").includes(`
|
|
2500
|
+
or3-vault:
|
|
2501
|
+
`);
|
|
2502
|
+
}
|
|
2503
|
+
async function stopProject(directory, mode) {
|
|
2504
|
+
await compose(directory, mode, ["stop", "or3", ...managedVaultEnabled(directory) ? ["or3-vault"] : []]);
|
|
2505
|
+
}
|
|
2506
|
+
async function projectServiceRunning(directory, mode) {
|
|
2507
|
+
const result = await run("docker", composeArgs(directory, mode, ["ps", "--status", "running", "-q", "or3"]), directory);
|
|
2508
|
+
if (!result.ok)
|
|
2509
|
+
throw new Error(`Could not determine whether OR3 is running. ${result.stderr.trim()}`);
|
|
2510
|
+
return Boolean(result.stdout.trim());
|
|
2511
|
+
}
|
|
2512
|
+
async function startProject(directory, mode, env) {
|
|
2513
|
+
await assertSafeComposeBinding(directory, mode, env);
|
|
2514
|
+
const services = process.env.OR3_DASHBOARD_UPDATE_JOB_ID ? ["or3"] : [];
|
|
2515
|
+
await compose(directory, mode, ["up", "-d", "--wait", "--wait-timeout", "180", ...services], secretValues(env));
|
|
2516
|
+
await waitForDeepHealth(directory, mode, secretValues(env));
|
|
2517
|
+
await assertRunningAppImage(directory, mode, env.OR3_IMAGE);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// src/backup/config.ts
|
|
2521
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2522
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2523
|
+
import { join as join12 } from "node:path";
|
|
2524
|
+
async function readAuthenticatedBackupConfig(backupPath, manifest2) {
|
|
2525
|
+
const bytes = await readFile4(join12(backupPath, "config.env"));
|
|
2526
|
+
if (!manifest2.configSha256 || createHash3("sha256").update(bytes).digest("hex") !== manifest2.configSha256) {
|
|
2527
|
+
throw new Error(`Backup configuration checksum mismatch for ${manifest2.backupId}.`);
|
|
2528
|
+
}
|
|
2529
|
+
const text = bytes.toString("utf8");
|
|
2530
|
+
if (!Buffer.from(text, "utf8").equals(bytes))
|
|
2531
|
+
throw new Error("Backup configuration has invalid UTF-8.");
|
|
2532
|
+
return text;
|
|
2533
|
+
}
|
|
2534
|
+
function hasUnambiguousEnvSyntax(text, allowedKeys) {
|
|
2535
|
+
const keys = new Set;
|
|
2536
|
+
for (const source of text.split(/\r?\n/)) {
|
|
2537
|
+
const line = source.trim();
|
|
2538
|
+
if (!line || line.startsWith("#"))
|
|
2539
|
+
continue;
|
|
2540
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
2541
|
+
if (!match || keys.has(match[1]) || allowedKeys && !allowedKeys.has(match[1]))
|
|
2542
|
+
return false;
|
|
2543
|
+
keys.add(match[1]);
|
|
2544
|
+
const value = match[2];
|
|
2545
|
+
if (!/^'(?:[^'\\\x00\r\n]|\\['\\])*'$/.test(value) && !/^[A-Za-z0-9._:@%+=/-]*$/.test(value))
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
return true;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// src/backup/vault.ts
|
|
2552
|
+
var LEGACY_IMAGE = "ghcr.io/saluana/or3-chat:0.1.90";
|
|
2553
|
+
var LEGACY_DIGEST = "sha256:d50b70d6522574798f1a3081894b5bf8fe9eca7aa5f183ad5a562de2e4f41c12";
|
|
2554
|
+
var LEGACY_ASSETS = {
|
|
2555
|
+
"compose.yaml": "6f4bdb7901eb997710501ef46c4c837870a830b6b835ea7fcc9a2036f9e51fc8",
|
|
2556
|
+
"compose.public.yaml": "08e7b9a6e64c6d7df526a8a1372815c153765b098cdffe3af9086dd19e00b0d1",
|
|
2557
|
+
Caddyfile: "623bd4a05b89530cff6a88e33cb6bf0b98b0561de6dd734d2d5b187abc1c48dd",
|
|
2558
|
+
"compose.operator.yaml": "36ff63bed54f11b8e46daadd2dab2c5fa4b2fadde1e3d6dbd9bf9f1c37e9de86",
|
|
2559
|
+
"dashboard-operator.mjs": "47dd5d186f4e87edd962d45cdfc274fea4c95533d676e4eddf59bdc59bb51308",
|
|
2560
|
+
"dashboard-lifecycle.mjs": "628b903e290786aca97ef94ef9c0452f396b10edb11f14e620500671981ffbd2"
|
|
2561
|
+
};
|
|
2562
|
+
var LEGACY_OPERATOR_IMAGE = "ghcr.io/saluana/or3-chat@sha256:7148fc08014a0a35c1bd211243c3cf7cecb4b25cce9ec0dac1ecd7e50a61121a";
|
|
2563
|
+
var LEGACY_ENV_KEYS = new Set([
|
|
2564
|
+
"OR3_VERSION",
|
|
2565
|
+
"OR3_IMAGE",
|
|
2566
|
+
"OR3_DEPLOYMENT_ID",
|
|
2567
|
+
"OR3_COMPOSE_PROJECT",
|
|
2568
|
+
"OR3_VOLUME_NAME",
|
|
2569
|
+
"OR3_CADDY_DATA_VOLUME",
|
|
2570
|
+
"OR3_CADDY_CONFIG_VOLUME",
|
|
2571
|
+
"OR3_PORT",
|
|
2572
|
+
"SSR_AUTH_ENABLED",
|
|
2573
|
+
"AUTH_PROVIDER",
|
|
2574
|
+
"OR3_AUTH_PROVIDER",
|
|
2575
|
+
"OR3_AUTH_REGISTRATION_MODE",
|
|
2576
|
+
"OR3_AUTH_AUTO_PROVISION",
|
|
2577
|
+
"OR3_GUEST_ACCESS_ENABLED",
|
|
2578
|
+
"OR3_PLUGIN_ZIP_INSTALL_ENABLED",
|
|
2579
|
+
"OR3_ADMIN_ALLOW_REBUILD",
|
|
2580
|
+
"OR3_BASIC_AUTH_JWT_SECRET",
|
|
2581
|
+
"OR3_BASIC_AUTH_REFRESH_SECRET",
|
|
2582
|
+
"OR3_BASIC_AUTH_ACCESS_TTL_SECONDS",
|
|
2583
|
+
"OR3_BASIC_AUTH_REFRESH_TTL_SECONDS",
|
|
2584
|
+
"OR3_BASIC_AUTH_DB_PATH",
|
|
2585
|
+
"OR3_BASIC_AUTH_BOOTSTRAP_EMAIL",
|
|
2586
|
+
"OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD",
|
|
2587
|
+
"OR3_MANAGED_OWNER_EMAIL",
|
|
2588
|
+
"OR3_AUTH_INVITE_TOKEN_SECRET",
|
|
2589
|
+
"OR3_SYNC_ENABLED",
|
|
2590
|
+
"OR3_CLOUD_SYNC_ENABLED",
|
|
2591
|
+
"OR3_SYNC_PROVIDER",
|
|
2592
|
+
"OR3_SQLITE_DB_PATH",
|
|
2593
|
+
"OR3_SQLITE_PRAGMA_JOURNAL_MODE",
|
|
2594
|
+
"OR3_SQLITE_PRAGMA_SYNCHRONOUS",
|
|
2595
|
+
"OR3_SQLITE_ALLOW_IN_MEMORY",
|
|
2596
|
+
"OR3_SQLITE_STRICT",
|
|
2597
|
+
"OR3_STORAGE_ENABLED",
|
|
2598
|
+
"OR3_CLOUD_STORAGE_ENABLED",
|
|
2599
|
+
"NUXT_PUBLIC_STORAGE_PROVIDER",
|
|
2600
|
+
"OR3_STORAGE_FS_ROOT",
|
|
2601
|
+
"OR3_STORAGE_FS_TOKEN_SECRET",
|
|
2602
|
+
"OR3_STORAGE_FS_URL_TTL_SECONDS",
|
|
2603
|
+
"OR3_ADMIN_USERNAME",
|
|
2604
|
+
"OR3_ADMIN_PASSWORD",
|
|
2605
|
+
"OR3_ADMIN_JWT_SECRET",
|
|
2606
|
+
"OR3_ADMIN_JWT_EXPIRY",
|
|
2607
|
+
"OR3_PUBLIC_DOMAIN",
|
|
2608
|
+
"OR3_ALLOWED_ORIGINS",
|
|
2609
|
+
"OR3_FORCE_HTTPS",
|
|
2610
|
+
"OR3_TRUST_PROXY",
|
|
2611
|
+
"OR3_FORWARDED_FOR_HEADER",
|
|
2612
|
+
"OR3_DASHBOARD_UPDATES_ENABLED",
|
|
2613
|
+
"OR3_OPERATOR_IMAGE",
|
|
2614
|
+
"OR3_DEPLOYMENT_DIR",
|
|
2615
|
+
"OR3_OPERATOR_UID",
|
|
2616
|
+
"OR3_OPERATOR_GID",
|
|
2617
|
+
"OR3_DOCKER_SOCKET",
|
|
2618
|
+
"OR3_DOCKER_GID"
|
|
2619
|
+
]);
|
|
2620
|
+
var LEGACY_FIXED_PROFILE = {
|
|
2621
|
+
SSR_AUTH_ENABLED: "true",
|
|
2622
|
+
AUTH_PROVIDER: "basic-auth",
|
|
2623
|
+
OR3_AUTH_PROVIDER: "basic-auth",
|
|
2624
|
+
OR3_AUTH_REGISTRATION_MODE: "invite_only",
|
|
2625
|
+
OR3_AUTH_AUTO_PROVISION: "false",
|
|
2626
|
+
OR3_GUEST_ACCESS_ENABLED: "false",
|
|
2627
|
+
OR3_PLUGIN_ZIP_INSTALL_ENABLED: "false",
|
|
2628
|
+
OR3_ADMIN_ALLOW_REBUILD: "false",
|
|
2629
|
+
OR3_BASIC_AUTH_DB_PATH: "/data/auth.sqlite",
|
|
2630
|
+
OR3_SYNC_ENABLED: "true",
|
|
2631
|
+
OR3_CLOUD_SYNC_ENABLED: "true",
|
|
2632
|
+
OR3_SYNC_PROVIDER: "sqlite",
|
|
2633
|
+
OR3_SQLITE_DB_PATH: "/data/sync.sqlite",
|
|
2634
|
+
OR3_STORAGE_ENABLED: "true",
|
|
2635
|
+
OR3_CLOUD_STORAGE_ENABLED: "true",
|
|
2636
|
+
NUXT_PUBLIC_STORAGE_PROVIDER: "fs",
|
|
2637
|
+
OR3_STORAGE_FS_ROOT: "/data/storage"
|
|
2638
|
+
};
|
|
2412
2639
|
async function readAuthenticatedVaultId(path, expectedSha256) {
|
|
2413
2640
|
if (!/^[a-f0-9]{64}$/.test(expectedSha256))
|
|
2414
2641
|
throw new Error("Invalid vault backup checksum.");
|
|
@@ -2419,7 +2646,7 @@ async function readAuthenticatedVaultId(path, expectedSha256) {
|
|
|
2419
2646
|
const pathInfo = await lstat2(path, { bigint: true });
|
|
2420
2647
|
if (!before.isFile() || !pathInfo.isFile() || pathInfo.isSymbolicLink() || before.nlink !== 1n || before.size < 113n || before.size > BigInt(2 ** 30 + 112) || identity(before) !== identity(pathInfo))
|
|
2421
2648
|
throw new Error("invalid vault backup");
|
|
2422
|
-
const hash =
|
|
2649
|
+
const hash = createHash4("sha256"), buffer = Buffer.alloc(1024 * 1024), header = Buffer.alloc(96);
|
|
2423
2650
|
let offset = 0;
|
|
2424
2651
|
while (offset < Number(before.size)) {
|
|
2425
2652
|
const count = (await file.read(buffer, 0, Math.min(buffer.length, Number(before.size) - offset), offset)).bytesRead;
|
|
@@ -2442,22 +2669,31 @@ async function readAuthenticatedVaultId(path, expectedSha256) {
|
|
|
2442
2669
|
await file.close();
|
|
2443
2670
|
}
|
|
2444
2671
|
}
|
|
2445
|
-
function backupHasVaultCoverage(manifest2,
|
|
2672
|
+
async function backupHasVaultCoverage(manifest2, archivedConfig, backupPath) {
|
|
2673
|
+
if (!manifest2.configSha256 || createHash4("sha256").update(archivedConfig).digest("hex") !== manifest2.configSha256)
|
|
2674
|
+
return false;
|
|
2675
|
+
const env = parseEnv(archivedConfig);
|
|
2446
2676
|
if (Object.keys(env).some((key) => /^(?:RUNTIME_CONFIG|NUXT_SECRETS|NITRO_ENV_(?:PREFIX|EXPANSION)|NITRO_SECRETS(?:_.*)?)$/.test(key)))
|
|
2447
2677
|
return false;
|
|
2448
2678
|
const provider = env.NUXT_SECRETS_PROVIDER;
|
|
2449
|
-
if (
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
if (
|
|
2679
|
+
if (!manifest2.vaultSha256 && (!hasUnambiguousEnvSyntax(archivedConfig) || Object.keys(env).some((key) => /^(?:NUXT_SECRETS_(?!PROVIDER$)|OR3_MANAGED_VAULT(?:_|$)|NODE_|BUN_|LD_|DYLD_|PATH$)/.test(key))))
|
|
2680
|
+
return false;
|
|
2681
|
+
if (provider === undefined && manifest2.appVersion === "0.1.90" && manifest2.image === LEGACY_IMAGE && manifest2.imageDigest === LEGACY_DIGEST && !manifest2.vaultSha256) {
|
|
2682
|
+
if (!["local", "public"].includes(manifest2.mode) || env.OR3_VERSION !== manifest2.appVersion || env.OR3_IMAGE !== manifest2.image || !hasUnambiguousEnvSyntax(archivedConfig, LEGACY_ENV_KEYS) || Object.entries(LEGACY_FIXED_PROFILE).some(([key, value]) => env[key] !== value) || env.OR3_FORCE_HTTPS !== (manifest2.mode === "public" ? "true" : "false") || env.OR3_TRUST_PROXY !== (manifest2.mode === "public" ? "true" : "false") || env.OR3_DASHBOARD_UPDATES_ENABLED !== undefined && !["true", "false"].includes(env.OR3_DASHBOARD_UPDATES_ENABLED))
|
|
2683
|
+
return false;
|
|
2684
|
+
const names = ["compose.yaml", ...manifest2.mode === "public" ? ["compose.public.yaml", "Caddyfile"] : []];
|
|
2685
|
+
if (env.OR3_DASHBOARD_UPDATES_ENABLED === "true") {
|
|
2686
|
+
if (env.OR3_OPERATOR_IMAGE !== LEGACY_OPERATOR_IMAGE)
|
|
2457
2687
|
return false;
|
|
2458
|
-
|
|
2688
|
+
names.push("compose.operator.yaml", "dashboard-operator.mjs", "dashboard-lifecycle.mjs");
|
|
2459
2689
|
}
|
|
2460
|
-
|
|
2690
|
+
if (names.some((name) => manifest2.managedAssetSha256?.[name] !== LEGACY_ASSETS[name]))
|
|
2691
|
+
return false;
|
|
2692
|
+
const assets = await verifiedManagedAssetContents(backupPath, manifest2);
|
|
2693
|
+
return names.every((name) => {
|
|
2694
|
+
const bytes = assets?.get(name);
|
|
2695
|
+
return bytes !== undefined && createHash4("sha256").update(bytes).digest("hex") === LEGACY_ASSETS[name];
|
|
2696
|
+
});
|
|
2461
2697
|
}
|
|
2462
2698
|
return provider === "" || (provider === undefined || provider === "or3-secrets") && Boolean(manifest2.vaultSha256);
|
|
2463
2699
|
}
|
|
@@ -2470,13 +2706,15 @@ async function assertVaultVolumesExist(directory, state) {
|
|
|
2470
2706
|
throw new Error("Vault custody is missing or cannot be inspected. The operation is refused; preserve the encrypted backup and recover the vault key and volumes first.");
|
|
2471
2707
|
}
|
|
2472
2708
|
}
|
|
2473
|
-
async function assertExistingVaultCustody(directory, state, manifest2, backupPath,
|
|
2474
|
-
|
|
2475
|
-
if (!backupHasVaultCoverage(manifest2, backupEnv, archivedConfig))
|
|
2709
|
+
async function assertExistingVaultCustody(directory, state, manifest2, backupPath, archivedConfig) {
|
|
2710
|
+
if (!await backupHasVaultCoverage(manifest2, archivedConfig, backupPath))
|
|
2476
2711
|
throw new Error("Backup lacks the vault archive required by its saved configuration. Preserve it and use a separately reviewed recovery procedure.");
|
|
2477
|
-
if (!manifest2.vaultSha256)
|
|
2712
|
+
if (!manifest2.vaultSha256) {
|
|
2713
|
+
if (managedVaultEnabled(directory))
|
|
2714
|
+
await assertVaultVolumesExist(directory, state);
|
|
2478
2715
|
return;
|
|
2479
|
-
|
|
2716
|
+
}
|
|
2717
|
+
const expectedVaultId = await readAuthenticatedVaultId(join13(backupPath, "vault.backup"), manifest2.vaultSha256);
|
|
2480
2718
|
const dataVolume = `${state.volumeName}-vault-data`;
|
|
2481
2719
|
const keyVolume = `${state.volumeName}-vault-key`;
|
|
2482
2720
|
await assertVaultVolumesExist(directory, state);
|
|
@@ -2621,14 +2859,14 @@ function backupAuthenticationTag(key, manifestContents) {
|
|
|
2621
2859
|
}
|
|
2622
2860
|
async function writeBackupAuthentication(directory, backupPath, manifestContents) {
|
|
2623
2861
|
const key = await backupAuthenticationKey(directory, true);
|
|
2624
|
-
await writeSecure(
|
|
2862
|
+
await writeSecure(join14(backupPath, "manifest.auth"), `${backupAuthenticationTag(key, manifestContents)}
|
|
2625
2863
|
`);
|
|
2626
2864
|
}
|
|
2627
2865
|
async function assertBackupAuthentication(directory, backupPath, manifestContents) {
|
|
2628
2866
|
const key = await backupAuthenticationKey(directory);
|
|
2629
2867
|
let provided;
|
|
2630
2868
|
try {
|
|
2631
|
-
const value = (await readText(
|
|
2869
|
+
const value = (await readText(join14(backupPath, "manifest.auth"))).trim();
|
|
2632
2870
|
if (!/^[0-9a-f]{64}$/i.test(value))
|
|
2633
2871
|
throw new Error("invalid authentication tag");
|
|
2634
2872
|
provided = Buffer.from(value, "hex");
|
|
@@ -2657,7 +2895,7 @@ function classifyBackupError(error) {
|
|
|
2657
2895
|
return { code: "backup-entry-invalid", message };
|
|
2658
2896
|
}
|
|
2659
2897
|
async function inspectBackupEntry(backupsRoot, entry, directory) {
|
|
2660
|
-
const path =
|
|
2898
|
+
const path = join14(backupsRoot, entry);
|
|
2661
2899
|
let info;
|
|
2662
2900
|
try {
|
|
2663
2901
|
info = await lstat3(path);
|
|
@@ -2689,7 +2927,7 @@ async function inspectBackupEntry(backupsRoot, entry, directory) {
|
|
|
2689
2927
|
};
|
|
2690
2928
|
}
|
|
2691
2929
|
try {
|
|
2692
|
-
await lstat3(
|
|
2930
|
+
await lstat3(join14(path, "manifest.auth"));
|
|
2693
2931
|
} catch (error) {
|
|
2694
2932
|
if (error.code === "ENOENT") {
|
|
2695
2933
|
return {
|
|
@@ -2702,7 +2940,7 @@ async function inspectBackupEntry(backupsRoot, entry, directory) {
|
|
|
2702
2940
|
return { kind: "unreadable", entryName: entry, ...classifyBackupError(error) };
|
|
2703
2941
|
}
|
|
2704
2942
|
try {
|
|
2705
|
-
const raw = JSON.parse(await readText(
|
|
2943
|
+
const raw = JSON.parse(await readText(join14(path, "manifest.json")));
|
|
2706
2944
|
if (typeof raw.schemaVersion === "number" && raw.schemaVersion > 2) {
|
|
2707
2945
|
return {
|
|
2708
2946
|
kind: "unsupported",
|
|
@@ -2734,12 +2972,10 @@ async function inspectBackupEntry(backupsRoot, entry, directory) {
|
|
|
2734
2972
|
}
|
|
2735
2973
|
let bytes = 0;
|
|
2736
2974
|
for (const file of ["data.tgz", "config.env", "manifest.json", "manifest.auth", ...manifest2.vaultSha256 ? ["vault.backup"] : [], ...manifest2.schemaVersion === 2 ? [manifest2.remote.file] : []]) {
|
|
2737
|
-
bytes += (await lstat3(
|
|
2975
|
+
bytes += (await lstat3(join14(path, file))).size;
|
|
2738
2976
|
}
|
|
2739
|
-
const savedConfig = await
|
|
2740
|
-
|
|
2741
|
-
throw new Error(`Backup configuration changed during inventory for ${manifest2.backupId}.`);
|
|
2742
|
-
const recoveryArtifactsComplete = Boolean(manifest2.managedAssetSha256) && backupHasVaultCoverage(manifest2, parseEnv(savedConfig), savedConfig);
|
|
2977
|
+
const savedConfig = await readAuthenticatedBackupConfig(path, manifest2);
|
|
2978
|
+
const recoveryArtifactsComplete = Boolean(manifest2.managedAssetSha256) && await backupHasVaultCoverage(manifest2, savedConfig, path);
|
|
2743
2979
|
return {
|
|
2744
2980
|
kind: "verified",
|
|
2745
2981
|
backup: {
|
|
@@ -2849,7 +3085,7 @@ function assertBackupMatchesDeployment(manifest2, backupEnv, state, env) {
|
|
|
2849
3085
|
}
|
|
2850
3086
|
async function readManifest(backupPath, authenticatedForDirectory) {
|
|
2851
3087
|
await lifecycleFaults.beforeArchiveRead?.();
|
|
2852
|
-
const manifestContents = await readText(
|
|
3088
|
+
const manifestContents = await readText(join14(backupPath, "manifest.json"));
|
|
2853
3089
|
if (authenticatedForDirectory)
|
|
2854
3090
|
await assertBackupAuthentication(authenticatedForDirectory, backupPath, manifestContents);
|
|
2855
3091
|
const manifest2 = JSON.parse(manifestContents);
|
|
@@ -2857,15 +3093,10 @@ async function readManifest(backupPath, authenticatedForDirectory) {
|
|
|
2857
3093
|
if (unvalidated.schemaVersion !== 1 && unvalidated.schemaVersion !== 2 || !BACKUP_ID_PATTERN.test(manifest2.backupId) || unvalidated.snapshotKind !== undefined && unvalidated.snapshotKind !== "full" && unvalidated.snapshotKind !== "safety" || !Number.isFinite(Date.parse(manifest2.createdAt)) || !isVersion(manifest2.appVersion) || !manifest2.image || !/^sha256:[0-9a-f]{64}$/i.test(manifest2.imageDigest) || !/^[0-9a-f]{64}$/i.test(manifest2.dataSha256) || !/^[0-9a-f]{64}$/i.test(manifest2.configSha256 ?? "") || manifest2.vaultSha256 !== undefined && !/^[0-9a-f]{64}$/i.test(manifest2.vaultSha256) || manifest2.dataBytes !== undefined && (!Number.isSafeInteger(manifest2.dataBytes) || manifest2.dataBytes < 0)) {
|
|
2858
3094
|
throw new Error(`Invalid backup manifest at ${backupPath}.`);
|
|
2859
3095
|
}
|
|
2860
|
-
const actual = await sha256File(
|
|
3096
|
+
const actual = await sha256File(join14(backupPath, "data.tgz"));
|
|
2861
3097
|
if (actual !== manifest2.dataSha256)
|
|
2862
3098
|
throw new Error(`Backup checksum mismatch for ${manifest2.backupId}. Expected ${manifest2.dataSha256}, got ${actual}.`);
|
|
2863
|
-
|
|
2864
|
-
const configActual = await sha256File(join12(backupPath, "config.env"));
|
|
2865
|
-
if (configActual !== manifest2.configSha256)
|
|
2866
|
-
throw new Error(`Backup configuration checksum mismatch for ${manifest2.backupId}.`);
|
|
2867
|
-
}
|
|
2868
|
-
const config = parseEnv(await readText(join12(backupPath, "config.env")));
|
|
3099
|
+
const config = parseEnv(await readAuthenticatedBackupConfig(backupPath, manifest2));
|
|
2869
3100
|
const storage = (config.NUXT_PUBLIC_STORAGE_PROVIDER ?? "").trim().toLowerCase();
|
|
2870
3101
|
if (manifest2.schemaVersion === 1 && storage === "s3") {
|
|
2871
3102
|
throw new Error(`Invalid backup manifest for ${manifest2.backupId}: schema-1 S3 snapshots do not contain recoverable remote objects.`);
|
|
@@ -2879,13 +3110,13 @@ async function readManifest(backupPath, authenticatedForDirectory) {
|
|
|
2879
3110
|
if (!remote.source || remote.source.endpointOrigin !== source.endpointOrigin || remote.source.bucket !== source.bucket || remote.source.keyPrefix !== source.keyPrefix) {
|
|
2880
3111
|
throw new Error(`Invalid backup manifest for ${manifest2.backupId}: S3 source differs from the authenticated configuration.`);
|
|
2881
3112
|
}
|
|
2882
|
-
const file =
|
|
3113
|
+
const file = join14(backupPath, remote.file);
|
|
2883
3114
|
const info = await lstat3(file);
|
|
2884
3115
|
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size !== remote.bytes || await sha256File(file) !== remote.sha256) {
|
|
2885
3116
|
throw new Error(`Backup remote checksum mismatch or size mismatch for ${manifest2.backupId}.`);
|
|
2886
3117
|
}
|
|
2887
3118
|
}
|
|
2888
|
-
if (manifest2.vaultSha256 && await sha256File(
|
|
3119
|
+
if (manifest2.vaultSha256 && await sha256File(join14(backupPath, "vault.backup")) !== manifest2.vaultSha256)
|
|
2889
3120
|
throw new Error(`Vault backup checksum mismatch for ${manifest2.backupId}.`);
|
|
2890
3121
|
await verifiedManagedAssetContents(backupPath, manifest2);
|
|
2891
3122
|
return manifest2;
|
|
@@ -2895,7 +3126,7 @@ async function resolveBackup(directory, value) {
|
|
|
2895
3126
|
throw new Error(`Backup ID "${value}" is invalid. Use an OR3-generated backup ID or an absolute external backup path.`);
|
|
2896
3127
|
}
|
|
2897
3128
|
const candidate = isAbsolute3(value) ? value : backupDirectory(directory, value);
|
|
2898
|
-
if (!await fileExists(
|
|
3129
|
+
if (!await fileExists(join14(candidate, "manifest.json")))
|
|
2899
3130
|
throw new Error(`Backup ${value} was not found in ${deploymentPaths(directory).backups}.`);
|
|
2900
3131
|
return resolve4(candidate);
|
|
2901
3132
|
}
|
|
@@ -2904,7 +3135,7 @@ async function recordedBackupPath(directory, pending) {
|
|
|
2904
3135
|
const path = previous ? pending.previousBackupPath : pending.backupPath;
|
|
2905
3136
|
const backupId = previous ? pending.previousBackupId : pending.backupId;
|
|
2906
3137
|
const resolved = path ? resolve4(path) : backupId ? await resolveBackup(directory, backupId) : undefined;
|
|
2907
|
-
if (!resolved || !await fileExists(
|
|
3138
|
+
if (!resolved || !await fileExists(join14(resolved, "manifest.json"))) {
|
|
2908
3139
|
throw new Error(`The incomplete ${pending.operation} has no readable ${previous ? "pre-mutation" : "target"} backup source.`);
|
|
2909
3140
|
}
|
|
2910
3141
|
const manifest2 = await readManifest(resolved, directory);
|
|
@@ -3046,87 +3277,7 @@ async function maskedQuestion(question) {
|
|
|
3046
3277
|
|
|
3047
3278
|
// src/deployment/adoption-source.ts
|
|
3048
3279
|
import { existsSync as existsSync2 } from "node:fs";
|
|
3049
|
-
import { basename as basename2, join as
|
|
3050
|
-
|
|
3051
|
-
// src/runtime/health.ts
|
|
3052
|
-
var DEEP_HEALTH_TIMEOUT_MS = 180000;
|
|
3053
|
-
var HEALTH_SCRIPT = "const fs=require('node:fs');try{const auth=process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth',storage=process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs',sync=process.env.OR3_SYNC_PROVIDER||'sqlite';if(sync!=='sqlite'||!(auth==='basic-auth'&&storage==='fs'||auth==='clerk'&&storage==='s3')||process.env.AUTH_PROVIDER&&process.env.OR3_AUTH_PROVIDER&&process.env.AUTH_PROVIDER!==process.env.OR3_AUTH_PROVIDER)throw Error('profile');fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);const paths=auth==='basic-auth'?[process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH]:[process.env.OR3_SQLITE_DB_PATH];if(paths.some(p=>!p))throw Error('database path');for(const path of paths){const fd=fs.openSync(path,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async response=>{const body=await response.json().catch(()=>({}));if(!response.ok||body.status!=='ok'||body.providers?.auth?.provider!==(process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth')||body.providers?.sync?.provider!=='sqlite'||body.providers?.storage?.provider!==(process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs'))process.exit(1)}).catch(()=>process.exit(1))";
|
|
3054
|
-
var VERIFY_DATABASES_SCRIPT = `
|
|
3055
|
-
const fs = require('node:fs');
|
|
3056
|
-
const Database = require('/app/.output/server/node_modules/better-sqlite3');
|
|
3057
|
-
const results = [];
|
|
3058
|
-
const auth = process.env.AUTH_PROVIDER || process.env.OR3_AUTH_PROVIDER || 'basic-auth';
|
|
3059
|
-
const storage = process.env.NUXT_PUBLIC_STORAGE_PROVIDER || 'fs', sync = process.env.OR3_SYNC_PROVIDER || 'sqlite';
|
|
3060
|
-
if (sync !== 'sqlite' || !(auth === 'basic-auth' && storage === 'fs' || auth === 'clerk' && storage === 's3') || process.env.AUTH_PROVIDER && process.env.OR3_AUTH_PROVIDER && process.env.AUTH_PROVIDER !== process.env.OR3_AUTH_PROVIDER) throw new Error('Unsupported auth profile');
|
|
3061
|
-
const paths = auth === 'basic-auth' ? [process.env.OR3_BASIC_AUTH_DB_PATH, process.env.OR3_SQLITE_DB_PATH] : [process.env.OR3_SQLITE_DB_PATH];
|
|
3062
|
-
if (paths.some(path => !path)) throw new Error('Missing managed database path');
|
|
3063
|
-
for (const path of paths) {
|
|
3064
|
-
const info = fs.statSync(path);
|
|
3065
|
-
if (info.uid !== 65532 || info.gid !== 65532) throw new Error(path + ' must be owned by 65532:65532');
|
|
3066
|
-
const db = new Database(path, { readonly: true, fileMustExist: true });
|
|
3067
|
-
const quickCheck = db.pragma('quick_check', { simple: true });
|
|
3068
|
-
const tables = db.prepare("select count(*) as count from sqlite_master where type = 'table'").get().count;
|
|
3069
|
-
db.close();
|
|
3070
|
-
if (quickCheck !== 'ok') throw new Error(path + ' quick_check failed: ' + quickCheck);
|
|
3071
|
-
results.push({ path, quickCheck, tables });
|
|
3072
|
-
}
|
|
3073
|
-
if (process.env.OR3_FORCE_HTTPS === 'true' && process.env.NUXT_SECURITY_PROXY_TRUST_PROXY !== 'true') {
|
|
3074
|
-
throw new Error('NUXT_SECURITY_PROXY_TRUST_PROXY must be true for a managed public deployment');
|
|
3075
|
-
}
|
|
3076
|
-
console.log(JSON.stringify(results));
|
|
3077
|
-
`;
|
|
3078
|
-
var CONTAINER_NODE = "/nodejs/bin/node";
|
|
3079
|
-
var LEGACY_CONTAINER_NODE = "/usr/local/bin/node";
|
|
3080
|
-
var CONTAINER_NODE_SHELL = `if [ -x ${CONTAINER_NODE} ]; then exec ${CONTAINER_NODE} -e "$1"; elif [ -x ${LEGACY_CONTAINER_NODE} ]; then exec ${LEGACY_CONTAINER_NODE} -e "$1"; else exit 127; fi`;
|
|
3081
|
-
function containerNodeCommand(script) {
|
|
3082
|
-
return ["sh", "-c", CONTAINER_NODE_SHELL, "or3-node", script];
|
|
3083
|
-
}
|
|
3084
|
-
async function waitForDeepHealthWithArgs(composeCommand, directory, secrets = []) {
|
|
3085
|
-
const startedAt = Date.now();
|
|
3086
|
-
const deadline = startedAt + DEEP_HEALTH_TIMEOUT_MS;
|
|
3087
|
-
let lastError = "health check did not complete";
|
|
3088
|
-
let lastProgressAt = startedAt;
|
|
3089
|
-
while (Date.now() < deadline) {
|
|
3090
|
-
const result = await run("docker", [
|
|
3091
|
-
...composeCommand,
|
|
3092
|
-
"exec",
|
|
3093
|
-
"-T",
|
|
3094
|
-
"or3",
|
|
3095
|
-
...containerNodeCommand(HEALTH_SCRIPT)
|
|
3096
|
-
], directory);
|
|
3097
|
-
if (result.ok)
|
|
3098
|
-
return;
|
|
3099
|
-
lastError = redact(result.stderr, secrets);
|
|
3100
|
-
if (Date.now() - lastProgressAt >= 15000) {
|
|
3101
|
-
console.error(`Waiting for OR3 deep health (${Math.round((Date.now() - startedAt) / 1000)}s elapsed)…`);
|
|
3102
|
-
lastProgressAt = Date.now();
|
|
3103
|
-
}
|
|
3104
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
|
|
3105
|
-
}
|
|
3106
|
-
throw new Error(`OR3 deep health did not pass within ${DEEP_HEALTH_TIMEOUT_MS / 1000} seconds. Last error: ${lastError}`);
|
|
3107
|
-
}
|
|
3108
|
-
async function waitForDeepHealth(directory, mode, secrets = []) {
|
|
3109
|
-
try {
|
|
3110
|
-
await waitForDeepHealthWithArgs(composeArgs(directory, mode), directory, secrets);
|
|
3111
|
-
} catch (error) {
|
|
3112
|
-
const captured = await captureComposeDiagnostics(directory, mode, secrets).catch((captureError) => redact(`Diagnostics capture failed: ${captureError instanceof Error ? captureError.message : String(captureError)}`, secrets));
|
|
3113
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)}
|
|
3114
|
-
Captured Docker diagnostics:
|
|
3115
|
-
${captured}
|
|
3116
|
-
Diagnostics: ${diagnostics(directory, mode)}`);
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
async function probeDeepHealth(directory, mode) {
|
|
3120
|
-
const result = await run("docker", [...composeArgs(directory, mode, ["exec", "-T", "or3", ...containerNodeCommand(HEALTH_SCRIPT)])], directory);
|
|
3121
|
-
if (result.ok)
|
|
3122
|
-
return "ok";
|
|
3123
|
-
const ps = await run("docker", [...composeArgs(directory, mode, ["ps", "-q", "or3"])], directory);
|
|
3124
|
-
if (ps.ok && ps.stdout.trim())
|
|
3125
|
-
return "degraded";
|
|
3126
|
-
return "unreachable";
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
// src/deployment/adoption-source.ts
|
|
3280
|
+
import { basename as basename2, join as join15 } from "node:path";
|
|
3130
3281
|
async function sourceServiceRunning(directory, composeFiles) {
|
|
3131
3282
|
const result = await run("docker", [...sourceComposeArgs(directory, composeFiles), "ps", "--status", "running", "-q", "or3"], directory);
|
|
3132
3283
|
if (!result.ok)
|
|
@@ -3236,9 +3387,9 @@ async function readSourceVolume(sourceDirectory) {
|
|
|
3236
3387
|
"--project-directory",
|
|
3237
3388
|
sourceDirectory,
|
|
3238
3389
|
"--env-file",
|
|
3239
|
-
|
|
3390
|
+
join15(sourceDirectory, ".env"),
|
|
3240
3391
|
"-f",
|
|
3241
|
-
|
|
3392
|
+
join15(sourceDirectory, "compose.yaml"),
|
|
3242
3393
|
"ps",
|
|
3243
3394
|
"-aq",
|
|
3244
3395
|
"or3"
|
|
@@ -3259,7 +3410,7 @@ async function readSourceVolume(sourceDirectory) {
|
|
|
3259
3410
|
}
|
|
3260
3411
|
function sourceMode(sourceDirectory, sourceEnv) {
|
|
3261
3412
|
const publicDeployment = sourceEnv.OR3_PUBLIC_DOMAIN && sourceEnv.OR3_PUBLIC_DOMAIN !== "localhost" && sourceEnv.OR3_FORCE_HTTPS !== "false";
|
|
3262
|
-
return publicDeployment && existsSync2(
|
|
3413
|
+
return publicDeployment && existsSync2(join15(sourceDirectory, "compose.public.yaml")) ? "public" : "local";
|
|
3263
3414
|
}
|
|
3264
3415
|
function sourceComposeArgs(sourceDirectory, sourceComposeFiles) {
|
|
3265
3416
|
return [
|
|
@@ -3267,7 +3418,7 @@ function sourceComposeArgs(sourceDirectory, sourceComposeFiles) {
|
|
|
3267
3418
|
"--project-directory",
|
|
3268
3419
|
sourceDirectory,
|
|
3269
3420
|
"--env-file",
|
|
3270
|
-
|
|
3421
|
+
join15(sourceDirectory, ".env"),
|
|
3271
3422
|
...sourceComposeFiles
|
|
3272
3423
|
];
|
|
3273
3424
|
}
|
|
@@ -3414,7 +3565,7 @@ function assertSupportedSourceCompose(configText, sourceVolume, expectedPort, so
|
|
|
3414
3565
|
// src/runtime/dashboard-operator.ts
|
|
3415
3566
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
3416
3567
|
import { chmod as chmod4, mkdir as mkdir4, rm as rm5, stat as stat3 } from "node:fs/promises";
|
|
3417
|
-
import { isAbsolute as isAbsolute4, join as
|
|
3568
|
+
import { isAbsolute as isAbsolute4, join as join16, resolve as resolve6 } from "node:path";
|
|
3418
3569
|
|
|
3419
3570
|
// src/runtime/images.ts
|
|
3420
3571
|
async function pullImage(image, expectedDigest2) {
|
|
@@ -3679,7 +3830,7 @@ function dashboardOperatorHandoffArgs(directory, env, jobId) {
|
|
|
3679
3830
|
"--entrypoint",
|
|
3680
3831
|
"/usr/local/bin/node",
|
|
3681
3832
|
env.OR3_OPERATOR_IMAGE,
|
|
3682
|
-
|
|
3833
|
+
join16(directory, "dashboard-operator.mjs"),
|
|
3683
3834
|
"--complete-handoff",
|
|
3684
3835
|
jobId,
|
|
3685
3836
|
project
|
|
@@ -3763,34 +3914,8 @@ async function checkPublicPrerequisites(domain) {
|
|
|
3763
3914
|
return address;
|
|
3764
3915
|
}
|
|
3765
3916
|
|
|
3766
|
-
// src/runtime/project.ts
|
|
3767
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3768
|
-
import { join as join15 } from "node:path";
|
|
3769
|
-
function managedVaultEnabled(directory, env) {
|
|
3770
|
-
const selected = env ?? parseEnv(readFileSync5(join15(directory, ".env"), "utf8"));
|
|
3771
|
-
return selected.SSR_AUTH_ENABLED === "true" && (selected.NUXT_SECRETS_PROVIDER ?? "or3-secrets") === "or3-secrets" && readFileSync5(join15(directory, "compose.yaml"), "utf8").includes(`
|
|
3772
|
-
or3-vault:
|
|
3773
|
-
`);
|
|
3774
|
-
}
|
|
3775
|
-
async function stopProject(directory, mode) {
|
|
3776
|
-
await compose(directory, mode, ["stop", "or3", ...managedVaultEnabled(directory) ? ["or3-vault"] : []]);
|
|
3777
|
-
}
|
|
3778
|
-
async function projectServiceRunning(directory, mode) {
|
|
3779
|
-
const result = await run("docker", composeArgs(directory, mode, ["ps", "--status", "running", "-q", "or3"]), directory);
|
|
3780
|
-
if (!result.ok)
|
|
3781
|
-
throw new Error(`Could not determine whether OR3 is running. ${result.stderr.trim()}`);
|
|
3782
|
-
return Boolean(result.stdout.trim());
|
|
3783
|
-
}
|
|
3784
|
-
async function startProject(directory, mode, env) {
|
|
3785
|
-
await assertSafeComposeBinding(directory, mode, env);
|
|
3786
|
-
const services = process.env.OR3_DASHBOARD_UPDATE_JOB_ID ? ["or3"] : [];
|
|
3787
|
-
await compose(directory, mode, ["up", "-d", "--wait", "--wait-timeout", "180", ...services], secretValues(env));
|
|
3788
|
-
await waitForDeepHealth(directory, mode, secretValues(env));
|
|
3789
|
-
await assertRunningAppImage(directory, mode, env.OR3_IMAGE);
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
3917
|
// src/runtime/verification.ts
|
|
3793
|
-
import { createHash as
|
|
3918
|
+
import { createHash as createHash5, randomBytes as randomBytes7, randomUUID as randomUUID4 } from "node:crypto";
|
|
3794
3919
|
function validateVerificationHealth(value, profile = "basic-sqlite-fs") {
|
|
3795
3920
|
const health = value;
|
|
3796
3921
|
if (!health || health.status !== "ok" || health.providers?.auth?.provider !== (profile === "basic-sqlite-fs" ? "basic-auth" : "clerk") || health.providers.sync?.provider !== "sqlite" || health.providers.storage?.provider !== (profile === "basic-sqlite-fs" ? "fs" : "s3")) {
|
|
@@ -3889,7 +4014,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
3889
4014
|
Buffer.from("89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489", "hex"),
|
|
3890
4015
|
randomBytes7(8)
|
|
3891
4016
|
]);
|
|
3892
|
-
const hash = `sha256:${
|
|
4017
|
+
const hash = `sha256:${createHash5("sha256").update(probeBytes).digest("hex")}`;
|
|
3893
4018
|
let storageId;
|
|
3894
4019
|
let metadataAttempted = false;
|
|
3895
4020
|
let physicalCleanupDeferred = false;
|
|
@@ -4081,31 +4206,31 @@ async function adoptCommand(positionals, flags) {
|
|
|
4081
4206
|
if (targetDirectory === sourceDirectory)
|
|
4082
4207
|
throw new Error("Choose a separate target directory for adoption.");
|
|
4083
4208
|
await readDirectoryEmpty(targetDirectory);
|
|
4084
|
-
if (!await fileExists(
|
|
4209
|
+
if (!await fileExists(join17(sourceDirectory, "or3-release.json")))
|
|
4085
4210
|
throw new Error(`V1 release metadata is missing at ${sourceDirectory}/or3-release.json.`);
|
|
4086
|
-
const release = JSON.parse(await readText(
|
|
4211
|
+
const release = JSON.parse(await readText(join17(sourceDirectory, "or3-release.json")));
|
|
4087
4212
|
if (!release.or3Version || !isVersion(release.or3Version))
|
|
4088
4213
|
throw new Error("V1 release metadata does not contain a usable OR3 version.");
|
|
4089
4214
|
if (release.or3Version !== PACKAGE_VERSION) {
|
|
4090
4215
|
throw new Error(`V1 is on OR3 ${release.or3Version}, but this CLI ships managed assets for ${PACKAGE_VERSION}. Run the matching @or3/cloud version or perform a qualified host CLI update before adoption.`);
|
|
4091
4216
|
}
|
|
4092
|
-
const sourceEnv = parseEnv(await readText(
|
|
4093
|
-
const providers = await readText(
|
|
4217
|
+
const sourceEnv = parseEnv(await readText(join17(sourceDirectory, ".env")));
|
|
4218
|
+
const providers = await readText(join17(sourceDirectory, "or3.providers.generated.ts"));
|
|
4094
4219
|
assertSupportedSource(sourceDirectory, sourceEnv, providers);
|
|
4095
4220
|
const mode = sourceMode(sourceDirectory, sourceEnv);
|
|
4096
4221
|
const domain = mode === "public" ? sourceEnv.OR3_PUBLIC_DOMAIN : undefined;
|
|
4097
4222
|
if (domain)
|
|
4098
4223
|
await checkPublicPrerequisites(domain);
|
|
4099
|
-
const sourceComposeFiles = ["-f",
|
|
4224
|
+
const sourceComposeFiles = ["-f", join17(sourceDirectory, "compose.yaml")];
|
|
4100
4225
|
if (mode === "public")
|
|
4101
|
-
sourceComposeFiles.push("-f",
|
|
4226
|
+
sourceComposeFiles.push("-f", join17(sourceDirectory, "compose.public.yaml"));
|
|
4102
4227
|
await ensureDocker();
|
|
4103
4228
|
const sourceConfig = await run("docker", [
|
|
4104
4229
|
"compose",
|
|
4105
4230
|
"--project-directory",
|
|
4106
4231
|
sourceDirectory,
|
|
4107
4232
|
"--env-file",
|
|
4108
|
-
|
|
4233
|
+
join17(sourceDirectory, ".env"),
|
|
4109
4234
|
...sourceComposeFiles,
|
|
4110
4235
|
"config",
|
|
4111
4236
|
"--format",
|
|
@@ -4193,7 +4318,7 @@ ${existing.stderr}`)) {
|
|
|
4193
4318
|
OR3_GUEST_ACCESS_ENABLED: "false"
|
|
4194
4319
|
});
|
|
4195
4320
|
await writeSecure(deploymentPaths(targetDirectory).env, serializeEnv(targetEnv));
|
|
4196
|
-
await writeSecure(
|
|
4321
|
+
await writeSecure(join17(targetDirectory, ".or3-initial-credentials"), serializeInitialCredentials({
|
|
4197
4322
|
bootstrapEmail: email,
|
|
4198
4323
|
bootstrapPassword: password,
|
|
4199
4324
|
adminUsername: targetEnv.OR3_ADMIN_USERNAME,
|
|
@@ -4230,7 +4355,7 @@ ${existing.stderr}`)) {
|
|
|
4230
4355
|
}
|
|
4231
4356
|
}
|
|
4232
4357
|
await mkdir5(sourceBackupDir, { recursive: true, mode: 448 });
|
|
4233
|
-
await copySecure(
|
|
4358
|
+
await copySecure(join17(sourceDirectory, ".env"), join17(sourceBackupDir, "config.env"));
|
|
4234
4359
|
await archiveExternalVolume(sourceImage, sourceVolume, sourceBackupDir);
|
|
4235
4360
|
const sourceManifest = {
|
|
4236
4361
|
schemaVersion: 1,
|
|
@@ -4239,8 +4364,8 @@ ${existing.stderr}`)) {
|
|
|
4239
4364
|
appVersion: release.or3Version,
|
|
4240
4365
|
image,
|
|
4241
4366
|
imageDigest: digest,
|
|
4242
|
-
dataSha256: await sha256File(
|
|
4243
|
-
configSha256: await sha256File(
|
|
4367
|
+
dataSha256: await sha256File(join17(sourceBackupDir, "data.tgz")),
|
|
4368
|
+
configSha256: await sha256File(join17(sourceBackupDir, "config.env")),
|
|
4244
4369
|
mode,
|
|
4245
4370
|
domain,
|
|
4246
4371
|
composeProject: sourceEnv.OR3_COMPOSE_PROJECT ?? sanitizeName(basename3(sourceDirectory)),
|
|
@@ -4251,7 +4376,7 @@ ${existing.stderr}`)) {
|
|
|
4251
4376
|
};
|
|
4252
4377
|
const sourceManifestContents = `${JSON.stringify(sourceManifest, null, 2)}
|
|
4253
4378
|
`;
|
|
4254
|
-
await writeSecure(
|
|
4379
|
+
await writeSecure(join17(sourceBackupDir, "manifest.json"), sourceManifestContents);
|
|
4255
4380
|
await writeBackupAuthentication(targetDirectory, sourceBackupDir, sourceManifestContents);
|
|
4256
4381
|
await readManifest(sourceBackupDir, targetDirectory);
|
|
4257
4382
|
await updatePending(targetDirectory, state, { phase: "snapshot-created" });
|
|
@@ -4291,10 +4416,10 @@ ${recoveryDetail}Managed files remain at ${targetDirectory}.${sourceBackupDir ?
|
|
|
4291
4416
|
|
|
4292
4417
|
// src/backup/create.ts
|
|
4293
4418
|
import { chmod as chmod6, lstat as lstat4, mkdir as mkdir6 } from "node:fs/promises";
|
|
4294
|
-
import { join as
|
|
4419
|
+
import { join as join20, resolve as resolve10 } from "node:path";
|
|
4295
4420
|
|
|
4296
4421
|
// src/backup/references.ts
|
|
4297
|
-
import { join as
|
|
4422
|
+
import { join as join18, resolve as resolve8 } from "node:path";
|
|
4298
4423
|
var INSPECTOR3 = "/app/scripts/docker/backup-inspect.mjs";
|
|
4299
4424
|
var NODE3 = 'if [ -x /nodejs/bin/node ]; then exec /nodejs/bin/node "$@"; else exec node "$@"; fi';
|
|
4300
4425
|
function assertBackupOperatorMount(directory, enabled, composeMounts, actualMounts) {
|
|
@@ -4564,7 +4689,7 @@ async function assertBackupSourceProfile(directory, state, env) {
|
|
|
4564
4689
|
async function validateCanonicalBackupReferences(directory, env, backupPath) {
|
|
4565
4690
|
assertSupportedBackupProfile(env);
|
|
4566
4691
|
await withBackupHelper(directory, env.OR3_IMAGE, env, { network: "none" }, async (helper) => {
|
|
4567
|
-
await helper.loadSqlite(
|
|
4692
|
+
await helper.loadSqlite(join18(backupPath, "data.tgz"));
|
|
4568
4693
|
const result = JSON.parse(await helper.exec(["sh", "-c", NODE3, "or3-backup-inspection", INSPECTOR3, "references", env.OR3_STORAGE_FS_ROOT ?? "/data/storage"]));
|
|
4569
4694
|
if (result.contract !== 1 || result.complete !== true || !Number.isSafeInteger(result.liveAttachments) || result.liveAttachments < 0)
|
|
4570
4695
|
throw new Error("Offline canonical backup reference inspection did not complete.");
|
|
@@ -4573,7 +4698,7 @@ async function validateCanonicalBackupReferences(directory, env, backupPath) {
|
|
|
4573
4698
|
|
|
4574
4699
|
// src/backup/retention.ts
|
|
4575
4700
|
import { rm as rm6 } from "node:fs/promises";
|
|
4576
|
-
import { basename as basename4, dirname as dirname4, join as
|
|
4701
|
+
import { basename as basename4, dirname as dirname4, join as join19, resolve as resolve9 } from "node:path";
|
|
4577
4702
|
var BACKUP_RETENTION_KEEP = 5;
|
|
4578
4703
|
function assertRemovableArtifactName(name) {
|
|
4579
4704
|
if (!BACKUP_ID_PATTERN.test(name)) {
|
|
@@ -4589,7 +4714,7 @@ async function removeNamedBackupArtifact(directory, backupId) {
|
|
|
4589
4714
|
throw new Error(`Refusing to remove a path outside the backups directory: ${target}.`);
|
|
4590
4715
|
}
|
|
4591
4716
|
await rm6(target, { recursive: true, force: true });
|
|
4592
|
-
await rm6(
|
|
4717
|
+
await rm6(join19(deploymentPaths(directory).exports, `${backupId}.json`), { force: true });
|
|
4593
4718
|
}
|
|
4594
4719
|
async function removeEnumeratedBackupArtifact(directory, backup) {
|
|
4595
4720
|
await lifecycleFaults.beforeArtifactDelete?.();
|
|
@@ -4599,7 +4724,7 @@ async function removeEnumeratedBackupArtifact(directory, backup) {
|
|
|
4599
4724
|
throw new Error(`Refusing to prune an untrusted backup path ${backup.path}.`);
|
|
4600
4725
|
}
|
|
4601
4726
|
await rm6(target, { recursive: true, force: true });
|
|
4602
|
-
await rm6(
|
|
4727
|
+
await rm6(join19(deploymentPaths(directory).exports, `${backup.backupId}.json`), { force: true });
|
|
4603
4728
|
}
|
|
4604
4729
|
function planRetention(entries, keep, protectedIds, options = {}) {
|
|
4605
4730
|
if (!Number.isInteger(keep) || keep < 1)
|
|
@@ -4710,7 +4835,7 @@ async function cleanupPartialBackup(directory, backupId) {
|
|
|
4710
4835
|
}
|
|
4711
4836
|
const statuses = await Promise.all([
|
|
4712
4837
|
artifactStatus(backupDirectory(directory, backupId)),
|
|
4713
|
-
artifactStatus(
|
|
4838
|
+
artifactStatus(join20(deploymentPaths(directory).exports, `${backupId}.json`))
|
|
4714
4839
|
]);
|
|
4715
4840
|
const removed = statuses.every((status) => status.absent);
|
|
4716
4841
|
const detail = statuses.map((status) => status.detail).join(" ");
|
|
@@ -4780,9 +4905,9 @@ async function createBackup(directory, state, env, options = {}) {
|
|
|
4780
4905
|
"or3-vault",
|
|
4781
4906
|
"/app/vault/entrypoint.mjs",
|
|
4782
4907
|
"backup"
|
|
4783
|
-
]),
|
|
4908
|
+
]), join20(backupDir, "vault.backup"), directory, secretValues(env));
|
|
4784
4909
|
}
|
|
4785
|
-
await copySecure(deploymentPaths(directory).env,
|
|
4910
|
+
await copySecure(deploymentPaths(directory).env, join20(backupDir, "config.env"));
|
|
4786
4911
|
managedAssetSha256 = await snapshotManagedAssets(directory, state.mode, backupDir);
|
|
4787
4912
|
artifact = "captured";
|
|
4788
4913
|
} catch (error) {
|
|
@@ -4827,10 +4952,10 @@ async function createBackup(directory, state, env, options = {}) {
|
|
|
4827
4952
|
appVersion: state.appVersion,
|
|
4828
4953
|
image: state.image,
|
|
4829
4954
|
imageDigest: state.imageDigest,
|
|
4830
|
-
dataSha256: await sha256File(
|
|
4831
|
-
...managedVaultEnabled(directory, env) ? { vaultSha256: await sha256File(
|
|
4955
|
+
dataSha256: await sha256File(join20(backupDir, "data.tgz")),
|
|
4956
|
+
...managedVaultEnabled(directory, env) ? { vaultSha256: await sha256File(join20(backupDir, "vault.backup")) } : {},
|
|
4832
4957
|
dataBytes: volumeSize,
|
|
4833
|
-
configSha256: await sha256File(
|
|
4958
|
+
configSha256: await sha256File(join20(backupDir, "config.env")),
|
|
4834
4959
|
managedAssetSha256,
|
|
4835
4960
|
managedAssetInventoryVersion: managedAssetSha256?.["dashboard-lifecycle.mjs"] ? MANAGED_ASSET_INVENTORY_VERSION : managedAssetSha256?.["compose.operator.yaml"] ? 3 : managedAssetSha256?.["dashboard-operator.mjs"] ? 2 : undefined,
|
|
4836
4961
|
mode: state.mode,
|
|
@@ -4844,7 +4969,7 @@ async function createBackup(directory, state, env, options = {}) {
|
|
|
4844
4969
|
};
|
|
4845
4970
|
const manifestContents = `${JSON.stringify(manifest2, null, 2)}
|
|
4846
4971
|
`;
|
|
4847
|
-
await writeSecure(
|
|
4972
|
+
await writeSecure(join20(backupDir, "manifest.json"), manifestContents);
|
|
4848
4973
|
await writeBackupAuthentication(directory, backupDir, manifestContents);
|
|
4849
4974
|
verifiedManifest = await readManifest(backupDir, directory);
|
|
4850
4975
|
artifact = "verified";
|
|
@@ -4899,7 +5024,7 @@ async function inspectUnclaimedBackupArtifacts(directory, backupId, backupPath)
|
|
|
4899
5024
|
return;
|
|
4900
5025
|
const statuses = await Promise.all([
|
|
4901
5026
|
artifactStatus(expectedPath),
|
|
4902
|
-
artifactStatus(
|
|
5027
|
+
artifactStatus(join20(deploymentPaths(directory).exports, `${backupId}.json`))
|
|
4903
5028
|
]);
|
|
4904
5029
|
const detail = statuses.map((status) => status.detail).join(" ");
|
|
4905
5030
|
return statuses.every((status) => status.absent) ? detail : `Maintenance warning: backup artifact ownership was not recorded. ${detail} This recovery did not verify or remove these paths. Inspect them with "npx @or3/cloud backup list".`;
|
|
@@ -4926,7 +5051,7 @@ async function createPreMutationSnapshot(directory, state, env, backupId) {
|
|
|
4926
5051
|
|
|
4927
5052
|
// src/backup/export.ts
|
|
4928
5053
|
import { chmod as chmod7, mkdir as mkdir7, realpath as realpath2, rm as rm7, stat as stat4 } from "node:fs/promises";
|
|
4929
|
-
import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute5, join as
|
|
5054
|
+
import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute5, join as join21, resolve as resolve11, sep } from "node:path";
|
|
4930
5055
|
var PURGE_REQUIRES_BACKUP_WITHIN_MS = 24 * 60 * 60 * 1000;
|
|
4931
5056
|
async function exportBackup(directory, backupId, destination) {
|
|
4932
5057
|
const backupPath = await resolveBackup(directory, backupId);
|
|
@@ -4942,7 +5067,7 @@ async function exportBackup(directory, backupId, destination) {
|
|
|
4942
5067
|
if (await fileExists(dest))
|
|
4943
5068
|
throw new Error(`Destination ${dest} already exists. Choose a new empty destination so an export can never merge with unrelated files.`);
|
|
4944
5069
|
await mkdir7(dirname5(dest), { recursive: true, mode: 448 });
|
|
4945
|
-
const canonicalDestination =
|
|
5070
|
+
const canonicalDestination = join21(await realpath2(dirname5(dest)), basename5(dest));
|
|
4946
5071
|
if (canonicalDestination === deploymentRoot || canonicalDestination.startsWith(`${deploymentRoot}${sep}`)) {
|
|
4947
5072
|
throw new Error("Backup exports must live outside the managed deployment directory so `remove --purge-data` can never delete the remaining copy.");
|
|
4948
5073
|
}
|
|
@@ -4953,19 +5078,19 @@ async function exportBackup(directory, backupId, destination) {
|
|
|
4953
5078
|
}
|
|
4954
5079
|
await chmod7(dest, 448);
|
|
4955
5080
|
for (const file of ["data.tgz", "config.env", "manifest.json", "manifest.auth", ...manifest2.vaultSha256 ? ["vault.backup"] : [], ...manifest2.schemaVersion === 2 ? [manifest2.remote.file] : []]) {
|
|
4956
|
-
await copySecure(
|
|
5081
|
+
await copySecure(join21(backupPath, file), join21(dest, file));
|
|
4957
5082
|
}
|
|
4958
5083
|
if (manifest2.managedAssetSha256) {
|
|
4959
5084
|
for (const name of managedAssetNamesForInventory(manifest2.mode, manifest2.managedAssetInventoryVersion)) {
|
|
4960
|
-
await copySecure(
|
|
5085
|
+
await copySecure(join21(backupPath, "managed-assets", name), join21(dest, "managed-assets", name));
|
|
4961
5086
|
}
|
|
4962
5087
|
}
|
|
4963
|
-
const dataSha = await sha256File(
|
|
5088
|
+
const dataSha = await sha256File(join21(dest, "data.tgz"));
|
|
4964
5089
|
if (dataSha !== manifest2.dataSha256) {
|
|
4965
5090
|
throw new Error(`Exported data.tgz checksum mismatch for ${manifest2.backupId}. Expected ${manifest2.dataSha256}, got ${dataSha}.`);
|
|
4966
5091
|
}
|
|
4967
5092
|
if (manifest2.configSha256) {
|
|
4968
|
-
const configSha = await sha256File(
|
|
5093
|
+
const configSha = await sha256File(join21(dest, "config.env"));
|
|
4969
5094
|
if (configSha !== manifest2.configSha256) {
|
|
4970
5095
|
throw new Error(`Exported config.env checksum mismatch for ${manifest2.backupId}.`);
|
|
4971
5096
|
}
|
|
@@ -4982,14 +5107,14 @@ async function exportBackup(directory, backupId, destination) {
|
|
|
4982
5107
|
dataSha256: manifest2.dataSha256,
|
|
4983
5108
|
configSha256: manifest2.configSha256
|
|
4984
5109
|
};
|
|
4985
|
-
await writeSecure(
|
|
5110
|
+
await writeSecure(join21(deploymentPaths(directory).exports, `${manifest2.backupId}.json`), `${JSON.stringify(receipt, null, 2)}
|
|
4986
5111
|
`);
|
|
4987
|
-
let bytes = (manifest2.vaultSha256 ? (await stat4(
|
|
5112
|
+
let bytes = (manifest2.vaultSha256 ? (await stat4(join21(dest, "vault.backup"))).size : 0) + (await stat4(join21(dest, "data.tgz"))).size + (await stat4(join21(dest, "config.env"))).size + (await stat4(join21(dest, "manifest.json"))).size + (await stat4(join21(dest, "manifest.auth"))).size;
|
|
4988
5113
|
if (manifest2.schemaVersion === 2)
|
|
4989
|
-
bytes += (await stat4(
|
|
5114
|
+
bytes += (await stat4(join21(dest, manifest2.remote.file))).size;
|
|
4990
5115
|
if (manifest2.managedAssetSha256) {
|
|
4991
5116
|
for (const name of managedAssetNamesForInventory(manifest2.mode, manifest2.managedAssetInventoryVersion)) {
|
|
4992
|
-
bytes += (await stat4(
|
|
5117
|
+
bytes += (await stat4(join21(dest, "managed-assets", name))).size;
|
|
4993
5118
|
}
|
|
4994
5119
|
}
|
|
4995
5120
|
console.log(`Exported backup ${manifest2.backupId} (${bytes} bytes) to ${dest}`);
|
|
@@ -5191,7 +5316,7 @@ async function backupCommand(directory, positionals, flags) {
|
|
|
5191
5316
|
|
|
5192
5317
|
// src/credentials/reset.ts
|
|
5193
5318
|
import { rm as rm8 } from "node:fs/promises";
|
|
5194
|
-
import { join as
|
|
5319
|
+
import { join as join22 } from "node:path";
|
|
5195
5320
|
|
|
5196
5321
|
// src/credentials/reset-script.ts
|
|
5197
5322
|
function buildCredentialsResetScript(input2) {
|
|
@@ -5387,7 +5512,7 @@ async function applyCredentialReset(directory, state, nextEnv) {
|
|
|
5387
5512
|
await runCredentialsResetScript(directory, state.mode, values);
|
|
5388
5513
|
const runtimeEnv = withoutProvisioningCredentials(nextEnv);
|
|
5389
5514
|
await writeSecure(deploymentPaths(directory).env, serializeEnv(runtimeEnv));
|
|
5390
|
-
const initialCredentials =
|
|
5515
|
+
const initialCredentials = join22(directory, ".or3-initial-credentials");
|
|
5391
5516
|
await rm8(initialCredentials, { force: true });
|
|
5392
5517
|
await stopProject(directory, state.mode);
|
|
5393
5518
|
if (managedVaultEnabled(directory, runtimeEnv))
|
|
@@ -5455,7 +5580,7 @@ async function credentialsCommand(directory, positionals, flags) {
|
|
|
5455
5580
|
// src/commands/doctor.ts
|
|
5456
5581
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5457
5582
|
import { stat as stat5 } from "node:fs/promises";
|
|
5458
|
-
import { basename as basename6, join as
|
|
5583
|
+
import { basename as basename6, join as join23, resolve as resolve12 } from "node:path";
|
|
5459
5584
|
async function doctorCommand(directory) {
|
|
5460
5585
|
const resolved = resolve12(directory);
|
|
5461
5586
|
const paths = deploymentPaths(resolved);
|
|
@@ -5480,7 +5605,7 @@ async function doctorCommand(directory) {
|
|
|
5480
5605
|
failures.push(error instanceof Error ? error.message : String(error));
|
|
5481
5606
|
console.log("✗ Managed state or .env is missing/invalid");
|
|
5482
5607
|
}
|
|
5483
|
-
for (const file of [paths.env, paths.state,
|
|
5608
|
+
for (const file of [paths.env, paths.state, join23(resolved, ".or3-initial-credentials")]) {
|
|
5484
5609
|
if (!existsSync3(file))
|
|
5485
5610
|
continue;
|
|
5486
5611
|
const mode = (await stat5(file)).mode & 511;
|
|
@@ -5552,7 +5677,7 @@ async function doctorCommand(directory) {
|
|
|
5552
5677
|
}
|
|
5553
5678
|
assertDashboardOperatorMounts(container.Mounts ?? [], resolved);
|
|
5554
5679
|
const ipcDirectory = await stat5(paths.operatorIpc);
|
|
5555
|
-
const operatorSocket = await stat5(
|
|
5680
|
+
const operatorSocket = await stat5(join23(paths.operatorIpc, "operator.sock"));
|
|
5556
5681
|
const operatorUid = Number(env.OR3_OPERATOR_UID);
|
|
5557
5682
|
const operatorGid = Number(env.OR3_OPERATOR_GID);
|
|
5558
5683
|
if ((ipcDirectory.mode & 511) !== 456 || ipcDirectory.uid !== operatorUid || ipcDirectory.gid !== operatorGid || !operatorSocket.isSocket() || (operatorSocket.mode & 511) !== 432 || operatorSocket.uid !== operatorUid || operatorSocket.gid !== operatorGid) {
|
|
@@ -5611,7 +5736,7 @@ async function doctorCommand(directory) {
|
|
|
5611
5736
|
|
|
5612
5737
|
// src/commands/init.ts
|
|
5613
5738
|
import { chmod as chmod8, mkdir as mkdir8 } from "node:fs/promises";
|
|
5614
|
-
import { join as
|
|
5739
|
+
import { join as join24, relative, resolve as resolve13 } from "node:path";
|
|
5615
5740
|
async function initCommand(positionals, flags) {
|
|
5616
5741
|
const hasLocal = boolFlag(flags, "local");
|
|
5617
5742
|
const hasPublic = boolFlag(flags, "public");
|
|
@@ -5619,7 +5744,7 @@ async function initCommand(positionals, flags) {
|
|
|
5619
5744
|
throw new Error("Choose exactly one of --local or --public.");
|
|
5620
5745
|
const mode = hasPublic ? "public" : "local";
|
|
5621
5746
|
const directory = resolve13(process.cwd(), positionals[0] ?? "or3-cloud");
|
|
5622
|
-
if (await fileExists(
|
|
5747
|
+
if (await fileExists(join24(directory, ".or3-cloud", "state.json"))) {
|
|
5623
5748
|
throw new Error(`${directory} is already a managed deployment. Use update, doctor, or adopt.`);
|
|
5624
5749
|
}
|
|
5625
5750
|
await readDirectoryEmpty(directory);
|
|
@@ -5670,7 +5795,7 @@ ${existing.stderr}`)) {
|
|
|
5670
5795
|
await copyAssets(directory, mode);
|
|
5671
5796
|
const env = buildEnv({ mode, version, directory, image, email, password, domain, port, dashboardOperator: operator });
|
|
5672
5797
|
await writeSecure(deploymentPaths(directory).env, serializeEnv(env));
|
|
5673
|
-
await writeSecure(
|
|
5798
|
+
await writeSecure(join24(directory, ".or3-initial-credentials"), serializeInitialCredentials({
|
|
5674
5799
|
bootstrapEmail: email,
|
|
5675
5800
|
bootstrapPassword: password,
|
|
5676
5801
|
adminUsername: email,
|
|
@@ -5695,7 +5820,7 @@ ${existing.stderr}`)) {
|
|
|
5695
5820
|
const folder = relative(process.cwd(), directory) || ".";
|
|
5696
5821
|
console.log(`
|
|
5697
5822
|
OR3 Cloud ${version} is running at ${url}`);
|
|
5698
|
-
console.log(`Sign in as ${email} with the password in ${
|
|
5823
|
+
console.log(`Sign in as ${email} with the password in ${join24(folder, ".or3-initial-credentials")}; the same login opens ${url}/admin.`);
|
|
5699
5824
|
console.log("Save it in a password manager, then delete that file.");
|
|
5700
5825
|
console.log(`
|
|
5701
5826
|
Run later commands from the deployment folder: cd ${quote(folder)} && npx @or3/cloud doctor`);
|
|
@@ -5754,7 +5879,7 @@ async function restartCommand(directory, lifecycle) {
|
|
|
5754
5879
|
}
|
|
5755
5880
|
|
|
5756
5881
|
// src/backup/restore.ts
|
|
5757
|
-
import { join as
|
|
5882
|
+
import { join as join25 } from "node:path";
|
|
5758
5883
|
|
|
5759
5884
|
// src/backup/generation-restore.ts
|
|
5760
5885
|
function rebindRestoredGenerations(fs, path, crypto, db, root, options = {}) {
|
|
@@ -5950,14 +6075,14 @@ if (root !== '/data' && !root.startsWith('/data/')) throw new Error('Generation
|
|
|
5950
6075
|
if (!result.ok)
|
|
5951
6076
|
throw new Error(`Restored generation verification failed. ${redact(result.stderr, secretValues(env))}`);
|
|
5952
6077
|
}
|
|
5953
|
-
function assertFilesystemPayloadHash(key, readChunk,
|
|
6078
|
+
function assertFilesystemPayloadHash(key, readChunk, createHash6) {
|
|
5954
6079
|
const match = /^(sha256|md5)_([a-fA-F0-9]+)$/.exec(key);
|
|
5955
6080
|
const algorithm = match?.[1];
|
|
5956
6081
|
const expected = match?.[2];
|
|
5957
6082
|
if (algorithm !== "sha256" && algorithm !== "md5" || expected?.length !== (algorithm === "sha256" ? 64 : 32)) {
|
|
5958
6083
|
throw new Error("Invalid canonical filesystem payload key");
|
|
5959
6084
|
}
|
|
5960
|
-
const hash =
|
|
6085
|
+
const hash = createHash6(algorithm);
|
|
5961
6086
|
for (let chunk = readChunk();chunk !== undefined; chunk = readChunk())
|
|
5962
6087
|
hash.update(chunk);
|
|
5963
6088
|
if (hash.digest("hex") !== expected.toLowerCase())
|
|
@@ -6013,7 +6138,7 @@ if (storage === 'fs' && (storageRoot === '/data' || storageRoot.startsWith('/dat
|
|
|
6013
6138
|
}
|
|
6014
6139
|
`;
|
|
6015
6140
|
async function assertRestoreFreeSpace(directory, state, env, manifest2, backupPath) {
|
|
6016
|
-
const archiveBytes = await gzipUncompressedBytes(
|
|
6141
|
+
const archiveBytes = await gzipUncompressedBytes(join25(backupPath, "data.tgz"));
|
|
6017
6142
|
const requiredBytes = Math.max(manifest2.dataBytes ?? 0, archiveBytes);
|
|
6018
6143
|
const freeBytes = await dataVolumeFreeBytes(directory, state.mode, env);
|
|
6019
6144
|
const reclaimableBytes = await dataVolumeSize(directory, state.mode, env, "allocated");
|
|
@@ -6022,7 +6147,8 @@ async function assertRestoreFreeSpace(directory, state, env, manifest2, backupPa
|
|
|
6022
6147
|
async function restoreBackupData(directory, state, env, backupPath, options = {}) {
|
|
6023
6148
|
const manifest2 = await readManifest(backupPath, directory);
|
|
6024
6149
|
assertRestorableManagedAssets(manifest2);
|
|
6025
|
-
const
|
|
6150
|
+
const backupConfig = await readAuthenticatedBackupConfig(backupPath, manifest2);
|
|
6151
|
+
const backupEnv = parseEnv(backupConfig);
|
|
6026
6152
|
if (backupEnv.OR3_VERSION !== manifest2.appVersion || backupEnv.OR3_IMAGE !== manifest2.image) {
|
|
6027
6153
|
throw new Error(`Backup ${manifest2.backupId} configuration does not match its manifest.`);
|
|
6028
6154
|
}
|
|
@@ -6034,7 +6160,7 @@ async function restoreBackupData(directory, state, env, backupPath, options = {}
|
|
|
6034
6160
|
}
|
|
6035
6161
|
if (manifest2.imageDigest)
|
|
6036
6162
|
await requireImageDigest(manifest2.image, manifest2.imageDigest, `Backup ${manifest2.backupId}`);
|
|
6037
|
-
await assertExistingVaultCustody(directory, state, manifest2, backupPath,
|
|
6163
|
+
await assertExistingVaultCustody(directory, state, manifest2, backupPath, backupConfig);
|
|
6038
6164
|
await validateVolumeArchive(directory, state.mode, env, backupPath, backupEnv.OR3_STORAGE_FS_ROOT);
|
|
6039
6165
|
if (manifest2.schemaVersion === 2 && manifest2.remote.kind === "s3-copy-v1") {
|
|
6040
6166
|
const result = await checkRemoteCopyTarget(directory, manifest2, env, backupPath, manifest2.remote);
|
|
@@ -6136,18 +6262,18 @@ async function restorePreMutationSnapshot(directory, state, env) {
|
|
|
6136
6262
|
}
|
|
6137
6263
|
|
|
6138
6264
|
// src/deployment/observation.ts
|
|
6139
|
-
import { createHash as
|
|
6265
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
6140
6266
|
import { resolve as resolve14 } from "node:path";
|
|
6141
6267
|
|
|
6142
6268
|
// src/deployment/lease.ts
|
|
6143
6269
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
6144
6270
|
import { mkdir as mkdir10, rename as rename3, rm as rm10 } from "node:fs/promises";
|
|
6145
|
-
import { dirname as dirname7, join as
|
|
6271
|
+
import { dirname as dirname7, join as join27 } from "node:path";
|
|
6146
6272
|
|
|
6147
6273
|
// ../../shared/server/restarts/controller.ts
|
|
6148
|
-
import { randomUUID as randomUUID5, createHash as
|
|
6274
|
+
import { randomUUID as randomUUID5, createHash as createHash6 } from "node:crypto";
|
|
6149
6275
|
import { mkdir as mkdir9, open as open4, readFile as readFile5, rename as rename2, rm as rm9 } from "node:fs/promises";
|
|
6150
|
-
import { dirname as dirname6, join as
|
|
6276
|
+
import { dirname as dirname6, join as join26 } from "node:path";
|
|
6151
6277
|
|
|
6152
6278
|
// ../../node_modules/zod/v4/classic/external.js
|
|
6153
6279
|
var exports_external = {};
|
|
@@ -25596,7 +25722,7 @@ var recordSchema = exports_external.object({
|
|
|
25596
25722
|
expected: exports_external.record(exports_external.string(), exports_external.union([exports_external.string().max(128), exports_external.boolean()])),
|
|
25597
25723
|
executionIntent: exports_external.boolean(),
|
|
25598
25724
|
runtimeIdentity: exports_external.string().max(512).optional()
|
|
25599
|
-
}).strict().refine((record2) => record2.requestId === record2.request.requestId && record2.fingerprint ===
|
|
25725
|
+
}).strict().refine((record2) => record2.requestId === record2.request.requestId && record2.fingerprint === createHash6("sha256").update(JSON.stringify(record2.request)).digest("hex"), "Receipt identity mismatch");
|
|
25600
25726
|
|
|
25601
25727
|
class RestartError extends Error {
|
|
25602
25728
|
code;
|
|
@@ -25660,7 +25786,7 @@ class RestartController {
|
|
|
25660
25786
|
constructor(directory, driver) {
|
|
25661
25787
|
this.directory = directory;
|
|
25662
25788
|
this.driver = driver;
|
|
25663
|
-
this.path =
|
|
25789
|
+
this.path = join26(directory, "restarts.json");
|
|
25664
25790
|
}
|
|
25665
25791
|
lock(action) {
|
|
25666
25792
|
const next = this.serial.then(action, action);
|
|
@@ -25750,7 +25876,7 @@ class RestartController {
|
|
|
25750
25876
|
if (!parsed.success)
|
|
25751
25877
|
throw new RestartError("invalid_request", 400);
|
|
25752
25878
|
const request = parsed.data;
|
|
25753
|
-
const fingerprint =
|
|
25879
|
+
const fingerprint = createHash6("sha256").update(JSON.stringify(request)).digest("hex");
|
|
25754
25880
|
return this.lock(async () => {
|
|
25755
25881
|
const old = this.operations.find((o2) => o2.requestId === request.requestId);
|
|
25756
25882
|
if (old) {
|
|
@@ -25958,7 +26084,7 @@ class RestartController {
|
|
|
25958
26084
|
var DASHBOARD_LEASE_STALE_MS = 30000;
|
|
25959
26085
|
async function readLeaseOwner(leasePath) {
|
|
25960
26086
|
try {
|
|
25961
|
-
const owner = JSON.parse(await readText(
|
|
26087
|
+
const owner = JSON.parse(await readText(join27(leasePath, "owner.json")));
|
|
25962
26088
|
if (owner.schemaVersion !== 1 || typeof owner.nonce !== "string" || typeof owner.command !== "string" || owner.origin !== "cli" && owner.origin !== "dashboard" || !Number.isSafeInteger(owner.pid) || typeof owner.acquiredAt !== "string" || typeof owner.heartbeatAt !== "string")
|
|
25963
26089
|
return;
|
|
25964
26090
|
return owner;
|
|
@@ -26017,7 +26143,7 @@ async function acquireDeploymentLease(directory, command, dashboardJobId) {
|
|
|
26017
26143
|
}
|
|
26018
26144
|
}
|
|
26019
26145
|
try {
|
|
26020
|
-
await writeSecure(
|
|
26146
|
+
await writeSecure(join27(leasePath, "owner.json"), `${JSON.stringify(owner, null, 2)}
|
|
26021
26147
|
`);
|
|
26022
26148
|
} catch (error62) {
|
|
26023
26149
|
await rm10(leasePath, { recursive: true, force: true }).catch(() => {
|
|
@@ -26027,7 +26153,7 @@ async function acquireDeploymentLease(directory, command, dashboardJobId) {
|
|
|
26027
26153
|
}
|
|
26028
26154
|
const heartbeat = setInterval(() => {
|
|
26029
26155
|
owner.heartbeatAt = now();
|
|
26030
|
-
writeSecure(
|
|
26156
|
+
writeSecure(join27(leasePath, "owner.json"), `${JSON.stringify(owner, null, 2)}
|
|
26031
26157
|
`).catch(() => {
|
|
26032
26158
|
return;
|
|
26033
26159
|
});
|
|
@@ -26052,7 +26178,7 @@ async function withDeploymentLease(directory, command, action) {
|
|
|
26052
26178
|
}
|
|
26053
26179
|
async function assertRestartJournalSettled(directory) {
|
|
26054
26180
|
try {
|
|
26055
|
-
const journal = await readRestartJournal(
|
|
26181
|
+
const journal = await readRestartJournal(join27(directory, ".or3-cloud", "restarts", "restarts.json"));
|
|
26056
26182
|
if (journal.operations.some((operation) => !["succeeded", "cancelled", "failed"].includes(operation.phase))) {
|
|
26057
26183
|
throw new Error("A dashboard restart is unresolved. Run the compatible CLI recover --dry-run to inspect and recover --finish to reconcile it before another mutation.");
|
|
26058
26184
|
}
|
|
@@ -26235,7 +26361,7 @@ function observationFindings(observation) {
|
|
|
26235
26361
|
return findings;
|
|
26236
26362
|
}
|
|
26237
26363
|
function stateFingerprint(state) {
|
|
26238
|
-
return
|
|
26364
|
+
return createHash7("sha256").update(JSON.stringify({
|
|
26239
26365
|
schema: state.schemaVersion,
|
|
26240
26366
|
appVersion: state.appVersion,
|
|
26241
26367
|
image: state.image,
|
|
@@ -26738,7 +26864,6 @@ async function removeCommand(directory, flags) {
|
|
|
26738
26864
|
}
|
|
26739
26865
|
|
|
26740
26866
|
// src/commands/restore.ts
|
|
26741
|
-
import { join as join27 } from "node:path";
|
|
26742
26867
|
async function restoreCommand(directory, flags, positionals) {
|
|
26743
26868
|
if (!boolFlag(flags, "yes"))
|
|
26744
26869
|
throw new Error("Restore replaces live data. Re-run with --yes after confirming the backup and data-loss boundary.");
|
|
@@ -26751,7 +26876,8 @@ async function restoreCommand(directory, flags, positionals) {
|
|
|
26751
26876
|
const backupPath = await resolveBackup(loaded.directory, backupValue);
|
|
26752
26877
|
const manifest2 = await readManifest(backupPath, loaded.directory);
|
|
26753
26878
|
assertRestorableManagedAssets(manifest2);
|
|
26754
|
-
const
|
|
26879
|
+
const backupConfig = await readAuthenticatedBackupConfig(backupPath, manifest2);
|
|
26880
|
+
const backupEnv = parseEnv(backupConfig);
|
|
26755
26881
|
if (backupEnv.OR3_VERSION !== manifest2.appVersion || backupEnv.OR3_IMAGE !== manifest2.image) {
|
|
26756
26882
|
throw new Error(`Backup ${manifest2.backupId} configuration does not match its manifest.`);
|
|
26757
26883
|
}
|
|
@@ -26762,7 +26888,7 @@ async function restoreCommand(directory, flags, positionals) {
|
|
|
26762
26888
|
if (manifest2.imageDigest)
|
|
26763
26889
|
await pullAndRequireImage(manifest2.image, manifest2.imageDigest, `Backup ${manifest2.backupId}`);
|
|
26764
26890
|
await assertSupportedHostArchitecture(manifest2.image);
|
|
26765
|
-
await assertExistingVaultCustody(loaded.directory, loaded.state, manifest2, backupPath,
|
|
26891
|
+
await assertExistingVaultCustody(loaded.directory, loaded.state, manifest2, backupPath, backupConfig);
|
|
26766
26892
|
await assertRestoreFreeSpace(loaded.directory, loaded.state, loaded.env, manifest2, backupPath);
|
|
26767
26893
|
await validateVolumeArchive(loaded.directory, loaded.state.mode, loaded.env, backupPath, backupEnv.OR3_STORAGE_FS_ROOT);
|
|
26768
26894
|
const remotePreflight = manifest2.schemaVersion === 2 ? await preflightRemoteRestore(loaded.directory, manifest2, loaded.env, backupPath) : undefined;
|
|
@@ -26852,7 +26978,6 @@ async function restoreCommand(directory, flags, positionals) {
|
|
|
26852
26978
|
}
|
|
26853
26979
|
|
|
26854
26980
|
// src/commands/rollback.ts
|
|
26855
|
-
import { join as join28 } from "node:path";
|
|
26856
26981
|
async function rollbackCommand(directory, flags) {
|
|
26857
26982
|
if (!boolFlag(flags, "yes"))
|
|
26858
26983
|
throw new Error("Rollback restores the previous image and data snapshot. Re-run with --yes after confirming post-update data loss.");
|
|
@@ -26868,7 +26993,8 @@ async function rollbackCommand(directory, flags) {
|
|
|
26868
26993
|
if (manifest2.appVersion !== point.appVersion || manifest2.image !== point.image || manifest2.imageDigest !== point.imageDigest) {
|
|
26869
26994
|
throw new Error(`Rollback point ${point.backupId} no longer matches its recorded image/version. Refusing to mutate the deployment.`);
|
|
26870
26995
|
}
|
|
26871
|
-
const
|
|
26996
|
+
const backupConfig = await readAuthenticatedBackupConfig(backupPath, manifest2);
|
|
26997
|
+
const backupEnv = parseEnv(backupConfig);
|
|
26872
26998
|
if (backupEnv.OR3_VERSION !== manifest2.appVersion || backupEnv.OR3_IMAGE !== manifest2.image) {
|
|
26873
26999
|
throw new Error(`Backup ${manifest2.backupId} configuration does not match its manifest.`);
|
|
26874
27000
|
}
|
|
@@ -26878,7 +27004,7 @@ async function rollbackCommand(directory, flags) {
|
|
|
26878
27004
|
const recreateDataVolume = restoreRequiresVolumeRecreation(loaded.env, backupEnv);
|
|
26879
27005
|
await pullAndRequireImage(point.image, point.imageDigest, "Rollback");
|
|
26880
27006
|
await assertSupportedHostArchitecture(point.image);
|
|
26881
|
-
await assertExistingVaultCustody(loaded.directory, loaded.state, manifest2, backupPath,
|
|
27007
|
+
await assertExistingVaultCustody(loaded.directory, loaded.state, manifest2, backupPath, backupConfig);
|
|
26882
27008
|
await assertRestoreFreeSpace(loaded.directory, loaded.state, loaded.env, manifest2, backupPath);
|
|
26883
27009
|
await validateVolumeArchive(loaded.directory, loaded.state.mode, loaded.env, backupPath, backupEnv.OR3_STORAGE_FS_ROOT);
|
|
26884
27010
|
const remotePreflight = manifest2.schemaVersion === 2 ? await preflightRemoteRestore(loaded.directory, manifest2, loaded.env, backupPath) : undefined;
|
|
@@ -27090,7 +27216,7 @@ ${result.stderr}`);
|
|
|
27090
27216
|
|
|
27091
27217
|
// src/commands/update.ts
|
|
27092
27218
|
import { readFile as readFile6, statfs as statfs2 } from "node:fs/promises";
|
|
27093
|
-
import { join as
|
|
27219
|
+
import { join as join28, resolve as resolve15 } from "node:path";
|
|
27094
27220
|
async function assessUpdate(directory, targetVersion, options = {}) {
|
|
27095
27221
|
const checkDocker = options.checkDocker ?? false;
|
|
27096
27222
|
const observation = await observeDeployment(directory, { checkDocker, checkImage: false });
|
|
@@ -27155,7 +27281,7 @@ async function assessUpdate(directory, targetVersion, options = {}) {
|
|
|
27155
27281
|
for (const name of managedAssetNames(state?.mode ?? "local")) {
|
|
27156
27282
|
let desired;
|
|
27157
27283
|
try {
|
|
27158
|
-
desired = await readFile6(
|
|
27284
|
+
desired = await readFile6(join28(ASSET_ROOT, name));
|
|
27159
27285
|
} catch (error62) {
|
|
27160
27286
|
findings.push({
|
|
27161
27287
|
code: "managed-asset-unreadable",
|
|
@@ -27166,7 +27292,7 @@ async function assessUpdate(directory, targetVersion, options = {}) {
|
|
|
27166
27292
|
continue;
|
|
27167
27293
|
}
|
|
27168
27294
|
try {
|
|
27169
|
-
const installedPath =
|
|
27295
|
+
const installedPath = join28(directory, name);
|
|
27170
27296
|
if (!await fileExists(installedPath)) {
|
|
27171
27297
|
changed.push(`${name} (add)`);
|
|
27172
27298
|
continue;
|
|
@@ -27525,7 +27651,7 @@ async function runUpdateCommand(directory, flags, asJson) {
|
|
|
27525
27651
|
}
|
|
27526
27652
|
|
|
27527
27653
|
// src/commands/verify.ts
|
|
27528
|
-
import { join as
|
|
27654
|
+
import { join as join29, resolve as resolve16 } from "node:path";
|
|
27529
27655
|
async function verificationCredentials(directory, flags, env) {
|
|
27530
27656
|
const passwordFile = stringFlag(flags, "verification-password-file");
|
|
27531
27657
|
if (flags["verification-email"] !== undefined && !stringFlag(flags, "verification-email")) {
|
|
@@ -27537,7 +27663,7 @@ async function verificationCredentials(directory, flags, env) {
|
|
|
27537
27663
|
const configuredEmail = stringFlag(flags, "verification-email")?.trim() || env.OR3_MANAGED_OWNER_EMAIL || env.OR3_BASIC_AUTH_BOOTSTRAP_EMAIL;
|
|
27538
27664
|
let initialCredentials = {};
|
|
27539
27665
|
if (!configuredEmail || !passwordFile && !env.OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD) {
|
|
27540
|
-
const initialCredentialsPath =
|
|
27666
|
+
const initialCredentialsPath = join29(directory, ".or3-initial-credentials");
|
|
27541
27667
|
if (await fileExists(initialCredentialsPath)) {
|
|
27542
27668
|
initialCredentials = parseEnv(await readOwnerOnlyText(initialCredentialsPath, "The initial credentials file"));
|
|
27543
27669
|
}
|
|
@@ -27659,10 +27785,10 @@ async function verifyReadOnlyCommand(directory, flags) {
|
|
|
27659
27785
|
|
|
27660
27786
|
// src/commands/vault-key.ts
|
|
27661
27787
|
import { spawn as spawn2 } from "node:child_process";
|
|
27662
|
-
import { createHash as
|
|
27788
|
+
import { createHash as createHash8, randomUUID as randomUUID6 } from "node:crypto";
|
|
27663
27789
|
import { constants as constants2 } from "node:fs";
|
|
27664
27790
|
import { open as open5, realpath as realpath3, stat as stat6 } from "node:fs/promises";
|
|
27665
|
-
import { basename as basename7, dirname as dirname8, isAbsolute as isAbsolute6, join as
|
|
27791
|
+
import { basename as basename7, dirname as dirname8, isAbsolute as isAbsolute6, join as join30, sep as sep2 } from "node:path";
|
|
27666
27792
|
async function vaultKeyCommand(directory, positionals, flags) {
|
|
27667
27793
|
if (positionals.length !== 1 || positionals[0] !== "export")
|
|
27668
27794
|
throw new Error("Use vault-key export --out <absolute-path>.");
|
|
@@ -27677,7 +27803,7 @@ async function vaultKeyCommand(directory, positionals, flags) {
|
|
|
27677
27803
|
if (!parentInfo.isDirectory() || (parentInfo.mode & 511) !== 448 || typeof process.getuid === "function" && parentInfo.uid !== process.getuid()) {
|
|
27678
27804
|
throw new Error("Vault key destination parent must be an owner-only directory (0700).");
|
|
27679
27805
|
}
|
|
27680
|
-
const target =
|
|
27806
|
+
const target = join30(parent, basename7(destination));
|
|
27681
27807
|
const deployment = await realpath3(directory);
|
|
27682
27808
|
if (target === deployment || target.startsWith(`${deployment}${sep2}`))
|
|
27683
27809
|
throw new Error("Vault key destination must be outside the managed deployment directory.");
|
|
@@ -27765,7 +27891,7 @@ async function vaultKeyCommand(directory, positionals, flags) {
|
|
|
27765
27891
|
await parentHandle.close();
|
|
27766
27892
|
}
|
|
27767
27893
|
written = true;
|
|
27768
|
-
console.log(`Exported vault master key to ${target} (SHA-256 ${
|
|
27894
|
+
console.log(`Exported vault master key to ${target} (SHA-256 ${createHash8("sha256").update(key).digest("hex")}). Protect it separately from the backup.`);
|
|
27769
27895
|
} finally {
|
|
27770
27896
|
key.fill(0);
|
|
27771
27897
|
}
|
|
@@ -27905,11 +28031,11 @@ The installer never changes firewall, DNS, Cloudflare, or Tailscale settings.`);
|
|
|
27905
28031
|
|
|
27906
28032
|
// src/commands/recover-restart.ts
|
|
27907
28033
|
import { request } from "node:http";
|
|
27908
|
-
import { join as
|
|
28034
|
+
import { join as join31 } from "node:path";
|
|
27909
28035
|
async function recoverRestart(directory, flags) {
|
|
27910
28036
|
let journal;
|
|
27911
28037
|
try {
|
|
27912
|
-
journal = await readRestartJournal(
|
|
28038
|
+
journal = await readRestartJournal(join31(directory, ".or3-cloud", "restarts", "restarts.json"));
|
|
27913
28039
|
} catch (error62) {
|
|
27914
28040
|
if (error62.code === "ENOENT")
|
|
27915
28041
|
return false;
|
|
@@ -27934,7 +28060,7 @@ async function recoverRestart(directory, flags) {
|
|
|
27934
28060
|
const value = JSON.stringify({ requestId: operation.requestId });
|
|
27935
28061
|
await new Promise((resolve18, reject) => {
|
|
27936
28062
|
const req = request({
|
|
27937
|
-
socketPath:
|
|
28063
|
+
socketPath: join31(deploymentPaths(directory).operatorIpc, "operator.sock"),
|
|
27938
28064
|
path: "/lifecycle/v1/reconcile",
|
|
27939
28065
|
method: "POST",
|
|
27940
28066
|
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(value) }
|