@or3/cloud 0.1.72 → 0.1.75
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 +81 -62
- 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.75";
|
|
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(
|
|
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()
|
|
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) {
|
|
@@ -2754,7 +2763,7 @@ Options:
|
|
|
2754
2763
|
--verification-password-file <path> Read the current owner password for this verification
|
|
2755
2764
|
--port <port> Local OR3 port (default: 3000)
|
|
2756
2765
|
--keep <n> Backups to retain when pruning (default: 5)
|
|
2757
|
-
--force
|
|
2766
|
+
--force Bypass suspect-entry deferral; protected recovery backups remain protected
|
|
2758
2767
|
--tail <n> Log lines to show (default: 200)
|
|
2759
2768
|
--public Require verification through the public HTTPS origin
|
|
2760
2769
|
--read-only Verify without the lease, login, storage, or database writes
|
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
4715
|
-
|
|
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: {
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
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/
|
|
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.
|
|
3
|
+
"version": "0.1.75",
|
|
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:daa27fb6f4d3ee7b2fbdd0ab7727d114f3ad042e8330174338d44d720f872ece",
|
|
23
|
+
"operatorImageDigest": "sha256:cc5c0873085a6efae51252cd96046c526b976584cf69c9829403647433cf9710",
|
|
24
|
+
"sourceRevision": "988310e28ebbbb06795195e159fe8257feb8e395"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|