@or3/cloud 0.1.70 → 0.1.74
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/README.md +7 -4
- package/dist/cli.mjs +102 -74
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -69,8 +69,8 @@ never a development image.
|
|
|
69
69
|
|
|
70
70
|
Run `verify` after every update and before declaring a deployment healthy. It
|
|
71
71
|
checks the managed image digest, deep provider health, Basic Auth sign-in,
|
|
72
|
-
session hydration, SQLite sync, a disposable filesystem upload/download
|
|
73
|
-
cycle, both SQLite databases, volume ownership, proxy runtime settings, and a
|
|
72
|
+
session hydration, SQLite sync, a disposable filesystem upload/download and
|
|
73
|
+
canonical metadata cleanup cycle, both SQLite databases, volume ownership, proxy runtime settings, and a
|
|
74
74
|
bounded window of serious container logs. On a public VPS, require the real
|
|
75
75
|
HTTPS path (with no redirects) using `npx @or3/cloud verify --public`.
|
|
76
76
|
After changing the owner password in the app, pass the current credential only
|
|
@@ -80,8 +80,11 @@ Until the one-time handoff file is deleted, `verify` may read its initial owner
|
|
|
80
80
|
credential from that protected file; it is never restored to `.env` or Docker
|
|
81
81
|
container metadata.
|
|
82
82
|
Verification rejects cross-origin filesystem grants and unexpected methods or
|
|
83
|
-
headers, validates the storage ID before uploading,
|
|
84
|
-
revokes its temporary session even when a later
|
|
83
|
+
headers, validates the storage ID before uploading, tombstones its probe before
|
|
84
|
+
requesting physical deletion, and revokes its temporary session even when a later
|
|
85
|
+
check fails. When the provider explicitly requires deletion coordination,
|
|
86
|
+
verification reports physical cleanup as deferred; retained probe bytes remain
|
|
87
|
+
subject to the provider retention policy. Other deletion failures still fail verification.
|
|
85
88
|
|
|
86
89
|
Do not run `docker compose down --volumes` on a normal deployment: it deletes
|
|
87
90
|
the application data. Use `backup` before an update or any destructive action.
|
package/dist/cli.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { fileURLToPath } from "node:url";
|
|
|
31
31
|
import { createGunzip } from "node:zlib";
|
|
32
32
|
var execFile = promisify(execFileCallback);
|
|
33
33
|
var PACKAGE_ROOT = resolve(fileURLToPath(new URL("../", import.meta.url)));
|
|
34
|
-
var PACKAGE_VERSION = "0.1.
|
|
34
|
+
var PACKAGE_VERSION = "0.1.74";
|
|
35
35
|
var IMAGE_REPOSITORY = "ghcr.io/saluana/or3-chat";
|
|
36
36
|
var ASSET_ROOT = resolve(fileURLToPath(new URL("../assets/", import.meta.url)));
|
|
37
37
|
var LEGACY_STATE_SCHEMA_VERSION = 1;
|
|
@@ -368,8 +368,8 @@ async function run(command, args, cwd, environment) {
|
|
|
368
368
|
});
|
|
369
369
|
return {
|
|
370
370
|
ok: true,
|
|
371
|
-
stdout: result.stdout
|
|
372
|
-
stderr: result.stderr
|
|
371
|
+
stdout: result.stdout,
|
|
372
|
+
stderr: result.stderr,
|
|
373
373
|
command: printable,
|
|
374
374
|
exitCode: 0
|
|
375
375
|
};
|
|
@@ -494,7 +494,7 @@ function serializeCredentialValue(value) {
|
|
|
494
494
|
return serializeEnvValue(value);
|
|
495
495
|
}
|
|
496
496
|
function serializeEnv(values) {
|
|
497
|
-
return `${Object.entries(values).filter((
|
|
497
|
+
return `${Object.entries(values).filter((entry) => entry[1] !== undefined).map(([key, value]) => `${key}=${serializeEnvValue(value)}`).join(`
|
|
498
498
|
`)}
|
|
499
499
|
`;
|
|
500
500
|
}
|
|
@@ -968,10 +968,12 @@ async function managedVolumeRootOwnership(image, volume) {
|
|
|
968
968
|
throw new Error(`Managed volume ${volume} returned an invalid root ownership value.`);
|
|
969
969
|
return { uid: Number(match[1]), gid: Number(match[2]) };
|
|
970
970
|
}
|
|
971
|
-
async function setManagedVolumeRootOwnership(
|
|
971
|
+
async function setManagedVolumeRootOwnership(directory, state, env, image, ownership) {
|
|
972
972
|
if (!Number.isSafeInteger(ownership.uid) || ownership.uid < 0 || !Number.isSafeInteger(ownership.gid) || ownership.gid < 0) {
|
|
973
973
|
throw new Error("Refusing an invalid managed volume root UID/GID.");
|
|
974
974
|
}
|
|
975
|
+
await ensureManagedDataVolume(directory, state.mode, state, env);
|
|
976
|
+
const volume = state.volumeName;
|
|
975
977
|
const result = await run("docker", [
|
|
976
978
|
"run",
|
|
977
979
|
"--rm",
|
|
@@ -1036,7 +1038,7 @@ async function removeManagedDataVolumeForRecreation(directory, state) {
|
|
|
1036
1038
|
} catch {
|
|
1037
1039
|
throw new Error(`Docker returned unreadable metadata for managed volume ${state.volumeName}.`);
|
|
1038
1040
|
}
|
|
1039
|
-
if (volume.Name !== state.volumeName || volume.Labels?.["com.docker.compose.project"] !== state.composeProject || volume.Labels
|
|
1041
|
+
if (volume.Name !== state.volumeName || volume.Labels?.["com.docker.compose.project"] !== state.composeProject || volume.Labels["com.docker.compose.volume"] !== "or3-data") {
|
|
1040
1042
|
throw new Error(`Volume ${state.volumeName} is not bound to this managed Compose deployment. Refusing to recreate it.`);
|
|
1041
1043
|
}
|
|
1042
1044
|
const removedVolume = await run("docker", ["volume", "rm", state.volumeName], directory);
|
|
@@ -1045,35 +1047,42 @@ async function removeManagedDataVolumeForRecreation(directory, state) {
|
|
|
1045
1047
|
}
|
|
1046
1048
|
async function ensureManagedDataVolume(directory, mode, state, env) {
|
|
1047
1049
|
const existing = await run("docker", ["volume", "inspect", state.volumeName, "--format", "{{.Name}}"], directory);
|
|
1048
|
-
if (existing.ok && existing.stdout.trim()
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
"
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1050
|
+
if (existing.ok && existing.stdout.trim() !== state.volumeName) {
|
|
1051
|
+
throw new Error(`Docker returned an unexpected name for managed volume ${state.volumeName}.`);
|
|
1052
|
+
}
|
|
1053
|
+
if (!existing.ok) {
|
|
1054
|
+
if (existing.exitCode !== 1 || !/no such volume/i.test(`${existing.stdout}
|
|
1055
|
+
${existing.stderr}`)) {
|
|
1056
|
+
throw new Error(`Could not inspect managed volume ${state.volumeName}. Refusing to assume it is missing. ${redact(existing.stderr, secretValues(env))}`);
|
|
1057
|
+
}
|
|
1058
|
+
const created = await run("docker", composeArgs(directory, mode, [
|
|
1059
|
+
"run",
|
|
1060
|
+
"--rm",
|
|
1061
|
+
"-T",
|
|
1062
|
+
"--no-deps",
|
|
1063
|
+
"--entrypoint",
|
|
1064
|
+
"sh",
|
|
1065
|
+
"or3",
|
|
1066
|
+
"-c",
|
|
1067
|
+
"true"
|
|
1068
|
+
]), directory);
|
|
1069
|
+
if (!created.ok)
|
|
1070
|
+
throw new Error(`Could not create the managed data volume. ${redact(created.stderr, secretValues(env))}`);
|
|
1071
|
+
}
|
|
1063
1072
|
const inspected = await run("docker", ["volume", "inspect", state.volumeName, "--format", "{{json .Labels}}"], directory);
|
|
1064
1073
|
if (!inspected.ok)
|
|
1065
|
-
throw new Error(`Could not verify
|
|
1074
|
+
throw new Error(`Could not verify managed data volume ${state.volumeName}.`);
|
|
1066
1075
|
let labels;
|
|
1067
1076
|
try {
|
|
1068
1077
|
labels = JSON.parse(inspected.stdout.trim());
|
|
1069
1078
|
} catch {
|
|
1070
|
-
throw new Error(`Docker returned unreadable labels for
|
|
1079
|
+
throw new Error(`Docker returned unreadable labels for managed volume ${state.volumeName}.`);
|
|
1071
1080
|
}
|
|
1072
1081
|
if (!labels || typeof labels !== "object") {
|
|
1073
|
-
throw new Error(`
|
|
1082
|
+
throw new Error(`Volume ${state.volumeName} has no managed deployment labels.`);
|
|
1074
1083
|
}
|
|
1075
1084
|
if (labels["com.docker.compose.project"] !== state.composeProject || labels["com.docker.compose.volume"] !== "or3-data" || env.OR3_DEPLOYMENT_ID && labels["io.or3.cloud.deployment-id"] !== env.OR3_DEPLOYMENT_ID) {
|
|
1076
|
-
throw new Error(`
|
|
1085
|
+
throw new Error(`Volume ${state.volumeName} does not carry the expected managed deployment labels.`);
|
|
1077
1086
|
}
|
|
1078
1087
|
}
|
|
1079
1088
|
async function portAvailable(port) {
|
|
@@ -1269,6 +1278,8 @@ async function assertImageReleaseIdentity(image, version) {
|
|
|
1269
1278
|
} catch {
|
|
1270
1279
|
throw new Error(`OR3 image ${image} has no readable release labels.`);
|
|
1271
1280
|
}
|
|
1281
|
+
if (!labels || typeof labels !== "object")
|
|
1282
|
+
throw new Error(`OR3 image ${image} has no readable release labels.`);
|
|
1272
1283
|
assertImageReleaseLabels(image, labels, version, packagedSourceRevision(version));
|
|
1273
1284
|
}
|
|
1274
1285
|
async function imageDigest(image) {
|
|
@@ -1636,6 +1647,7 @@ async function installManagedAssets(directory, assets) {
|
|
|
1636
1647
|
const token = randomBytes(4).toString("hex");
|
|
1637
1648
|
const staged = [];
|
|
1638
1649
|
let retainRollbackCopies = false;
|
|
1650
|
+
const canCleanRollback = () => !retainRollbackCopies;
|
|
1639
1651
|
try {
|
|
1640
1652
|
for (const [name, contents] of assets) {
|
|
1641
1653
|
const destination = join(directory, name);
|
|
@@ -1687,7 +1699,7 @@ async function installManagedAssets(directory, assets) {
|
|
|
1687
1699
|
await rm(entry.replacement, { force: true }).catch(() => {
|
|
1688
1700
|
return;
|
|
1689
1701
|
});
|
|
1690
|
-
if (entry.rollback &&
|
|
1702
|
+
if (entry.rollback && canCleanRollback())
|
|
1691
1703
|
await rm(entry.rollback, { force: true }).catch(() => {
|
|
1692
1704
|
return;
|
|
1693
1705
|
});
|
|
@@ -2330,6 +2342,7 @@ async function streamCommandToFile(command, args, destination, cwd, secrets = []
|
|
|
2330
2342
|
});
|
|
2331
2343
|
let stderr = "";
|
|
2332
2344
|
let timedOut = false;
|
|
2345
|
+
const hasTimedOut = () => timedOut;
|
|
2333
2346
|
const timeout = setTimeout(() => {
|
|
2334
2347
|
timedOut = true;
|
|
2335
2348
|
terminateChildProcess(child);
|
|
@@ -2347,7 +2360,7 @@ async function streamCommandToFile(command, args, destination, cwd, secrets = []
|
|
|
2347
2360
|
exit,
|
|
2348
2361
|
pipeline(child.stdout, createWriteStream(temporary, { flags: "wx", mode: 384 }))
|
|
2349
2362
|
]);
|
|
2350
|
-
if (
|
|
2363
|
+
if (hasTimedOut())
|
|
2351
2364
|
throw new Error(`${command} exceeded the ${STREAM_COMMAND_TIMEOUT_MS / 1000}-second archive deadline.`);
|
|
2352
2365
|
if (exitCode !== 0) {
|
|
2353
2366
|
throw new Error(`${command} ${args.join(" ")} exited with ${exitCode}. ${redact(stderr, secrets)}`.trim());
|
|
@@ -2375,6 +2388,7 @@ async function streamFileToCommand(command, args, source, cwd, secrets = []) {
|
|
|
2375
2388
|
});
|
|
2376
2389
|
let stderr = "";
|
|
2377
2390
|
let timedOut = false;
|
|
2391
|
+
const hasTimedOut = () => timedOut;
|
|
2378
2392
|
const timeout = setTimeout(() => {
|
|
2379
2393
|
timedOut = true;
|
|
2380
2394
|
terminateChildProcess(child);
|
|
@@ -2392,7 +2406,7 @@ async function streamFileToCommand(command, args, source, cwd, secrets = []) {
|
|
|
2392
2406
|
exit,
|
|
2393
2407
|
pipeline(createReadStream(source), child.stdin)
|
|
2394
2408
|
]);
|
|
2395
|
-
if (
|
|
2409
|
+
if (hasTimedOut())
|
|
2396
2410
|
throw new Error(`${command} exceeded the ${STREAM_COMMAND_TIMEOUT_MS / 1000}-second archive deadline.`);
|
|
2397
2411
|
if (exitCode !== 0) {
|
|
2398
2412
|
throw new Error(`${command} ${args.join(" ")} exited with ${exitCode}. ${redact(stderr, secrets)}`.trim());
|
|
@@ -3785,7 +3799,7 @@ async function assessUpdate(directory, targetVersion, options = {}) {
|
|
|
3785
3799
|
}
|
|
3786
3800
|
try {
|
|
3787
3801
|
assertKnownStateSchema(state?.schemaVersion);
|
|
3788
|
-
checks.push({ code: "state-schema", status: "passed", detail: `Managed state schema ${String(state
|
|
3802
|
+
checks.push({ code: "state-schema", status: "passed", detail: `Managed state schema ${String(state.schemaVersion)} is supported.` });
|
|
3789
3803
|
} catch (error) {
|
|
3790
3804
|
findings.push({ code: "state-schema-unsupported", severity: "blocker", message: error instanceof Error ? error.message : String(error) });
|
|
3791
3805
|
}
|
|
@@ -3954,7 +3968,7 @@ async function runUpdateCommand(directory, flags, asJson) {
|
|
|
3954
3968
|
await stopProject(loaded.directory, state.mode);
|
|
3955
3969
|
}
|
|
3956
3970
|
if (migrateLegacyVolume && !recreateDataVolume) {
|
|
3957
|
-
await setManagedVolumeRootOwnership(
|
|
3971
|
+
await setManagedVolumeRootOwnership(loaded.directory, state, oldEnv, targetImage, {
|
|
3958
3972
|
uid: MANAGED_RUNTIME_UID,
|
|
3959
3973
|
gid: MANAGED_RUNTIME_GID
|
|
3960
3974
|
});
|
|
@@ -3972,7 +3986,7 @@ async function runUpdateCommand(directory, flags, asJson) {
|
|
|
3972
3986
|
return;
|
|
3973
3987
|
});
|
|
3974
3988
|
if (migrateLegacyVolume && !recreateDataVolume) {
|
|
3975
|
-
await setManagedVolumeRootOwnership(
|
|
3989
|
+
await setManagedVolumeRootOwnership(loaded.directory, state, oldEnv, targetImage, previousRootOwnership);
|
|
3976
3990
|
}
|
|
3977
3991
|
await restoreBackupData(loaded.directory, state, oldEnv, backup.backupDir, { recreateDataVolume });
|
|
3978
3992
|
} catch (restoreError) {
|
|
@@ -4164,7 +4178,7 @@ async function restorePreMutationSnapshot(directory, state, env) {
|
|
|
4164
4178
|
}
|
|
4165
4179
|
if (pending.previousRootOwnership) {
|
|
4166
4180
|
await pullAndRequireImage(state.image, state.imageDigest, "Previous deployment");
|
|
4167
|
-
await setManagedVolumeRootOwnership(state
|
|
4181
|
+
await setManagedVolumeRootOwnership(directory, state, env, state.image, pending.previousRootOwnership);
|
|
4168
4182
|
}
|
|
4169
4183
|
await restoreBackupData(directory, state, env, previous.path);
|
|
4170
4184
|
return previous;
|
|
@@ -4504,7 +4518,7 @@ async function doctorCommand(directory) {
|
|
|
4504
4518
|
}
|
|
4505
4519
|
function validateVerificationHealth(value) {
|
|
4506
4520
|
const health = value;
|
|
4507
|
-
if (!health || health.status !== "ok" || health.providers?.auth?.provider !== "basic-auth" || health.providers
|
|
4521
|
+
if (!health || health.status !== "ok" || health.providers?.auth?.provider !== "basic-auth" || health.providers.sync?.provider !== "sqlite" || health.providers.storage?.provider !== "fs") {
|
|
4508
4522
|
throw new Error("Public deep health does not report the managed Basic Auth + SQLite + filesystem profile.");
|
|
4509
4523
|
}
|
|
4510
4524
|
return health;
|
|
@@ -4524,15 +4538,29 @@ async function verificationJson(baseUrl, path, options = {}) {
|
|
|
4524
4538
|
accept: "application/json",
|
|
4525
4539
|
...options.body === undefined ? {} : { "content-type": "application/json" },
|
|
4526
4540
|
...options.cookie ? { cookie: options.cookie } : {},
|
|
4527
|
-
...method === "GET" ? {} : { origin: baseUrl.origin }
|
|
4541
|
+
...method === "GET" ? {} : { origin: baseUrl.origin, "x-or3-cloud-intent": "mutation" }
|
|
4528
4542
|
},
|
|
4529
4543
|
...options.body === undefined ? {} : { body: JSON.stringify(options.body) }
|
|
4530
4544
|
});
|
|
4531
4545
|
if (response.status !== 200) {
|
|
4532
|
-
const
|
|
4546
|
+
const responseText = await response.text().catch(() => "");
|
|
4547
|
+
const detail = responseText.slice(0, 300);
|
|
4548
|
+
if (options.allowStorageDeletionDeferral && path === "/api/storage/delete" && response.status === 503) {
|
|
4549
|
+
let failure;
|
|
4550
|
+
try {
|
|
4551
|
+
failure = JSON.parse(responseText);
|
|
4552
|
+
} catch {}
|
|
4553
|
+
if (failure && typeof failure === "object" && "statusMessage" in failure && failure.statusMessage === "Provider-owned deletion coordination is required") {
|
|
4554
|
+
return { deletionDeferred: true };
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4533
4557
|
throw new Error(`${response.url} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
4534
4558
|
}
|
|
4535
|
-
|
|
4559
|
+
const value = await response.json();
|
|
4560
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4561
|
+
throw new Error(`${response.url} returned an invalid JSON object.`);
|
|
4562
|
+
}
|
|
4563
|
+
return value;
|
|
4536
4564
|
}
|
|
4537
4565
|
function sameOriginVerificationUrl(baseUrl, value, label) {
|
|
4538
4566
|
if (typeof value !== "string")
|
|
@@ -4599,7 +4627,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
4599
4627
|
throw new Error("Public Basic Auth sign-in did not set a session cookie.");
|
|
4600
4628
|
try {
|
|
4601
4629
|
const session = await verificationJson(baseUrl, "/api/auth/session", { cookie });
|
|
4602
|
-
if (session.session?.user?.email !== email || !session.session
|
|
4630
|
+
if (session.session?.user?.email !== email || !session.session.workspace?.id) {
|
|
4603
4631
|
throw new Error("Public session hydration did not return the verification user and workspace.");
|
|
4604
4632
|
}
|
|
4605
4633
|
const workspaceId = String(session.session.workspace.id);
|
|
@@ -4617,6 +4645,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
4617
4645
|
const hash = `sha256:${createHash("sha256").update(probeBytes).digest("hex")}`;
|
|
4618
4646
|
let storageId;
|
|
4619
4647
|
let metadataAttempted = false;
|
|
4648
|
+
let physicalCleanupDeferred = false;
|
|
4620
4649
|
try {
|
|
4621
4650
|
const presign = await verificationJson(baseUrl, "/api/storage/presign-upload", {
|
|
4622
4651
|
cookie,
|
|
@@ -4639,6 +4668,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
4639
4668
|
});
|
|
4640
4669
|
if (!upload.ok)
|
|
4641
4670
|
throw new Error(`Filesystem verification upload returned HTTP ${upload.status}.`);
|
|
4671
|
+
metadataAttempted = true;
|
|
4642
4672
|
await verificationJson(baseUrl, "/api/storage/commit", {
|
|
4643
4673
|
cookie,
|
|
4644
4674
|
body: {
|
|
@@ -4654,7 +4684,6 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
4654
4684
|
});
|
|
4655
4685
|
const metadataCreatedAt = Date.now();
|
|
4656
4686
|
const metadataOpId = randomUUID();
|
|
4657
|
-
metadataAttempted = true;
|
|
4658
4687
|
const pushed = await verificationJson(baseUrl, "/api/sync/push", {
|
|
4659
4688
|
cookie,
|
|
4660
4689
|
body: {
|
|
@@ -4702,45 +4731,44 @@ async function verifyPublicApplication(baseUrl, credentials) {
|
|
|
4702
4731
|
}
|
|
4703
4732
|
} finally {
|
|
4704
4733
|
if (storageId) {
|
|
4705
|
-
|
|
4706
|
-
|
|
4734
|
+
if (metadataAttempted) {
|
|
4735
|
+
const metadataDeleteAt = Date.now();
|
|
4736
|
+
const metadataDeleteOpId = randomUUID();
|
|
4737
|
+
const deleted = await verificationJson(baseUrl, "/api/sync/push", {
|
|
4707
4738
|
cookie,
|
|
4708
|
-
body: {
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
opId: metadataDeleteOpId,
|
|
4727
|
-
hlc: `${String(metadataDeleteAt).padStart(13, "0")}:0000:or3-cloud-verification`,
|
|
4728
|
-
clock: metadataDeleteAt
|
|
4729
|
-
},
|
|
4730
|
-
createdAt: metadataDeleteAt,
|
|
4731
|
-
attempts: 0,
|
|
4732
|
-
status: "pending"
|
|
4733
|
-
}]
|
|
4734
|
-
}
|
|
4735
|
-
});
|
|
4736
|
-
if (deleted.results?.[0]?.success !== true) {
|
|
4737
|
-
throw new Error(`Filesystem verification metadata cleanup failed: ${JSON.stringify(deleted)}`);
|
|
4739
|
+
body: {
|
|
4740
|
+
scope: { workspaceId },
|
|
4741
|
+
ops: [{
|
|
4742
|
+
id: `or3-verification-${metadataDeleteOpId}`,
|
|
4743
|
+
tableName: "file_meta",
|
|
4744
|
+
operation: "delete",
|
|
4745
|
+
pk: hash,
|
|
4746
|
+
payload: { hash },
|
|
4747
|
+
stamp: {
|
|
4748
|
+
deviceId: "or3-cloud-verification",
|
|
4749
|
+
opId: metadataDeleteOpId,
|
|
4750
|
+
hlc: `${String(metadataDeleteAt).padStart(13, "0")}:0000:or3-cloud-verification`,
|
|
4751
|
+
clock: metadataDeleteAt
|
|
4752
|
+
},
|
|
4753
|
+
createdAt: metadataDeleteAt,
|
|
4754
|
+
attempts: 0,
|
|
4755
|
+
status: "pending"
|
|
4756
|
+
}]
|
|
4738
4757
|
}
|
|
4758
|
+
});
|
|
4759
|
+
if (deleted.results?.[0]?.success !== true) {
|
|
4760
|
+
throw new Error(`Filesystem verification metadata cleanup failed: ${JSON.stringify(deleted)}`);
|
|
4739
4761
|
}
|
|
4740
4762
|
}
|
|
4763
|
+
const deletion = await verificationJson(baseUrl, "/api/storage/delete", {
|
|
4764
|
+
cookie,
|
|
4765
|
+
body: { workspace_id: workspaceId, hash, storage_id: storageId },
|
|
4766
|
+
allowStorageDeletionDeferral: true
|
|
4767
|
+
});
|
|
4768
|
+
physicalCleanupDeferred = deletion.deletionDeferred === true;
|
|
4741
4769
|
}
|
|
4742
4770
|
}
|
|
4743
|
-
return health;
|
|
4771
|
+
return { ...health, physicalCleanupDeferred };
|
|
4744
4772
|
} finally {
|
|
4745
4773
|
await verificationJson(baseUrl, "/api/basic-auth/sign-out", {
|
|
4746
4774
|
cookie,
|
|
@@ -4767,7 +4795,7 @@ async function provisionManagedCredentialsOnce(baseUrl, email, password, adminUs
|
|
|
4767
4795
|
const appCookie = responseCookie(appSignIn, "Managed owner provisioning");
|
|
4768
4796
|
try {
|
|
4769
4797
|
const session = await verificationJson(baseUrl, "/api/auth/session", { cookie: appCookie });
|
|
4770
|
-
if (session.session?.user?.email !== email || !session.session
|
|
4798
|
+
if (session.session?.user?.email !== email || !session.session.workspace?.id) {
|
|
4771
4799
|
throw new Error("Managed owner provisioning did not create the expected account and workspace.");
|
|
4772
4800
|
}
|
|
4773
4801
|
} finally {
|
|
@@ -4818,7 +4846,7 @@ async function verifyCommand(directory, flags, positionals) {
|
|
|
4818
4846
|
}
|
|
4819
4847
|
await waitForDeepHealth(loaded.directory, loaded.state.mode, secretValues(loaded.env));
|
|
4820
4848
|
const baseUrl = new URL(loaded.state.mode === "public" ? `https://${loaded.state.domain}` : `http://127.0.0.1:${loaded.state.port}`);
|
|
4821
|
-
await verifyPublicApplication(baseUrl, await verificationCredentials(loaded.directory, flags, loaded.env));
|
|
4849
|
+
const verification = await verifyPublicApplication(baseUrl, await verificationCredentials(loaded.directory, flags, loaded.env));
|
|
4822
4850
|
const databaseCheck = await run("docker", [
|
|
4823
4851
|
...composeArgs(loaded.directory, loaded.state.mode, ["exec", "-T", "or3", ...containerNodeCommand(VERIFY_DATABASES_SCRIPT)])
|
|
4824
4852
|
], loaded.directory);
|
|
@@ -4849,7 +4877,7 @@ ${redact(serious.slice(-20).join(`
|
|
|
4849
4877
|
console.log(`✓ OR3 ${loaded.state.appVersion} image digest matches managed state`);
|
|
4850
4878
|
console.log(`✓ ${baseUrl.origin} root and deep health return HTTP 200 without redirects`);
|
|
4851
4879
|
console.log("✓ Basic Auth sign-in, session hydration, and SQLite sync pull passed");
|
|
4852
|
-
console.log("✓ Filesystem storage write/read/
|
|
4880
|
+
console.log(verification.physicalCleanupDeferred ? "✓ Filesystem storage write/read and canonical metadata cleanup passed; physical cleanup deferred by provider retention policy" : "✓ Filesystem storage write/read and canonical/physical cleanup passed");
|
|
4853
4881
|
console.log("✓ auth.sqlite and sync.sqlite quick_check passed with managed ownership");
|
|
4854
4882
|
console.log("✓ Recent bounded logs contain no fatal, panic, unhandled, or OOM events");
|
|
4855
4883
|
console.log("OR3 production verification passed.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@or3/cloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"description": "Install and operate the OR3 Cloud container distribution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"dashboardUpdateProtocol": 1,
|
|
20
20
|
"dashboardUpdateMinimumSourceVersion": "0.1.40",
|
|
21
21
|
"stateSchema": 1,
|
|
22
|
-
"imageDigest": "sha256:
|
|
23
|
-
"operatorImageDigest": "sha256:
|
|
24
|
-
"sourceRevision": "
|
|
22
|
+
"imageDigest": "sha256:d01c7513cb26858b23c279ab0cfe39a28bb73691e69e566be29460ed385d42ca",
|
|
23
|
+
"operatorImageDigest": "sha256:bd7162734324f40daa85861c314771d7437cbde258f0b45ae7d6a878810e5fdf",
|
|
24
|
+
"sourceRevision": "77049d904cf405b3f5a33ef85b622d91c9250008"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|