@or3/cloud 0.1.72 → 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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/dist/cli.mjs +80 -61
  3. 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/delete
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, deletes its probe, and
84
- revokes its temporary session even when a later check fails.
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.72";
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;
@@ -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(image, volume, ownership) {
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",
@@ -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() === state.volumeName)
1049
- return;
1050
- const created = await run("docker", composeArgs(directory, mode, [
1051
- "run",
1052
- "--rm",
1053
- "-T",
1054
- "--no-deps",
1055
- "--entrypoint",
1056
- "sh",
1057
- "or3",
1058
- "-c",
1059
- "true"
1060
- ]), directory);
1061
- if (!created.ok)
1062
- throw new Error(`Could not create the managed data volume. ${redact(created.stderr, secretValues(env))}`);
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 the recreated managed data volume ${state.volumeName}.`);
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 recreated managed volume ${state.volumeName}.`);
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(`Recreated volume ${state.volumeName} has no managed deployment labels.`);
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(`Recreated volume ${state.volumeName} does not carry the expected managed deployment labels.`);
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) {
@@ -3959,7 +3968,7 @@ async function runUpdateCommand(directory, flags, asJson) {
3959
3968
  await stopProject(loaded.directory, state.mode);
3960
3969
  }
3961
3970
  if (migrateLegacyVolume && !recreateDataVolume) {
3962
- await setManagedVolumeRootOwnership(targetImage, state.volumeName, {
3971
+ await setManagedVolumeRootOwnership(loaded.directory, state, oldEnv, targetImage, {
3963
3972
  uid: MANAGED_RUNTIME_UID,
3964
3973
  gid: MANAGED_RUNTIME_GID
3965
3974
  });
@@ -3977,7 +3986,7 @@ async function runUpdateCommand(directory, flags, asJson) {
3977
3986
  return;
3978
3987
  });
3979
3988
  if (migrateLegacyVolume && !recreateDataVolume) {
3980
- await setManagedVolumeRootOwnership(targetImage, state.volumeName, previousRootOwnership);
3989
+ await setManagedVolumeRootOwnership(loaded.directory, state, oldEnv, targetImage, previousRootOwnership);
3981
3990
  }
3982
3991
  await restoreBackupData(loaded.directory, state, oldEnv, backup.backupDir, { recreateDataVolume });
3983
3992
  } catch (restoreError) {
@@ -4169,7 +4178,7 @@ async function restorePreMutationSnapshot(directory, state, env) {
4169
4178
  }
4170
4179
  if (pending.previousRootOwnership) {
4171
4180
  await pullAndRequireImage(state.image, state.imageDigest, "Previous deployment");
4172
- await setManagedVolumeRootOwnership(state.image, state.volumeName, pending.previousRootOwnership);
4181
+ await setManagedVolumeRootOwnership(directory, state, env, state.image, pending.previousRootOwnership);
4173
4182
  }
4174
4183
  await restoreBackupData(directory, state, env, previous.path);
4175
4184
  return previous;
@@ -4534,7 +4543,17 @@ async function verificationJson(baseUrl, path, options = {}) {
4534
4543
  ...options.body === undefined ? {} : { body: JSON.stringify(options.body) }
4535
4544
  });
4536
4545
  if (response.status !== 200) {
4537
- const detail = (await response.text().catch(() => "")).slice(0, 300);
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
+ }
4538
4557
  throw new Error(`${response.url} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
4539
4558
  }
4540
4559
  const value = await response.json();
@@ -4626,6 +4645,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
4626
4645
  const hash = `sha256:${createHash("sha256").update(probeBytes).digest("hex")}`;
4627
4646
  let storageId;
4628
4647
  let metadataAttempted = false;
4648
+ let physicalCleanupDeferred = false;
4629
4649
  try {
4630
4650
  const presign = await verificationJson(baseUrl, "/api/storage/presign-upload", {
4631
4651
  cookie,
@@ -4648,6 +4668,7 @@ async function verifyPublicApplication(baseUrl, credentials) {
4648
4668
  });
4649
4669
  if (!upload.ok)
4650
4670
  throw new Error(`Filesystem verification upload returned HTTP ${upload.status}.`);
4671
+ metadataAttempted = true;
4651
4672
  await verificationJson(baseUrl, "/api/storage/commit", {
4652
4673
  cookie,
4653
4674
  body: {
@@ -4663,7 +4684,6 @@ async function verifyPublicApplication(baseUrl, credentials) {
4663
4684
  });
4664
4685
  const metadataCreatedAt = Date.now();
4665
4686
  const metadataOpId = randomUUID();
4666
- metadataAttempted = true;
4667
4687
  const pushed = await verificationJson(baseUrl, "/api/sync/push", {
4668
4688
  cookie,
4669
4689
  body: {
@@ -4711,45 +4731,44 @@ async function verifyPublicApplication(baseUrl, credentials) {
4711
4731
  }
4712
4732
  } finally {
4713
4733
  if (storageId) {
4714
- try {
4715
- await verificationJson(baseUrl, "/api/storage/delete", {
4734
+ if (metadataAttempted) {
4735
+ const metadataDeleteAt = Date.now();
4736
+ const metadataDeleteOpId = randomUUID();
4737
+ const deleted = await verificationJson(baseUrl, "/api/sync/push", {
4716
4738
  cookie,
4717
- body: { workspace_id: workspaceId, hash, storage_id: storageId }
4718
- });
4719
- } finally {
4720
- if (metadataAttempted) {
4721
- const metadataDeleteAt = Date.now();
4722
- const metadataDeleteOpId = randomUUID();
4723
- const deleted = await verificationJson(baseUrl, "/api/sync/push", {
4724
- cookie,
4725
- body: {
4726
- scope: { workspaceId },
4727
- ops: [{
4728
- id: `or3-verification-${metadataDeleteOpId}`,
4729
- tableName: "file_meta",
4730
- operation: "delete",
4731
- pk: hash,
4732
- payload: { hash },
4733
- stamp: {
4734
- deviceId: "or3-cloud-verification",
4735
- opId: metadataDeleteOpId,
4736
- hlc: `${String(metadataDeleteAt).padStart(13, "0")}:0000:or3-cloud-verification`,
4737
- clock: metadataDeleteAt
4738
- },
4739
- createdAt: metadataDeleteAt,
4740
- attempts: 0,
4741
- status: "pending"
4742
- }]
4743
- }
4744
- });
4745
- if (deleted.results?.[0]?.success !== true) {
4746
- 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
+ }]
4747
4757
  }
4758
+ });
4759
+ if (deleted.results?.[0]?.success !== true) {
4760
+ throw new Error(`Filesystem verification metadata cleanup failed: ${JSON.stringify(deleted)}`);
4748
4761
  }
4749
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;
4750
4769
  }
4751
4770
  }
4752
- return health;
4771
+ return { ...health, physicalCleanupDeferred };
4753
4772
  } finally {
4754
4773
  await verificationJson(baseUrl, "/api/basic-auth/sign-out", {
4755
4774
  cookie,
@@ -4827,7 +4846,7 @@ async function verifyCommand(directory, flags, positionals) {
4827
4846
  }
4828
4847
  await waitForDeepHealth(loaded.directory, loaded.state.mode, secretValues(loaded.env));
4829
4848
  const baseUrl = new URL(loaded.state.mode === "public" ? `https://${loaded.state.domain}` : `http://127.0.0.1:${loaded.state.port}`);
4830
- await verifyPublicApplication(baseUrl, await verificationCredentials(loaded.directory, flags, loaded.env));
4849
+ const verification = await verifyPublicApplication(baseUrl, await verificationCredentials(loaded.directory, flags, loaded.env));
4831
4850
  const databaseCheck = await run("docker", [
4832
4851
  ...composeArgs(loaded.directory, loaded.state.mode, ["exec", "-T", "or3", ...containerNodeCommand(VERIFY_DATABASES_SCRIPT)])
4833
4852
  ], loaded.directory);
@@ -4858,7 +4877,7 @@ ${redact(serious.slice(-20).join(`
4858
4877
  console.log(`✓ OR3 ${loaded.state.appVersion} image digest matches managed state`);
4859
4878
  console.log(`✓ ${baseUrl.origin} root and deep health return HTTP 200 without redirects`);
4860
4879
  console.log("✓ Basic Auth sign-in, session hydration, and SQLite sync pull passed");
4861
- console.log("✓ Filesystem storage write/read/delete probe passed");
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");
4862
4881
  console.log("✓ auth.sqlite and sync.sqlite quick_check passed with managed ownership");
4863
4882
  console.log("✓ Recent bounded logs contain no fatal, panic, unhandled, or OOM events");
4864
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.72",
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:c4b408ea3a5fbd647b26d6efe89ddad58f90be7528aa2b5b6ed20d6d41c928ac",
23
- "operatorImageDigest": "sha256:05b03560594da28e436977b82a905aa9a7702668b5e4ac4daea8b54376a75813",
24
- "sourceRevision": "90c5756fa313331df7be656fcbc1bce5a4164d6e"
22
+ "imageDigest": "sha256:d01c7513cb26858b23c279ab0cfe39a28bb73691e69e566be29460ed385d42ca",
23
+ "operatorImageDigest": "sha256:bd7162734324f40daa85861c314771d7437cbde258f0b45ae7d6a878810e5fdf",
24
+ "sourceRevision": "77049d904cf405b3f5a33ef85b622d91c9250008"
25
25
  },
26
26
  "files": [
27
27
  "dist",