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.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /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
+ }