sealkeep 0.9.0 → 0.11.0
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/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +223 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { acquireDurableTicketLock } from "./durable-ticket-lock.js";
|
|
5
|
+
import { canonicalPhrase } from "./mnemonic.js";
|
|
6
|
+
const STATE_VERSION = 2;
|
|
7
|
+
const PUBLICATION_POLICY_VERSION = 1;
|
|
8
|
+
const STATE_FILE = "index-publication-state.v2.json";
|
|
9
|
+
const STATE_CHECKSUM_DOMAIN = "sealkeep:index-publication-state:v2\0";
|
|
10
|
+
const KEY_DERIVATION_DOMAIN = "sealkeep:index-publication-state:key:v1\0";
|
|
11
|
+
export class IndexPublicationStateError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
cause;
|
|
14
|
+
constructor(code, message, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "IndexPublicationStateError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.cause = cause;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class IndexPublicationConflictError extends Error {
|
|
22
|
+
code = "INDEX_PUBLICATION_STATE_CONFLICT";
|
|
23
|
+
constructor(message = "The index publication requirement changed") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "IndexPublicationConflictError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function indexPublicationStatePath(dataDir) {
|
|
29
|
+
return join(dataDir, "index", STATE_FILE);
|
|
30
|
+
}
|
|
31
|
+
function lockQueuePath(dataDir) {
|
|
32
|
+
return join(dataDir, "index", ".index-publication-state.lock");
|
|
33
|
+
}
|
|
34
|
+
function isObject(value) {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function nonEmpty(value) {
|
|
38
|
+
return typeof value === "string" && value.length > 0;
|
|
39
|
+
}
|
|
40
|
+
function validScope(value) {
|
|
41
|
+
if (!isObject(value))
|
|
42
|
+
return false;
|
|
43
|
+
if (value.kind === "account")
|
|
44
|
+
return nonEmpty(value.vaultId) && Object.keys(value).length === 2;
|
|
45
|
+
return value.kind === "team"
|
|
46
|
+
&& nonEmpty(value.spaceKey)
|
|
47
|
+
&& Number.isSafeInteger(value.membershipVersion)
|
|
48
|
+
&& Number(value.membershipVersion) > 0
|
|
49
|
+
&& Object.keys(value).length === 3;
|
|
50
|
+
}
|
|
51
|
+
function validRequirement(value) {
|
|
52
|
+
return isObject(value)
|
|
53
|
+
&& nonEmpty(value.archiveId)
|
|
54
|
+
&& nonEmpty(value.archiveRef)
|
|
55
|
+
&& nonEmpty(value.entryDigest)
|
|
56
|
+
&& validScope(value.scope)
|
|
57
|
+
&& Object.keys(value).length === 4;
|
|
58
|
+
}
|
|
59
|
+
function validReceipt(value) {
|
|
60
|
+
return isObject(value)
|
|
61
|
+
&& nonEmpty(value.requirementKey)
|
|
62
|
+
&& value.policyVersion === PUBLICATION_POLICY_VERSION
|
|
63
|
+
&& nonEmpty(value.publisherId)
|
|
64
|
+
&& nonEmpty(value.generationId)
|
|
65
|
+
&& nonEmpty(value.registryReceiptId)
|
|
66
|
+
&& Number.isSafeInteger(value.registryRevision)
|
|
67
|
+
&& Number(value.registryRevision) >= 0
|
|
68
|
+
&& nonEmpty(value.objectRef)
|
|
69
|
+
&& typeof value.ciphertextSha256 === "string"
|
|
70
|
+
&& /^[0-9a-f]{64}$/.test(value.ciphertextSha256)
|
|
71
|
+
&& Number.isSafeInteger(value.ciphertextBytes)
|
|
72
|
+
&& Number(value.ciphertextBytes) >= 0
|
|
73
|
+
&& nonEmpty(value.committedAt)
|
|
74
|
+
&& Object.keys(value).length === 10;
|
|
75
|
+
}
|
|
76
|
+
function validStoredRequirement(value, key) {
|
|
77
|
+
return isObject(value)
|
|
78
|
+
&& value.logicalKey === key
|
|
79
|
+
&& nonEmpty(value.logicalKey)
|
|
80
|
+
&& nonEmpty(value.requirementKey)
|
|
81
|
+
&& Number.isSafeInteger(value.requirementRevision)
|
|
82
|
+
&& Number(value.requirementRevision) > 0
|
|
83
|
+
&& nonEmpty(value.requiredAt)
|
|
84
|
+
&& validRequirement(value.requirement)
|
|
85
|
+
&& Object.keys(value).length === 5;
|
|
86
|
+
}
|
|
87
|
+
function canonicalJson(value) {
|
|
88
|
+
if (value === null || typeof value !== "object")
|
|
89
|
+
return JSON.stringify(value) ?? "null";
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
92
|
+
const record = value;
|
|
93
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
94
|
+
}
|
|
95
|
+
function checksum(ledger) {
|
|
96
|
+
// Construct the wire payload explicitly. `Omit<..., "checksum">` is a
|
|
97
|
+
// compile-time property only; a spread of a parsed ledger still carries its
|
|
98
|
+
// old checksum at runtime and must never feed it into the next checksum.
|
|
99
|
+
const payload = {
|
|
100
|
+
version: STATE_VERSION,
|
|
101
|
+
generation: ledger.generation,
|
|
102
|
+
revision: ledger.revision,
|
|
103
|
+
requirements: ledger.requirements,
|
|
104
|
+
receipts: ledger.receipts,
|
|
105
|
+
};
|
|
106
|
+
return createHash("sha256")
|
|
107
|
+
.update(STATE_CHECKSUM_DOMAIN, "utf8")
|
|
108
|
+
.update(canonicalJson(payload), "utf8")
|
|
109
|
+
.digest("hex");
|
|
110
|
+
}
|
|
111
|
+
function equalText(left, right) {
|
|
112
|
+
const a = Buffer.from(left, "utf8");
|
|
113
|
+
const b = Buffer.from(right, "utf8");
|
|
114
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
115
|
+
}
|
|
116
|
+
function parseLedger(raw) {
|
|
117
|
+
let value;
|
|
118
|
+
try {
|
|
119
|
+
value = JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!isObject(value)
|
|
125
|
+
|| value.version !== STATE_VERSION
|
|
126
|
+
|| !nonEmpty(value.generation)
|
|
127
|
+
|| !Number.isSafeInteger(value.revision)
|
|
128
|
+
|| Number(value.revision) < 0
|
|
129
|
+
|| !isObject(value.requirements)
|
|
130
|
+
|| !isObject(value.receipts)
|
|
131
|
+
|| typeof value.checksum !== "string")
|
|
132
|
+
return null;
|
|
133
|
+
const requirements = value.requirements;
|
|
134
|
+
const receipts = value.receipts;
|
|
135
|
+
if (Object.entries(requirements).some(([key, requirement]) => !validStoredRequirement(requirement, key)))
|
|
136
|
+
return null;
|
|
137
|
+
if (Object.entries(receipts).some(([logicalKey, receipt]) => {
|
|
138
|
+
if (!validReceipt(receipt))
|
|
139
|
+
return true;
|
|
140
|
+
const requirement = requirements[logicalKey];
|
|
141
|
+
return !requirement || receipt.requirementKey !== requirement.requirementKey;
|
|
142
|
+
}))
|
|
143
|
+
return null;
|
|
144
|
+
const withoutChecksum = {
|
|
145
|
+
version: STATE_VERSION,
|
|
146
|
+
generation: value.generation,
|
|
147
|
+
revision: Number(value.revision),
|
|
148
|
+
requirements: requirements,
|
|
149
|
+
receipts: receipts,
|
|
150
|
+
};
|
|
151
|
+
const expected = checksum(withoutChecksum);
|
|
152
|
+
if (!/^[0-9a-f]{64}$/.test(value.checksum) || !equalText(value.checksum, expected))
|
|
153
|
+
return null;
|
|
154
|
+
return { ...withoutChecksum, checksum: value.checksum };
|
|
155
|
+
}
|
|
156
|
+
function errorCode(error) {
|
|
157
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
158
|
+
? String(error.code)
|
|
159
|
+
: undefined;
|
|
160
|
+
}
|
|
161
|
+
async function readLedger(dataDir, options = {}) {
|
|
162
|
+
const read = options.readFile ?? ((path, encoding) => readFile(path, encoding));
|
|
163
|
+
try {
|
|
164
|
+
const ledger = parseLedger(await read(indexPublicationStatePath(dataDir), "utf8"));
|
|
165
|
+
return ledger
|
|
166
|
+
? { status: "ok", ledger }
|
|
167
|
+
: { status: "unreadable", errorCode: "CORRUPT" };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const code = errorCode(error);
|
|
171
|
+
if (code === "ENOENT")
|
|
172
|
+
return { status: "missing" };
|
|
173
|
+
return { status: "unreadable", errorCode: code ?? "UNKNOWN", cause: error };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function keySeed(material) {
|
|
177
|
+
if (typeof material === "string") {
|
|
178
|
+
if (!material.length)
|
|
179
|
+
throw new TypeError("Publication phrase must not be empty");
|
|
180
|
+
return Buffer.from(canonicalPhrase(material), "utf8");
|
|
181
|
+
}
|
|
182
|
+
if ("phrase" in material) {
|
|
183
|
+
if (!material.phrase.length)
|
|
184
|
+
throw new TypeError("Publication phrase must not be empty");
|
|
185
|
+
return Buffer.from(canonicalPhrase(material.phrase), "utf8");
|
|
186
|
+
}
|
|
187
|
+
if ("vaultId" in material) {
|
|
188
|
+
if (!material.vaultId.length)
|
|
189
|
+
throw new TypeError("Publication vault id must not be empty");
|
|
190
|
+
// The ledger stores publication coordination metadata, not transcript
|
|
191
|
+
// plaintext or archive keys. Keying its opaque identities by the stable
|
|
192
|
+
// vault id lets both the indexer and the deletion gate address the same
|
|
193
|
+
// obligations without asking a background retention pass to unlock the
|
|
194
|
+
// user's recovery phrase.
|
|
195
|
+
return createHash("sha256")
|
|
196
|
+
.update("sealkeep:index-publication-state:vault-id:v1\0", "utf8")
|
|
197
|
+
.update(material.vaultId, "utf8")
|
|
198
|
+
.digest();
|
|
199
|
+
}
|
|
200
|
+
if (!(material.key instanceof Uint8Array) || material.key.byteLength === 0) {
|
|
201
|
+
throw new TypeError("Publication key material must not be empty");
|
|
202
|
+
}
|
|
203
|
+
// Always copy caller-owned bytes. Derivation zeroizes only this private copy.
|
|
204
|
+
return Buffer.from(material.key);
|
|
205
|
+
}
|
|
206
|
+
function withDerivedKey(material, work) {
|
|
207
|
+
const seed = keySeed(material);
|
|
208
|
+
let key;
|
|
209
|
+
try {
|
|
210
|
+
key = createHmac("sha256", seed).update(KEY_DERIVATION_DOMAIN, "utf8").digest();
|
|
211
|
+
return work(key);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
key?.fill(0);
|
|
215
|
+
seed.fill(0);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function exactScope(scope) {
|
|
219
|
+
return scope.kind === "account"
|
|
220
|
+
? ["account", scope.vaultId]
|
|
221
|
+
: ["team", scope.spaceKey, scope.membershipVersion];
|
|
222
|
+
}
|
|
223
|
+
function logicalScope(scope) {
|
|
224
|
+
return scope.kind === "account"
|
|
225
|
+
? ["account", scope.vaultId]
|
|
226
|
+
: ["team", scope.spaceKey];
|
|
227
|
+
}
|
|
228
|
+
function hmacKey(key, purpose, values) {
|
|
229
|
+
return `${purpose}1:${createHmac("sha256", key).update(canonicalJson(values), "utf8").digest("hex")}`;
|
|
230
|
+
}
|
|
231
|
+
function identitiesWithKey(key, requirement) {
|
|
232
|
+
if (!validRequirement(requirement))
|
|
233
|
+
throw new TypeError("Invalid index publication requirement");
|
|
234
|
+
return {
|
|
235
|
+
// membershipVersion is deliberately absent: a roster change replaces the
|
|
236
|
+
// same logical team/archive slot instead of creating a parallel obligation.
|
|
237
|
+
logicalKey: hmacKey(key, "l", [PUBLICATION_POLICY_VERSION, logicalScope(requirement.scope), requirement.archiveId]),
|
|
238
|
+
requirementKey: hmacKey(key, "r", [
|
|
239
|
+
PUBLICATION_POLICY_VERSION,
|
|
240
|
+
exactScope(requirement.scope),
|
|
241
|
+
requirement.archiveRef,
|
|
242
|
+
requirement.entryDigest,
|
|
243
|
+
]),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function identities(material, requirement) {
|
|
247
|
+
return withDerivedKey(material, (key) => identitiesWithKey(key, requirement));
|
|
248
|
+
}
|
|
249
|
+
export function indexPublicationRequirementKey(material, requirement) {
|
|
250
|
+
return identities(material, requirement).requirementKey;
|
|
251
|
+
}
|
|
252
|
+
export function indexPublicationLogicalKey(material, requirement) {
|
|
253
|
+
return identities(material, requirement).logicalKey;
|
|
254
|
+
}
|
|
255
|
+
function exactRequirement(left, right) {
|
|
256
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
257
|
+
}
|
|
258
|
+
function receiptSatisfies(requirement, receipt) {
|
|
259
|
+
return Boolean(receipt
|
|
260
|
+
&& receipt.policyVersion === PUBLICATION_POLICY_VERSION
|
|
261
|
+
&& equalText(receipt.requirementKey, requirement.requirementKey));
|
|
262
|
+
}
|
|
263
|
+
function handleFor(ledger, requirement) {
|
|
264
|
+
return {
|
|
265
|
+
logicalKey: requirement.logicalKey,
|
|
266
|
+
requirementKey: requirement.requirementKey,
|
|
267
|
+
ledgerGeneration: ledger.generation,
|
|
268
|
+
requirementRevision: requirement.requirementRevision,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function matchesHandle(ledger, requirement, expected) {
|
|
272
|
+
return Boolean(requirement
|
|
273
|
+
&& ledger.generation === expected.ledgerGeneration
|
|
274
|
+
&& requirement.logicalKey === expected.logicalKey
|
|
275
|
+
&& requirement.requirementKey === expected.requirementKey
|
|
276
|
+
&& requirement.requirementRevision === expected.requirementRevision);
|
|
277
|
+
}
|
|
278
|
+
function validateLedgerKeys(ledger, material) {
|
|
279
|
+
return withDerivedKey(material, (key) => Object.entries(ledger.requirements).every(([logicalKey, stored]) => {
|
|
280
|
+
const expected = identitiesWithKey(key, stored.requirement);
|
|
281
|
+
return equalText(logicalKey, expected.logicalKey)
|
|
282
|
+
&& equalText(stored.logicalKey, expected.logicalKey)
|
|
283
|
+
&& equalText(stored.requirementKey, expected.requirementKey);
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
function unreadableError(read) {
|
|
287
|
+
const corrupt = read.errorCode === "CORRUPT";
|
|
288
|
+
return new IndexPublicationStateError(corrupt ? "INDEX_PUBLICATION_STATE_CORRUPT" : "INDEX_PUBLICATION_STATE_UNREADABLE", corrupt
|
|
289
|
+
? "Index publication state is corrupt"
|
|
290
|
+
: `Index publication state could not be read (${read.errorCode})`, read.cause);
|
|
291
|
+
}
|
|
292
|
+
async function syncDirectory(path) {
|
|
293
|
+
let handle;
|
|
294
|
+
try {
|
|
295
|
+
handle = await open(path, "r");
|
|
296
|
+
await handle.sync();
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (process.platform !== "win32")
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await handle?.close();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function writeLedger(dataDir, ledger, uuid) {
|
|
307
|
+
const target = indexPublicationStatePath(dataDir);
|
|
308
|
+
const directory = dirname(target);
|
|
309
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
310
|
+
const complete = { ...ledger, checksum: checksum(ledger) };
|
|
311
|
+
const temporary = join(directory, `.${STATE_FILE}.${uuid()}.tmp`);
|
|
312
|
+
let handle;
|
|
313
|
+
try {
|
|
314
|
+
handle = await open(temporary, "wx", 0o600);
|
|
315
|
+
await handle.writeFile(`${JSON.stringify(complete)}\n`, "utf8");
|
|
316
|
+
await handle.sync();
|
|
317
|
+
await handle.close();
|
|
318
|
+
handle = undefined;
|
|
319
|
+
await rename(temporary, target);
|
|
320
|
+
await syncDirectory(directory);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
await handle?.close().catch(() => undefined);
|
|
324
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function emptyLedger(uuid) {
|
|
328
|
+
const withoutChecksum = {
|
|
329
|
+
version: STATE_VERSION,
|
|
330
|
+
generation: uuid(),
|
|
331
|
+
revision: 0,
|
|
332
|
+
requirements: Object.create(null),
|
|
333
|
+
receipts: Object.create(null),
|
|
334
|
+
};
|
|
335
|
+
return { ...withoutChecksum, checksum: checksum(withoutChecksum) };
|
|
336
|
+
}
|
|
337
|
+
async function withMutation(dataDir, material, options, mutate, initializeMissing = false) {
|
|
338
|
+
const lockOptions = {
|
|
339
|
+
signal: options.signal,
|
|
340
|
+
...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
|
|
341
|
+
...(options.lockPollMs === undefined ? {} : { pollMs: options.lockPollMs }),
|
|
342
|
+
};
|
|
343
|
+
const lock = await acquireDurableTicketLock(lockQueuePath(dataDir), indexPublicationStatePath(dataDir), lockOptions);
|
|
344
|
+
try {
|
|
345
|
+
const read = await readLedger(dataDir, options);
|
|
346
|
+
if (read.status === "unreadable")
|
|
347
|
+
throw unreadableError(read);
|
|
348
|
+
const uuid = options.uuid ?? randomUUID;
|
|
349
|
+
const ledger = read.status === "missing" ? emptyLedger(uuid) : read.ledger;
|
|
350
|
+
if (!validateLedgerKeys(ledger, material)) {
|
|
351
|
+
throw new IndexPublicationStateError("INDEX_PUBLICATION_STATE_CORRUPT", "Index publication state contains invalid keyed requirements");
|
|
352
|
+
}
|
|
353
|
+
const outcome = mutate(ledger, (options.now ?? (() => new Date().toISOString()))());
|
|
354
|
+
if (outcome.changed || (initializeMissing && read.status === "missing")) {
|
|
355
|
+
await lock.assertOwned();
|
|
356
|
+
await writeLedger(dataDir, outcome.ledger, uuid);
|
|
357
|
+
}
|
|
358
|
+
return outcome.value;
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await lock.release();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export async function readIndexPublicationState(dataDir, options = {}) {
|
|
365
|
+
const read = await readLedger(dataDir, options);
|
|
366
|
+
if (read.status !== "ok")
|
|
367
|
+
return read.status === "missing"
|
|
368
|
+
? { status: "missing" }
|
|
369
|
+
: { status: "unreadable", errorCode: read.errorCode };
|
|
370
|
+
return {
|
|
371
|
+
status: "ok",
|
|
372
|
+
generation: read.ledger.generation,
|
|
373
|
+
revision: read.ledger.revision,
|
|
374
|
+
requirements: Object.keys(read.ledger.requirements).length,
|
|
375
|
+
satisfied: Object.entries(read.ledger.requirements)
|
|
376
|
+
.filter(([logicalKey, requirement]) => receiptSatisfies(requirement, read.ledger.receipts[logicalKey]))
|
|
377
|
+
.length,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Installs or refreshes one exact obligation. Repeating the same tuple is a
|
|
382
|
+
* true no-op; changing a tuple removes only that logical archive's receipt.
|
|
383
|
+
*/
|
|
384
|
+
export async function recordIndexPublicationRequirement(dataDir, material, requirement, options = {}) {
|
|
385
|
+
const identity = identities(material, requirement);
|
|
386
|
+
return withMutation(dataDir, material, options, (ledger, now) => {
|
|
387
|
+
const current = ledger.requirements[identity.logicalKey];
|
|
388
|
+
if (options.expected === null && current)
|
|
389
|
+
throw new IndexPublicationConflictError();
|
|
390
|
+
if (options.expected && !matchesHandle(ledger, current, options.expected)) {
|
|
391
|
+
throw new IndexPublicationConflictError();
|
|
392
|
+
}
|
|
393
|
+
if (current
|
|
394
|
+
&& current.requirementKey === identity.requirementKey
|
|
395
|
+
&& exactRequirement(current.requirement, requirement)) {
|
|
396
|
+
return {
|
|
397
|
+
changed: false,
|
|
398
|
+
ledger,
|
|
399
|
+
value: handleFor(ledger, current),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const revision = ledger.revision + 1;
|
|
403
|
+
const stored = {
|
|
404
|
+
...identity,
|
|
405
|
+
requirementRevision: revision,
|
|
406
|
+
requiredAt: now,
|
|
407
|
+
requirement: structuredClone(requirement),
|
|
408
|
+
};
|
|
409
|
+
const requirements = { ...ledger.requirements, [identity.logicalKey]: stored };
|
|
410
|
+
const receipts = { ...ledger.receipts };
|
|
411
|
+
delete receipts[identity.logicalKey];
|
|
412
|
+
const next = { ...ledger, revision, requirements, receipts };
|
|
413
|
+
return {
|
|
414
|
+
changed: true,
|
|
415
|
+
ledger: next,
|
|
416
|
+
value: {
|
|
417
|
+
logicalKey: identity.logicalKey,
|
|
418
|
+
requirementKey: identity.requirementKey,
|
|
419
|
+
ledgerGeneration: ledger.generation,
|
|
420
|
+
requirementRevision: revision,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Atomically installs a checkpoint's obligations with one lock acquisition and
|
|
427
|
+
* one fsync. Large vaults routinely contain thousands of sessions, so making
|
|
428
|
+
* each row its own durable transaction would turn indexing into minutes of
|
|
429
|
+
* avoidable disk latency. Duplicate logical slots are rejected because their
|
|
430
|
+
* order would otherwise decide which exact tuple survived.
|
|
431
|
+
*/
|
|
432
|
+
export async function recordIndexPublicationRequirements(dataDir, material, requirements, options = {}) {
|
|
433
|
+
const prepared = requirements.map((requirement) => ({
|
|
434
|
+
requirement,
|
|
435
|
+
identity: identities(material, requirement),
|
|
436
|
+
}));
|
|
437
|
+
if (new Set(prepared.map((item) => item.identity.logicalKey)).size !== prepared.length) {
|
|
438
|
+
throw new TypeError("Duplicate logical index publication requirement");
|
|
439
|
+
}
|
|
440
|
+
return withMutation(dataDir, material, options, (ledger, now) => {
|
|
441
|
+
let revision = ledger.revision;
|
|
442
|
+
let changed = false;
|
|
443
|
+
const storedByLogical = { ...ledger.requirements };
|
|
444
|
+
const receipts = { ...ledger.receipts };
|
|
445
|
+
const result = [];
|
|
446
|
+
for (const item of prepared) {
|
|
447
|
+
const current = storedByLogical[item.identity.logicalKey];
|
|
448
|
+
if (current
|
|
449
|
+
&& current.requirementKey === item.identity.requirementKey
|
|
450
|
+
&& exactRequirement(current.requirement, item.requirement)) {
|
|
451
|
+
result.push({ requirement: structuredClone(item.requirement), handle: handleFor(ledger, current) });
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
revision += 1;
|
|
455
|
+
changed = true;
|
|
456
|
+
const stored = {
|
|
457
|
+
...item.identity,
|
|
458
|
+
requirementRevision: revision,
|
|
459
|
+
requiredAt: now,
|
|
460
|
+
requirement: structuredClone(item.requirement),
|
|
461
|
+
};
|
|
462
|
+
storedByLogical[item.identity.logicalKey] = stored;
|
|
463
|
+
delete receipts[item.identity.logicalKey];
|
|
464
|
+
result.push({
|
|
465
|
+
requirement: structuredClone(item.requirement),
|
|
466
|
+
handle: {
|
|
467
|
+
logicalKey: item.identity.logicalKey,
|
|
468
|
+
requirementKey: item.identity.requirementKey,
|
|
469
|
+
ledgerGeneration: ledger.generation,
|
|
470
|
+
requirementRevision: revision,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
changed,
|
|
476
|
+
ledger: changed
|
|
477
|
+
? { ...ledger, revision, requirements: storedByLogical, receipts }
|
|
478
|
+
: ledger,
|
|
479
|
+
value: result,
|
|
480
|
+
};
|
|
481
|
+
}, true); // Explicit registration of zero obligations must survive an empty-vault build.
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Records positive publication proof only while the exact requirement and its
|
|
485
|
+
* local generation/revision still match the writer's pre-publication handle.
|
|
486
|
+
*/
|
|
487
|
+
export async function recordIndexPublicationReceipt(dataDir, material, requirement, expected, details, options = {}) {
|
|
488
|
+
const identity = identities(material, requirement);
|
|
489
|
+
if (details.requirementKey !== undefined && details.requirementKey !== identity.requirementKey) {
|
|
490
|
+
throw new TypeError("Publication receipt requirement key does not match the requirement");
|
|
491
|
+
}
|
|
492
|
+
if (details.policyVersion !== undefined && details.policyVersion !== PUBLICATION_POLICY_VERSION) {
|
|
493
|
+
throw new TypeError("Publication receipt policy version does not match");
|
|
494
|
+
}
|
|
495
|
+
const receipt = {
|
|
496
|
+
requirementKey: identity.requirementKey,
|
|
497
|
+
policyVersion: PUBLICATION_POLICY_VERSION,
|
|
498
|
+
publisherId: details.publisherId,
|
|
499
|
+
generationId: details.generationId,
|
|
500
|
+
registryReceiptId: details.registryReceiptId,
|
|
501
|
+
registryRevision: details.registryRevision,
|
|
502
|
+
objectRef: details.objectRef,
|
|
503
|
+
ciphertextSha256: details.ciphertextSha256,
|
|
504
|
+
ciphertextBytes: details.ciphertextBytes,
|
|
505
|
+
committedAt: details.committedAt,
|
|
506
|
+
};
|
|
507
|
+
if (!validReceipt(receipt))
|
|
508
|
+
throw new TypeError("Invalid index publication receipt");
|
|
509
|
+
return withMutation(dataDir, material, options, (ledger) => {
|
|
510
|
+
const current = ledger.requirements[identity.logicalKey];
|
|
511
|
+
if (!current
|
|
512
|
+
|| !matchesHandle(ledger, current, expected)
|
|
513
|
+
|| current.requirementKey !== identity.requirementKey
|
|
514
|
+
|| !exactRequirement(current.requirement, requirement)) {
|
|
515
|
+
return { changed: false, ledger, value: false };
|
|
516
|
+
}
|
|
517
|
+
const previous = ledger.receipts[identity.logicalKey];
|
|
518
|
+
if (previous && canonicalJson(previous) === canonicalJson(receipt)) {
|
|
519
|
+
return { changed: false, ledger, value: true };
|
|
520
|
+
}
|
|
521
|
+
const next = {
|
|
522
|
+
...ledger,
|
|
523
|
+
revision: ledger.revision + 1,
|
|
524
|
+
receipts: { ...ledger.receipts, [identity.logicalKey]: receipt },
|
|
525
|
+
};
|
|
526
|
+
return { changed: true, ledger: next, value: true };
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/** Installs all still-current receipts from one verified remote generation. */
|
|
530
|
+
export async function recordIndexPublicationReceipts(dataDir, material, writes, options = {}) {
|
|
531
|
+
const prepared = writes.map((write) => {
|
|
532
|
+
const identity = identities(material, write.requirement);
|
|
533
|
+
if (write.details.requirementKey !== undefined && write.details.requirementKey !== identity.requirementKey) {
|
|
534
|
+
throw new TypeError("Publication receipt requirement key does not match the requirement");
|
|
535
|
+
}
|
|
536
|
+
if (write.details.policyVersion !== undefined && write.details.policyVersion !== PUBLICATION_POLICY_VERSION) {
|
|
537
|
+
throw new TypeError("Publication receipt policy version does not match");
|
|
538
|
+
}
|
|
539
|
+
const receipt = {
|
|
540
|
+
requirementKey: identity.requirementKey,
|
|
541
|
+
policyVersion: PUBLICATION_POLICY_VERSION,
|
|
542
|
+
publisherId: write.details.publisherId,
|
|
543
|
+
generationId: write.details.generationId,
|
|
544
|
+
registryReceiptId: write.details.registryReceiptId,
|
|
545
|
+
registryRevision: write.details.registryRevision,
|
|
546
|
+
objectRef: write.details.objectRef,
|
|
547
|
+
ciphertextSha256: write.details.ciphertextSha256,
|
|
548
|
+
ciphertextBytes: write.details.ciphertextBytes,
|
|
549
|
+
committedAt: write.details.committedAt,
|
|
550
|
+
};
|
|
551
|
+
if (!validReceipt(receipt))
|
|
552
|
+
throw new TypeError("Invalid index publication receipt");
|
|
553
|
+
return { ...write, identity, receipt };
|
|
554
|
+
});
|
|
555
|
+
if (new Set(prepared.map((item) => item.identity.logicalKey)).size !== prepared.length) {
|
|
556
|
+
throw new TypeError("Duplicate logical index publication receipt");
|
|
557
|
+
}
|
|
558
|
+
return withMutation(dataDir, material, options, (ledger) => {
|
|
559
|
+
let recorded = 0;
|
|
560
|
+
let stale = 0;
|
|
561
|
+
let changed = false;
|
|
562
|
+
const receipts = { ...ledger.receipts };
|
|
563
|
+
for (const item of prepared) {
|
|
564
|
+
const current = ledger.requirements[item.identity.logicalKey];
|
|
565
|
+
if (!current
|
|
566
|
+
|| !matchesHandle(ledger, current, item.handle)
|
|
567
|
+
|| current.requirementKey !== item.identity.requirementKey
|
|
568
|
+
|| !exactRequirement(current.requirement, item.requirement)) {
|
|
569
|
+
stale += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
recorded += 1;
|
|
573
|
+
if (receipts[item.identity.logicalKey]
|
|
574
|
+
&& canonicalJson(receipts[item.identity.logicalKey]) === canonicalJson(item.receipt))
|
|
575
|
+
continue;
|
|
576
|
+
receipts[item.identity.logicalKey] = item.receipt;
|
|
577
|
+
changed = true;
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
changed,
|
|
581
|
+
ledger: changed ? { ...ledger, revision: ledger.revision + 1, receipts } : ledger,
|
|
582
|
+
value: { recorded, stale },
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/** Missing, corrupt and unreadable state all deliberately return no proof. */
|
|
587
|
+
export async function getIndexPublicationReceipt(dataDir, material, requirement, options = {}) {
|
|
588
|
+
const identity = identities(material, requirement);
|
|
589
|
+
const read = await readLedger(dataDir, options);
|
|
590
|
+
if (read.status !== "ok" || !validateLedgerKeys(read.ledger, material))
|
|
591
|
+
return null;
|
|
592
|
+
const stored = read.ledger.requirements[identity.logicalKey];
|
|
593
|
+
const receipt = read.ledger.receipts[identity.logicalKey];
|
|
594
|
+
if (!stored
|
|
595
|
+
|| stored.requirementKey !== identity.requirementKey
|
|
596
|
+
|| !exactRequirement(stored.requirement, requirement)
|
|
597
|
+
|| !receiptSatisfies(stored, receipt))
|
|
598
|
+
return null;
|
|
599
|
+
return structuredClone(receipt);
|
|
600
|
+
}
|
|
601
|
+
export async function hasIndexPublicationReceipt(dataDir, material, requirement, options = {}) {
|
|
602
|
+
return (await getIndexPublicationReceipt(dataDir, material, requirement, options)) !== null;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Lists only durable obligations which lack their exact positive receipt.
|
|
606
|
+
* Missing state has no recorded requirements. Corrupt/EACCES state throws so
|
|
607
|
+
* callers cannot mistake an I/O fault for an authoritative empty list.
|
|
608
|
+
*/
|
|
609
|
+
export async function listUnsatisfiedIndexPublicationRequirements(dataDir, material, options = {}) {
|
|
610
|
+
const read = await readLedger(dataDir, options);
|
|
611
|
+
if (read.status === "missing")
|
|
612
|
+
return [];
|
|
613
|
+
if (read.status === "unreadable")
|
|
614
|
+
throw unreadableError(read);
|
|
615
|
+
if (!validateLedgerKeys(read.ledger, material)) {
|
|
616
|
+
throw new IndexPublicationStateError("INDEX_PUBLICATION_STATE_CORRUPT", "Index publication state contains invalid keyed requirements");
|
|
617
|
+
}
|
|
618
|
+
return Object.entries(read.ledger.requirements)
|
|
619
|
+
.filter(([logicalKey, requirement]) => !receiptSatisfies(requirement, read.ledger.receipts[logicalKey]))
|
|
620
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
621
|
+
.map(([, stored]) => ({
|
|
622
|
+
...handleFor(read.ledger, stored),
|
|
623
|
+
requirement: structuredClone(stored.requirement),
|
|
624
|
+
requiredAt: stored.requiredAt,
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Lists every exact obligation together with its positive receipt, if any.
|
|
629
|
+
* Missing state is an empty non-authoritative list; corrupt or unreadable state
|
|
630
|
+
* throws so destructive callers cannot interpret an I/O failure as success.
|
|
631
|
+
*/
|
|
632
|
+
export async function listIndexPublicationRequirements(dataDir, material, options = {}) {
|
|
633
|
+
const read = await readLedger(dataDir, options);
|
|
634
|
+
if (read.status === "missing")
|
|
635
|
+
return [];
|
|
636
|
+
if (read.status === "unreadable")
|
|
637
|
+
throw unreadableError(read);
|
|
638
|
+
if (!validateLedgerKeys(read.ledger, material)) {
|
|
639
|
+
throw new IndexPublicationStateError("INDEX_PUBLICATION_STATE_CORRUPT", "Index publication state contains invalid keyed requirements");
|
|
640
|
+
}
|
|
641
|
+
return Object.entries(read.ledger.requirements)
|
|
642
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
643
|
+
.map(([logicalKey, stored]) => {
|
|
644
|
+
const receipt = read.ledger.receipts[logicalKey];
|
|
645
|
+
return {
|
|
646
|
+
...handleFor(read.ledger, stored),
|
|
647
|
+
requirement: structuredClone(stored.requirement),
|
|
648
|
+
requiredAt: stored.requiredAt,
|
|
649
|
+
receipt: receiptSatisfies(stored, receipt) ? structuredClone(receipt) : null,
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/** Invalidates one exact receipt; there is intentionally no global clear. */
|
|
654
|
+
export async function invalidateIndexPublicationReceipt(dataDir, material, requirement, expected, options = {}) {
|
|
655
|
+
const identity = identities(material, requirement);
|
|
656
|
+
return withMutation(dataDir, material, options, (ledger) => {
|
|
657
|
+
const current = ledger.requirements[identity.logicalKey];
|
|
658
|
+
if (!current
|
|
659
|
+
|| !matchesHandle(ledger, current, expected)
|
|
660
|
+
|| current.requirementKey !== identity.requirementKey
|
|
661
|
+
|| !exactRequirement(current.requirement, requirement)) {
|
|
662
|
+
return { changed: false, ledger, value: false };
|
|
663
|
+
}
|
|
664
|
+
if (!ledger.receipts[identity.logicalKey])
|
|
665
|
+
return { changed: false, ledger, value: true };
|
|
666
|
+
const receipts = { ...ledger.receipts };
|
|
667
|
+
delete receipts[identity.logicalKey];
|
|
668
|
+
return {
|
|
669
|
+
changed: true,
|
|
670
|
+
ledger: { ...ledger, revision: ledger.revision + 1, receipts },
|
|
671
|
+
value: true,
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
/** Removes one no-longer-applicable obligation using the same per-entry CAS. */
|
|
676
|
+
export async function removeIndexPublicationRequirement(dataDir, material, requirement, expected, options = {}) {
|
|
677
|
+
const identity = identities(material, requirement);
|
|
678
|
+
return withMutation(dataDir, material, options, (ledger) => {
|
|
679
|
+
const current = ledger.requirements[identity.logicalKey];
|
|
680
|
+
if (!current
|
|
681
|
+
|| !matchesHandle(ledger, current, expected)
|
|
682
|
+
|| current.requirementKey !== identity.requirementKey
|
|
683
|
+
|| !exactRequirement(current.requirement, requirement)) {
|
|
684
|
+
return { changed: false, ledger, value: false };
|
|
685
|
+
}
|
|
686
|
+
const requirements = { ...ledger.requirements };
|
|
687
|
+
const receipts = { ...ledger.receipts };
|
|
688
|
+
delete requirements[identity.logicalKey];
|
|
689
|
+
delete receipts[identity.logicalKey];
|
|
690
|
+
return {
|
|
691
|
+
changed: true,
|
|
692
|
+
ledger: { ...ledger, revision: ledger.revision + 1, requirements, receipts },
|
|
693
|
+
value: true,
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
}
|