sealkeep 0.8.1 → 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 (275) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +244 -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 +1746 -166
  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.d.ts +1 -0
  150. package/dist/src/migrate.js +33 -5
  151. package/dist/src/notify.d.ts +1 -1
  152. package/dist/src/notify.js +5 -5
  153. package/dist/src/offload.d.ts +201 -14
  154. package/dist/src/offload.js +1848 -140
  155. package/dist/src/onboarding.d.ts +8 -1
  156. package/dist/src/onboarding.js +4 -4
  157. package/dist/src/packages.d.ts +2 -2
  158. package/dist/src/packages.js +10 -2
  159. package/dist/src/passkey.d.ts +0 -1
  160. package/dist/src/passkey.js +2 -7
  161. package/dist/src/password-lock.d.ts +2 -2
  162. package/dist/src/password-lock.js +6 -6
  163. package/dist/src/paths.d.ts +2 -0
  164. package/dist/src/paths.js +2 -0
  165. package/dist/src/presence.d.ts +86 -0
  166. package/dist/src/presence.js +240 -0
  167. package/dist/src/progress-deadline.d.ts +21 -0
  168. package/dist/src/progress-deadline.js +91 -0
  169. package/dist/src/project-repair.d.ts +55 -0
  170. package/dist/src/project-repair.js +131 -0
  171. package/dist/src/providers/gcs.d.ts +28 -7
  172. package/dist/src/providers/gcs.js +35 -24
  173. package/dist/src/providers/gdrive.d.ts +71 -8
  174. package/dist/src/providers/gdrive.js +223 -50
  175. package/dist/src/providers/index.d.ts +11 -3
  176. package/dist/src/providers/index.js +30 -10
  177. package/dist/src/providers/s3.d.ts +30 -8
  178. package/dist/src/providers/s3.js +41 -30
  179. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  180. package/dist/src/providers/safe-storage-fetch.js +72 -0
  181. package/dist/src/queue.d.ts +136 -19
  182. package/dist/src/queue.js +862 -96
  183. package/dist/src/reclaim-transaction.d.ts +156 -0
  184. package/dist/src/reclaim-transaction.js +1027 -0
  185. package/dist/src/recovery-codes.d.ts +32 -0
  186. package/dist/src/recovery-codes.js +338 -0
  187. package/dist/src/recovery.js +12 -9
  188. package/dist/src/rehydrate.d.ts +25 -22
  189. package/dist/src/rehydrate.js +319 -23
  190. package/dist/src/restore.d.ts +57 -4
  191. package/dist/src/restore.js +272 -36
  192. package/dist/src/resume-stub.d.ts +92 -0
  193. package/dist/src/resume-stub.js +417 -0
  194. package/dist/src/retention.d.ts +98 -7
  195. package/dist/src/retention.js +1066 -62
  196. package/dist/src/rotate.js +3 -3
  197. package/dist/src/search.d.ts +566 -8
  198. package/dist/src/search.js +5897 -290
  199. package/dist/src/secrets.d.ts +51 -7
  200. package/dist/src/secrets.js +316 -24
  201. package/dist/src/service.d.ts +49 -11
  202. package/dist/src/service.js +776 -35
  203. package/dist/src/share.js +3 -3
  204. package/dist/src/shared-spaces.d.ts +98 -0
  205. package/dist/src/shared-spaces.js +214 -0
  206. package/dist/src/source-reader.d.ts +73 -0
  207. package/dist/src/source-reader.js +715 -0
  208. package/dist/src/spool.d.ts +1 -1
  209. package/dist/src/spool.js +1 -1
  210. package/dist/src/start-tui.js +2 -1
  211. package/dist/src/start.js +2 -2
  212. package/dist/src/storage-endpoint.d.ts +21 -0
  213. package/dist/src/storage-endpoint.js +122 -0
  214. package/dist/src/storage-setup.js +12 -12
  215. package/dist/src/storage-targets.d.ts +109 -6
  216. package/dist/src/storage-targets.js +975 -67
  217. package/dist/src/stream-to-cloud.d.ts +5 -1
  218. package/dist/src/stream-to-cloud.js +34 -14
  219. package/dist/src/sync-rules.d.ts +31 -6
  220. package/dist/src/sync-rules.js +153 -14
  221. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  222. package/dist/src/team-backfill-scheduling.js +33 -0
  223. package/dist/src/team-backfill.d.ts +116 -0
  224. package/dist/src/team-backfill.js +1429 -0
  225. package/dist/src/team-index-cache.d.ts +16 -0
  226. package/dist/src/team-index-cache.js +152 -0
  227. package/dist/src/team-offboarding.d.ts +38 -0
  228. package/dist/src/team-offboarding.js +1043 -0
  229. package/dist/src/team-presence.d.ts +127 -0
  230. package/dist/src/team-presence.js +904 -0
  231. package/dist/src/team-publication-policy.d.ts +20 -0
  232. package/dist/src/team-publication-policy.js +140 -0
  233. package/dist/src/team-realtime.d.ts +68 -0
  234. package/dist/src/team-realtime.js +816 -0
  235. package/dist/src/team-source-facts-cache.d.ts +23 -0
  236. package/dist/src/team-source-facts-cache.js +255 -0
  237. package/dist/src/trash.d.ts +1 -1
  238. package/dist/src/trash.js +2 -2
  239. package/dist/src/tui.js +11 -12
  240. package/dist/src/types.d.ts +173 -7
  241. package/dist/src/types.js +20 -0
  242. package/dist/src/ui-server.d.ts +163 -35
  243. package/dist/src/ui-server.js +712 -72
  244. package/dist/src/ui.d.ts +1 -2
  245. package/dist/src/ui.js +1 -2
  246. package/dist/src/upload.d.ts +27 -0
  247. package/dist/src/upload.js +383 -43
  248. package/dist/src/vault.d.ts +235 -29
  249. package/dist/src/vault.js +1783 -190
  250. package/dist/src/watcher.d.ts +7 -1
  251. package/dist/src/watcher.js +198 -55
  252. package/dist/src/worker.d.ts +27 -3
  253. package/dist/src/worker.js +274 -55
  254. package/package.json +33 -12
  255. package/scripts/native-reboot-rehearsal.mjs +90 -0
  256. package/web/app.js +6032 -343
  257. package/web/bootstrap.js +17 -0
  258. package/web/index.html +255 -57
  259. package/web/rail.js +317 -40
  260. package/web/retention.html +2 -2
  261. package/web/rules-view.js +188 -16
  262. package/web/sessions-view.js +485 -62
  263. package/web/sessions.html +2 -2
  264. package/web/setup-api.js +152 -29
  265. package/web/setup-logic.js +68 -9
  266. package/web/setup.html +113 -44
  267. package/web/setup.js +604 -71
  268. package/web/style.css +513 -98
  269. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  275. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,715 @@
1
+ import { createHash } from "node:crypto";
2
+ import { open } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { Readable } from "node:stream";
5
+ import { createGunzip } from "node:zlib";
6
+ import { decryptArchiveBodyChunks, unsqueezeStream, validateEnvelope, x25519PrivateKeyFromRaw } from "../packages/sealkeep-crypto/src/index.js";
7
+ import { chunkWindowForRange, openChunkWindow, supportsChunkAccess } from "../packages/sealkeep-crypto/src/chunk-access.js";
8
+ import { collectChunks, LEGACY_BUFFER_LIMIT_BYTES, MAX_FRAME_METADATA_BYTES } from "./byte-stream.js";
9
+ import { fail, isSealkeepError } from "./errors.js";
10
+ import { isV2 } from "./types.js";
11
+ import { findArchive, listArchives, readConfig, resolveDeltaChain, teamSpaceOf } from "./vault.js";
12
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ export const envelopeDigest = (envelope) => digest(JSON.stringify(envelope));
14
+ const validHash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/i.test(value);
15
+ export function sourceReference(id, facts, scope = {}) {
16
+ return { version: 1, archiveId: id, envelopeArchiveId: facts.envelopeArchiveId ?? id,
17
+ sourceSha256: facts.sourceSha256, sourceBytes: facts.sourceBytes, ciphertextSha256: facts.ciphertextSha256, ...scope };
18
+ }
19
+ export async function resolveSourceReference(dataDir, phrase, archiveId, project, projectKey) {
20
+ const { pullBoundTeamProjectIndex, archiveCatalogFacts } = await import("./search.js");
21
+ const binding = teamSpaceOf(await readConfig(dataDir), project, projectKey);
22
+ if (binding) {
23
+ const access = await pullBoundTeamProjectIndex(dataDir, phrase, project, [], projectKey, { sourceFactsOnly: true });
24
+ if (!access)
25
+ return fail("forbidden", "The current Team source catalog is unavailable");
26
+ const id = Object.keys(access.index.archives).find((candidate) => candidate === archiveId
27
+ || access.index.meta?.[candidate]?.archiveRecord?.envelopeArchiveId === archiveId);
28
+ if (!id)
29
+ return fail("archive_not_found", "The archive is absent from the current Team project catalog");
30
+ const { facts } = teamCatalogEntry(access, id);
31
+ return sourceReference(id, facts, { project, ...(projectKey ? { projectKey } : {}), spaceKey: binding.spaceKey });
32
+ }
33
+ const record = await findArchive(dataDir, archiveId);
34
+ const facts = archiveCatalogFacts(record);
35
+ if (!facts || !isV2(record) || record.source.project !== project || (projectKey && record.source.projectKey !== projectKey)) {
36
+ return fail("archive_not_found", "The archive does not belong to the selected project");
37
+ }
38
+ return sourceReference(record.id, facts, { project, ...(projectKey ? { projectKey } : {}) });
39
+ }
40
+ function matchesSource(id, facts, source) {
41
+ return (facts.envelopeArchiveId ?? id) === source.envelopeArchiveId
42
+ && facts.sourceSha256 === source.sourceSha256 && facts.sourceBytes === source.sourceBytes
43
+ && facts.ciphertextSha256 === source.ciphertextSha256;
44
+ }
45
+ function catalogEntry(index, id, spaceKey) {
46
+ const line = index.archives[id];
47
+ const facts = index.meta?.[id]?.archiveRecord;
48
+ if (!line || (spaceKey !== undefined && line[3] !== spaceKey) || !facts || facts.v !== 1
49
+ || !Number.isSafeInteger(facts.sourceBytes) || facts.sourceBytes < 0
50
+ || !Number.isSafeInteger(facts.storedBytes) || facts.storedBytes <= 0
51
+ || !Number.isSafeInteger(facts.chunks) || facts.chunks < 1
52
+ || !validHash(facts.sourceSha256) || !validHash(facts.ciphertextSha256)
53
+ || (facts.envelopeSha256 !== undefined && !validHash(facts.envelopeSha256))) {
54
+ return fail("ciphertext_integrity_failed", "The shared source has no valid authenticated catalog entry");
55
+ }
56
+ const base = facts.delta?.baseBytes ?? 0;
57
+ if (!Number.isSafeInteger(base) || base < 0 || (facts.delta && (!facts.delta.baseArchiveId || base <= 0 || base >= facts.sourceBytes))) {
58
+ return fail("ciphertext_integrity_failed", "The shared source has an invalid dependency range");
59
+ }
60
+ return { line, facts, plaintextBytes: facts.sourceBytes - base };
61
+ }
62
+ export function teamCatalogEntry(access, id) {
63
+ return catalogEntry(access.index, id, access.spaceKey);
64
+ }
65
+ function verifyEnvelope(id, envelope, entry) {
66
+ const { facts, plaintextBytes } = entry;
67
+ validateEnvelope(envelope, facts.storedBytes);
68
+ const rawBytes = envelope.manifest.compression === undefined ? envelope.manifest.plaintextBytes : envelope.manifest.originalBytes;
69
+ const rawHash = envelope.manifest.compression === undefined ? envelope.manifest.plaintextSha256 : envelope.manifest.originalSha256;
70
+ if (envelope.archiveId !== (facts.envelopeArchiveId ?? id) || envelope.suite !== facts.algorithm
71
+ || envelope.chunks.length !== facts.chunks || rawBytes !== plaintextBytes
72
+ || (!facts.delta && rawHash !== facts.sourceSha256)
73
+ || (facts.envelopeSha256 && envelopeDigest(envelope) !== facts.envelopeSha256)) {
74
+ fail("ciphertext_integrity_failed", "The shared envelope does not match the cited source");
75
+ }
76
+ }
77
+ function recordFor(dataDir, id, envelope, entry) {
78
+ const { facts, line } = entry;
79
+ return { version: 2, id, createdAt: facts.createdAt,
80
+ source: { path: line[1] ?? `archive ${id}`, agent: line[0] ?? "unknown", bytes: facts.sourceBytes,
81
+ sha256: facts.sourceSha256, ...(line[2] ? { project: line[2] } : {}),
82
+ ...(line[3] ? { projectScope: line[3] } : {}), ...(line[4] ? { projectKey: line[4] } : {}) },
83
+ cipher: { algorithm: envelope.suite, ciphertextSha256: facts.ciphertextSha256, storedBytes: facts.storedBytes, chunks: facts.chunks },
84
+ envelope, objectPath: join(dataDir, "never-materialised", `${id}.skarchive`),
85
+ ...(facts.delta ? { delta: facts.delta } : {}), ...(facts.supersedes ? { supersedes: facts.supersedes } : {}) };
86
+ }
87
+ /** Shared by search snippets, context reads and explicit Team restoration. */
88
+ export async function resolveTeamArchive(dataDir, id, access, limits = {}) {
89
+ const { createTeamReadGuard, pullTeamCiphertextStream, unframeObject } = await import("./cloud.js");
90
+ const entry = teamCatalogEntry(access, id);
91
+ const maxWire = limits.maxWireBytes ?? LEGACY_BUFFER_LIMIT_BYTES;
92
+ const maxDecoded = limits.maxDecodedBytes ?? LEGACY_BUFFER_LIMIT_BYTES;
93
+ if (entry.facts.storedBytes + 8 > maxWire || entry.facts.sourceBytes > maxDecoded) {
94
+ return fail("provider_unsupported", "This source needs a verified partial read or a larger explicit restore budget", { reason: "partial_read_unsupported" });
95
+ }
96
+ const guard = access.readGuard ?? await createTeamReadGuard(dataDir, access.spaceKey, [id], access.membershipVersion, { signal: limits.signal });
97
+ await guard.assertCurrent([id]);
98
+ const framed = await collectChunks(await pullTeamCiphertextStream(dataDir, access.spaceKey, id, undefined, { signal: limits.signal, maxBytes: maxWire }), maxWire);
99
+ const { envelope, ciphertext } = unframeObject(framed);
100
+ if (!envelope || ciphertext.length !== entry.facts.storedBytes || digest(ciphertext) !== entry.facts.ciphertextSha256) {
101
+ fail("ciphertext_integrity_failed", "The downloaded object does not match the cited source");
102
+ }
103
+ verifyEnvelope(id, envelope, entry);
104
+ await guard.assertCurrent();
105
+ return { record: recordFor(dataDir, id, envelope, entry),
106
+ teamRead: { ...access, framed, grants: new Set([id]), assertCurrent: guard.assertCurrent }, guard };
107
+ }
108
+ async function openWhole(envelope, ciphertext, unlock, maxBytes, signal) {
109
+ const compressed = envelope.manifest.compression !== undefined;
110
+ const expected = compressed ? envelope.manifest.originalBytes : envelope.manifest.plaintextBytes;
111
+ const expectedHash = compressed ? envelope.manifest.originalSha256 : envelope.manifest.plaintextSha256;
112
+ if (!Number.isSafeInteger(expected) || expected < 0 || expected > maxBytes || !validHash(expectedHash)) {
113
+ return fail("provider_unsupported", "The source exceeds this read's decompression budget", { reason: "decoded_budget_exceeded" });
114
+ }
115
+ const chunks = decryptArchiveBodyChunks(envelope, ciphertext, unlock);
116
+ const codec = envelope.manifest.compression;
117
+ const expand = () => codec === "gzip" || codec === "gzip-chunk" ? createGunzip()
118
+ : codec === "zstd" || codec === "zstd-chunk" ? unsqueezeStream()
119
+ : fail("ciphertext_integrity_failed", "Unsupported source compression");
120
+ async function* plaintext() {
121
+ if (!codec) {
122
+ for (const chunk of chunks) {
123
+ signal?.throwIfAborted();
124
+ yield chunk;
125
+ }
126
+ return;
127
+ }
128
+ const groups = codec.endsWith("-chunk") ? chunks.map((chunk) => [chunk]) : [chunks];
129
+ for (const group of groups) {
130
+ const stream = Readable.from(group).pipe(expand());
131
+ try {
132
+ for await (const chunk of stream) {
133
+ signal?.throwIfAborted();
134
+ yield Buffer.from(chunk);
135
+ }
136
+ }
137
+ finally {
138
+ stream.destroy();
139
+ }
140
+ }
141
+ }
142
+ try {
143
+ const bytes = await collectChunks(plaintext(), maxBytes);
144
+ if (bytes.length !== expected || digest(bytes) !== expectedHash) {
145
+ bytes.fill(0);
146
+ fail("plaintext_integrity_failed", "The source did not reproduce its authenticated original bytes");
147
+ }
148
+ return bytes;
149
+ }
150
+ finally {
151
+ for (const chunk of chunks)
152
+ chunk.fill(0);
153
+ }
154
+ }
155
+ async function personalReadGuard(dataDir, signal) {
156
+ const { cloudToken, cloudAuthGeneration, cloudUrl, readCloudAuthSnapshot } = await import("./cloud.js");
157
+ await cloudToken(dataDir, undefined, { signal }); // Complete any renewal before pinning the read.
158
+ const snapshot = () => readCloudAuthSnapshot(dataDir, async () => ({
159
+ token: await cloudToken(dataDir, undefined, { signal, renew: false }),
160
+ generation: await cloudAuthGeneration(dataDir), origin: cloudUrl(), vaultId: (await readConfig(dataDir)).vaultId,
161
+ }));
162
+ const initial = await snapshot();
163
+ return async () => {
164
+ signal?.throwIfAborted();
165
+ const current = await snapshot();
166
+ if (current.token !== initial.token || current.generation !== initial.generation
167
+ || current.origin !== initial.origin || current.vaultId !== initial.vaultId)
168
+ fail("unauthorized", "The account changed during the source read");
169
+ };
170
+ }
171
+ async function localCiphertext(record, signal) {
172
+ const handle = await open(record.objectPath, "r").catch(() => null);
173
+ if (!handle)
174
+ return null;
175
+ try {
176
+ if ((await handle.stat()).size !== record.cipher.storedBytes)
177
+ fail("ciphertext_integrity_failed", "The local source has an unexpected stored size");
178
+ const bytes = Buffer.alloc(record.cipher.storedBytes);
179
+ try {
180
+ let at = 0;
181
+ while (at < bytes.length) {
182
+ signal?.throwIfAborted();
183
+ const read = await handle.read(bytes, at, bytes.length - at, at);
184
+ if (!read.bytesRead)
185
+ fail("ciphertext_integrity_failed", "The local source is truncated");
186
+ at += read.bytesRead;
187
+ }
188
+ if (digest(bytes) !== record.cipher.ciphertextSha256)
189
+ fail("ciphertext_integrity_failed", "The local source ciphertext failed verification");
190
+ return bytes;
191
+ }
192
+ catch (error) {
193
+ bytes.fill(0);
194
+ throw error;
195
+ }
196
+ }
197
+ finally {
198
+ await handle.close();
199
+ }
200
+ }
201
+ async function readPersonalRemoteSource(dataDir, phrase, source, range, maxWire, maxDecoded, signal, chunkIndex, allowMissingIndex = false, managedFolder = false) {
202
+ const assertCurrent = await personalReadGuard(dataDir, signal);
203
+ const { loadPersonalSourceFacts } = await import("./search.js");
204
+ let decodedBytes = 0;
205
+ const index = await loadPersonalSourceFacts(dataDir, phrase, { signal, maxIndexBytes: maxWire, maxIndexDecodedBytes: maxDecoded,
206
+ onIndexDecoded(bytes) { decodedBytes += bytes; } }).catch((error) => {
207
+ // A retained local record can still use the existing bounded whole-body
208
+ // path when no index was built. A damaged index is never such a fallback.
209
+ if (allowMissingIndex && isSealkeepError(error) && error.code === "invalid_argument")
210
+ return null;
211
+ throw error;
212
+ });
213
+ if (!index)
214
+ return null;
215
+ const facts = index.meta?.[source.archiveId]?.archiveRecord;
216
+ const line = index.archives[source.archiveId];
217
+ if (!facts || !line || !matchesSource(source.archiveId, facts, source)
218
+ || (source.project && line[2] !== source.project) || (source.projectKey && line[4] !== source.projectKey)) {
219
+ return fail("ciphertext_integrity_failed", "The personal catalog does not match this immutable project citation");
220
+ }
221
+ const entry = catalogEntry(index, source.archiveId);
222
+ if ((managedFolder || range.start > 0 || range.end < source.sourceBytes) && !facts.delta && facts.envelopeSha256) {
223
+ return readPersonalRange(dataDir, phrase, source, range, entry, maxWire, maxDecoded, decodedBytes, assertCurrent, signal, chunkIndex, managedFolder);
224
+ }
225
+ if (facts.storedBytes + 8 > maxWire || facts.sourceBytes > maxDecoded - decodedBytes) {
226
+ return fail("provider_unsupported", !facts.envelopeSha256
227
+ ? "This source catalog predates authenticated chunk mappings; bounded reading requires an authenticated catalog upgrade"
228
+ : "This personal source exceeds the bounded whole-object reader", { reason: "partial_read_unsupported" });
229
+ }
230
+ const { pullCiphertextStream, unframeObject } = await import("./cloud.js");
231
+ const chain = [];
232
+ let current = source.archiveId;
233
+ while (current) {
234
+ if (chain.length >= 16 || chain.includes(current))
235
+ fail("ciphertext_integrity_failed", "The personal dependency chain is cyclic or too deep");
236
+ const entry = catalogEntry(index, current);
237
+ if ((source.project && entry.line[2] !== source.project) || (source.projectKey && entry.line[4] !== source.projectKey))
238
+ fail("forbidden", "A source dependency does not belong to the cited project");
239
+ chain.unshift(current);
240
+ const delta = entry.facts.delta;
241
+ if (delta && catalogEntry(index, delta.baseArchiveId).facts.sourceBytes !== delta.baseBytes)
242
+ fail("ciphertext_integrity_failed", "The personal dependency has an inconsistent prefix");
243
+ current = delta?.baseArchiveId;
244
+ }
245
+ const parts = [];
246
+ const assembled = createHash("sha256");
247
+ let originalBytes = 0;
248
+ let wireBytes = 0;
249
+ let record;
250
+ try {
251
+ for (const ref of chain) {
252
+ const entry = catalogEntry(index, ref);
253
+ if (entry.facts.storedBytes + 8 > maxWire - wireBytes || entry.plaintextBytes > maxDecoded - decodedBytes)
254
+ fail("provider_unsupported", "The personal dependency exceeds the aggregate read budget");
255
+ await assertCurrent();
256
+ const framed = await collectChunks(await pullCiphertextStream(dataDir, ref, undefined, { signal, maxBytes: maxWire - wireBytes }), maxWire - wireBytes);
257
+ wireBytes += framed.length;
258
+ const { envelope, ciphertext } = unframeObject(framed);
259
+ if (!envelope || ciphertext.length !== entry.facts.storedBytes || digest(ciphertext) !== entry.facts.ciphertextSha256)
260
+ fail("ciphertext_integrity_failed", "The personal object does not match the source catalog");
261
+ verifyEnvelope(ref, envelope, entry);
262
+ const part = await openWhole(envelope, ciphertext, { phrase }, maxDecoded - decodedBytes, signal);
263
+ parts.push(part);
264
+ decodedBytes += part.length;
265
+ originalBytes += part.length;
266
+ assembled.update(part);
267
+ if (originalBytes !== entry.facts.sourceBytes || assembled.copy().digest("hex") !== entry.facts.sourceSha256)
268
+ fail("plaintext_integrity_failed", "The personal dependency prefix did not reproduce its cited bytes");
269
+ if (ref === source.archiveId)
270
+ record = recordFor(dataDir, ref, envelope, entry);
271
+ }
272
+ if (!record || originalBytes !== source.sourceBytes || assembled.digest("hex") !== source.sourceSha256)
273
+ fail("plaintext_integrity_failed", "The personal source did not reproduce the cited bytes");
274
+ await assertCurrent();
275
+ const whole = Buffer.concat(parts);
276
+ const bytes = Buffer.from(whole.subarray(range.start, range.end));
277
+ whole.fill(0);
278
+ if (bytes.length !== range.end - range.start)
279
+ fail("plaintext_integrity_failed", "The personal source returned an incomplete interval");
280
+ return { source, record, bytes, byteStart: range.start, byteEnd: range.end, wireBytes, decodedBytes, assertCurrent };
281
+ }
282
+ finally {
283
+ for (const part of parts)
284
+ part.fill(0);
285
+ }
286
+ }
287
+ /** The encrypted catalog, not a plaintext local record or caller hint,
288
+ * authenticates the complete raw-to-ciphertext mapping before any chunk use. */
289
+ async function readPersonalRange(dataDir, phrase, source, range, entry, maxWire, maxDecoded, decodedBytes, assertCurrent, signal, chunkIndex, managedFolder = false) {
290
+ const { pullCiphertextRange } = await import("./cloud.js");
291
+ const { managedChunkRefs } = await import("./managed-chunks.js");
292
+ const folder = managedFolder ? managedChunkRefs(source.archiveId, entry.facts.chunks) : undefined;
293
+ let descriptor;
294
+ let wireBytes = 0;
295
+ const pull = async (offset, length, ref = folder?.envelope ?? source.archiveId, expectedChunk) => {
296
+ if (!Number.isSafeInteger(length) || length < 1 || length > maxWire - wireBytes)
297
+ fail("provider_unsupported", "The source exceeded its aggregate wire budget");
298
+ await assertCurrent();
299
+ const value = await pullCiphertextRange(dataDir, ref, offset, length, undefined, {
300
+ signal, maxBytes: maxWire - wireBytes,
301
+ async onDescriptor(current) {
302
+ if (expectedChunk) {
303
+ if (current.bytes !== expectedChunk.storedBytes || current.ciphertextSha256 !== expectedChunk.sha256)
304
+ fail("ciphertext_integrity_failed", "The managed chunk descriptor does not match its authenticated envelope");
305
+ }
306
+ else {
307
+ if (descriptor && (descriptor.vaultRef !== current.vaultRef || descriptor.bytes !== current.bytes
308
+ || descriptor.ciphertextSha256 !== current.ciphertextSha256))
309
+ fail("ciphertext_integrity_failed", "The personal object changed between range reads");
310
+ descriptor ??= current;
311
+ }
312
+ await assertCurrent();
313
+ },
314
+ });
315
+ wireBytes += value.length;
316
+ return value;
317
+ };
318
+ const header = await pull(0, 8);
319
+ if (header.subarray(0, 4).toString("ascii") !== "VLA1")
320
+ fail("ciphertext_integrity_failed", "The personal source frame is invalid");
321
+ const metadataBytes = header.readUInt32BE(4);
322
+ if (!metadataBytes)
323
+ fail("ciphertext_integrity_failed", "The personal source envelope is empty");
324
+ if (metadataBytes > MAX_FRAME_METADATA_BYTES || metadataBytes > maxWire - wireBytes)
325
+ fail("provider_unsupported", "The source envelope exceeds the wire budget");
326
+ if (!folder && descriptor.bytes !== 8 + metadataBytes + entry.facts.storedBytes)
327
+ fail("ciphertext_integrity_failed", "The personal frame size does not match its authenticated catalog");
328
+ const metadata = await pull(8, metadataBytes);
329
+ let envelope;
330
+ try {
331
+ envelope = JSON.parse(metadata.toString("utf8"));
332
+ }
333
+ catch {
334
+ return fail("ciphertext_integrity_failed", "The personal source envelope is invalid");
335
+ }
336
+ verifyEnvelope(source.archiveId, envelope, entry);
337
+ if (!entry.facts.envelopeSha256 || envelopeDigest(envelope) !== entry.facts.envelopeSha256)
338
+ fail("ciphertext_integrity_failed", "The personal source has no authenticated chunk mapping");
339
+ if (!supportsChunkAccess(envelope))
340
+ fail("provider_unsupported", "This personal source predates independent compression", { reason: "partial_read_unsupported" });
341
+ if (chunkIndex !== undefined && range.start === 0) {
342
+ if (chunkIndex >= envelope.chunks.length)
343
+ fail("invalid_argument", "The chunk hint is outside this source");
344
+ const size = range.end - range.start;
345
+ range.start = envelope.chunks.slice(0, chunkIndex).reduce((n, chunk) => n + chunk.rawBytes, 0);
346
+ range.end = Math.min(source.sourceBytes, range.start + size);
347
+ }
348
+ const window = chunkWindowForRange(envelope, range.start, range.end);
349
+ const expandedBytes = envelope.chunks.slice(window.firstIndex, window.lastIndex + 1).reduce((n, chunk) => n + chunk.rawBytes, 0);
350
+ if (expandedBytes > maxDecoded - decodedBytes)
351
+ fail("provider_unsupported", "The source chunks exceed the aggregate decompression budget", { reason: "decoded_budget_exceeded" });
352
+ let stored;
353
+ if (folder) {
354
+ // The local layout is only a routing hint. The encrypted catalog digest
355
+ // above authenticates every offset/hash before any chunk is selected.
356
+ if (window.storedLength > maxWire - wireBytes)
357
+ fail("provider_unsupported", "The source exceeded its aggregate wire budget");
358
+ const parts = [];
359
+ try {
360
+ for (let i = window.firstIndex; i <= window.lastIndex; i++) {
361
+ const chunk = envelope.chunks[i];
362
+ parts.push(await pull(0, chunk.storedBytes, folder.chunks[i], chunk));
363
+ }
364
+ stored = Buffer.concat(parts);
365
+ }
366
+ finally {
367
+ for (const part of parts)
368
+ part.fill(0);
369
+ }
370
+ }
371
+ else
372
+ stored = await pull(8 + metadataBytes + window.storedOffset, window.storedLength);
373
+ let bytes;
374
+ try {
375
+ const wholeSource = range.start === 0 && range.end === source.sourceBytes;
376
+ if (wholeSource && (stored.length !== entry.facts.storedBytes || digest(stored) !== entry.facts.ciphertextSha256)) {
377
+ fail("ciphertext_integrity_failed", "The complete source chunks do not match their authenticated ciphertext");
378
+ }
379
+ bytes = openChunkWindow(envelope, window, stored, { phrase }, { maxStoredBytes: maxWire, maxExpandedBytes: maxDecoded - decodedBytes, signal });
380
+ decodedBytes += expandedBytes;
381
+ if (bytes.length !== range.end - range.start)
382
+ fail("plaintext_integrity_failed", "The personal source returned an incomplete interval");
383
+ if (wholeSource && digest(bytes) !== source.sourceSha256)
384
+ fail("plaintext_integrity_failed", "The complete source chunks do not reproduce the cited source");
385
+ await assertCurrent();
386
+ return { source, record: recordFor(dataDir, source.archiveId, envelope, entry), bytes,
387
+ byteStart: range.start, byteEnd: range.end, wireBytes, decodedBytes, assertCurrent };
388
+ }
389
+ catch (error) {
390
+ bytes?.fill(0);
391
+ throw error;
392
+ }
393
+ finally {
394
+ stored.fill(0);
395
+ }
396
+ }
397
+ function readLimit(value, fallback) {
398
+ const result = value ?? fallback;
399
+ if (!Number.isSafeInteger(result) || result < 1 || result > LEGACY_BUFFER_LIMIT_BYTES) {
400
+ fail("invalid_argument", `Read budgets must be integers between 1 and ${LEGACY_BUFFER_LIMIT_BYTES}`);
401
+ }
402
+ return result;
403
+ }
404
+ /** Resolves the exact immutable source again for every read; citations are never permissions. */
405
+ export async function readSourceBytes(dataDir, phrase, source, options = {}) {
406
+ if (source.version !== 1 || !source.archiveId || !source.envelopeArchiveId || !validHash(source.sourceSha256)
407
+ || !validHash(source.ciphertextSha256) || !Number.isSafeInteger(source.sourceBytes) || source.sourceBytes < 0) {
408
+ return fail("invalid_argument", "A complete immutable search source reference is required");
409
+ }
410
+ const maxWire = readLimit(options.maxWireBytes, 8 * 1024 * 1024);
411
+ const maxDecoded = readLimit(options.maxDecodedBytes, 16 * 1024 * 1024);
412
+ const range = { ...(options.range ?? { start: 0, end: source.sourceBytes }) };
413
+ if (options.chunkIndex !== undefined && (!Number.isSafeInteger(options.chunkIndex) || options.chunkIndex < 0))
414
+ fail("invalid_argument", "chunkIndex must be a non-negative integer");
415
+ if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0
416
+ || range.end < range.start || range.end > source.sourceBytes || range.end - range.start > maxDecoded) {
417
+ return fail("provider_unsupported", "The requested source range exceeds the bounded read", { reason: "decoded_budget_exceeded" });
418
+ }
419
+ options.signal?.throwIfAborted();
420
+ if (!source.spaceKey) {
421
+ const record = await findArchive(dataDir, source.archiveId).catch((error) => {
422
+ if (isSealkeepError(error) && error.code === "archive_not_found")
423
+ return null;
424
+ throw error;
425
+ });
426
+ if (!record) {
427
+ const remote = await readPersonalRemoteSource(dataDir, phrase, source, range, maxWire, maxDecoded, options.signal, options.chunkIndex);
428
+ return remote ?? fail("archive_not_found", "The personal source has no authenticated catalog");
429
+ }
430
+ if (!isV2(record) || record.envelope.archiveId !== source.envelopeArchiveId || record.source.sha256 !== source.sourceSha256
431
+ || record.source.bytes !== source.sourceBytes || record.cipher.ciphertextSha256 !== source.ciphertextSha256
432
+ || (source.projectKey && record.source.projectKey !== source.projectKey)
433
+ || (source.project && record.source.project !== source.project)) {
434
+ return fail("ciphertext_integrity_failed", "The local source no longer matches this immutable citation");
435
+ }
436
+ const partial = range.start > 0 || range.end < source.sourceBytes;
437
+ // A managed chunk folder has no single archive-id object, even when a
438
+ // small citation fits wholly in the caller's unchanged byte budgets.
439
+ const managedFolder = record.remote?.provider === "vaultline" && record.remote.layout?.kind === "chunks";
440
+ if ((partial || managedFolder) && !record.delta && supportsChunkAccess(record.envelope)) {
441
+ const handle = await open(record.objectPath, "r").catch(() => null);
442
+ if (handle && !partial) {
443
+ // Keep the existing complete local-body verification when it exists.
444
+ await handle.close();
445
+ }
446
+ else if (!handle) {
447
+ const remote = await readPersonalRemoteSource(dataDir, phrase, source, range, maxWire, maxDecoded, options.signal, options.chunkIndex, true, managedFolder);
448
+ if (remote)
449
+ return remote;
450
+ }
451
+ else {
452
+ try {
453
+ if (options.chunkIndex !== undefined && range.start === 0) {
454
+ const size = range.end - range.start;
455
+ if (options.chunkIndex >= record.envelope.chunks.length)
456
+ fail("invalid_argument", "The chunk hint is outside this source");
457
+ range.start = record.envelope.chunks.slice(0, options.chunkIndex).reduce((n, chunk) => n + chunk.rawBytes, 0);
458
+ range.end = Math.min(source.sourceBytes, range.start + size);
459
+ }
460
+ const window = chunkWindowForRange(record.envelope, range.start, range.end);
461
+ if (window.storedLength > maxWire)
462
+ fail("provider_unsupported", "The source chunks exceed the read budget");
463
+ const stored = Buffer.alloc(window.storedLength);
464
+ try {
465
+ let offset = 0;
466
+ while (offset < stored.length) {
467
+ options.signal?.throwIfAborted();
468
+ const result = await handle.read(stored, offset, stored.length - offset, window.storedOffset + offset);
469
+ if (!result.bytesRead)
470
+ fail("ciphertext_integrity_failed", "The source range is truncated");
471
+ offset += result.bytesRead;
472
+ }
473
+ const bytes = openChunkWindow(record.envelope, window, stored, { phrase }, { maxStoredBytes: maxWire, maxExpandedBytes: maxDecoded, signal: options.signal });
474
+ if (bytes.length !== range.end - range.start) {
475
+ bytes.fill(0);
476
+ fail("plaintext_integrity_failed", "The source returned an incomplete interval");
477
+ }
478
+ return { source, record, bytes, byteStart: range.start, byteEnd: range.end, wireBytes: 0,
479
+ decodedBytes: record.envelope.chunks.slice(window.firstIndex, window.lastIndex + 1).reduce((n, c) => n + c.rawBytes, 0), assertCurrent: async () => { } };
480
+ }
481
+ finally {
482
+ stored.fill(0);
483
+ }
484
+ }
485
+ finally {
486
+ await handle.close();
487
+ }
488
+ }
489
+ }
490
+ if (record.source.bytes > maxDecoded || record.cipher.storedBytes > maxWire) {
491
+ return fail("provider_unsupported", "This source requires a verified bounded range read", { reason: "partial_read_unsupported" });
492
+ }
493
+ const chain = record.delta ? resolveDeltaChain(await listArchives(dataDir), record) : [record];
494
+ const parts = [];
495
+ const assembled = createHash("sha256");
496
+ let decodedBytes = 0;
497
+ let wireBytes = 0;
498
+ let storedBytes = 0;
499
+ let assertCurrent = async () => { };
500
+ let remote = false;
501
+ try {
502
+ for (const link of chain) {
503
+ if (!isV2(link))
504
+ fail("provider_unsupported", "The dependency predates the bounded source reader", { reason: "partial_read_unsupported" });
505
+ storedBytes += link.cipher.storedBytes;
506
+ if (storedBytes > maxWire)
507
+ fail("provider_unsupported", "The dependency chain exceeds the aggregate stored-byte budget");
508
+ let ciphertext = await localCiphertext(link, options.signal);
509
+ if (!ciphertext) {
510
+ if (!remote) {
511
+ assertCurrent = await personalReadGuard(dataDir, options.signal);
512
+ remote = true;
513
+ }
514
+ await assertCurrent();
515
+ const { pullCiphertextStream, unframeObject } = await import("./cloud.js");
516
+ const framed = await collectChunks(await pullCiphertextStream(dataDir, link.id, undefined, { signal: options.signal, maxBytes: maxWire - wireBytes }), maxWire - wireBytes);
517
+ wireBytes += framed.length;
518
+ const opened = unframeObject(framed);
519
+ if (!opened.envelope || opened.ciphertext.length !== link.cipher.storedBytes
520
+ || digest(opened.ciphertext) !== link.cipher.ciphertextSha256
521
+ || envelopeDigest(opened.envelope) !== envelopeDigest(link.envelope))
522
+ fail("ciphertext_integrity_failed", "The remote dependency does not match its local catalog");
523
+ ciphertext = opened.ciphertext;
524
+ }
525
+ try {
526
+ const part = await openWhole(link.envelope, ciphertext, { phrase }, maxDecoded - decodedBytes, options.signal);
527
+ parts.push(part);
528
+ decodedBytes += part.length;
529
+ assembled.update(part);
530
+ if (decodedBytes !== link.source.bytes || assembled.copy().digest("hex") !== link.source.sha256) {
531
+ fail("plaintext_integrity_failed", "A dependency prefix did not reproduce its authenticated source");
532
+ }
533
+ }
534
+ finally {
535
+ ciphertext.fill(0);
536
+ }
537
+ }
538
+ const whole = Buffer.concat(parts);
539
+ if (whole.length !== source.sourceBytes || digest(whole) !== source.sourceSha256)
540
+ fail("plaintext_integrity_failed", "Source hash mismatch");
541
+ const bytes = Buffer.from(whole.subarray(range.start, range.end));
542
+ whole.fill(0);
543
+ try {
544
+ if (bytes.length !== range.end - range.start)
545
+ fail("plaintext_integrity_failed", "The dependency chain returned an incomplete interval");
546
+ await assertCurrent();
547
+ return { source, record, bytes, byteStart: range.start, byteEnd: range.end, wireBytes, decodedBytes, assertCurrent };
548
+ }
549
+ catch (error) {
550
+ bytes.fill(0);
551
+ throw error;
552
+ }
553
+ }
554
+ finally {
555
+ for (const part of parts)
556
+ part.fill(0);
557
+ }
558
+ }
559
+ if (!source.project)
560
+ fail("invalid_argument", "A Team citation must include its locally confirmed project");
561
+ const binding = teamSpaceOf(await readConfig(dataDir), source.project, source.projectKey);
562
+ if (!binding || binding.spaceKey !== source.spaceKey)
563
+ fail("forbidden", "The citation does not belong to this confirmed project");
564
+ const { pullBoundTeamProjectIndex } = await import("./search.js");
565
+ let indexWireBytes = 0;
566
+ const current = await pullBoundTeamProjectIndex(dataDir, phrase, source.project, [], source.projectKey, {
567
+ signal: options.signal, maxIndexBytes: maxWire, maxIndexDecodedBytes: maxDecoded, strictErrors: true, sourceFactsOnly: true,
568
+ onProgress(bytes) {
569
+ indexWireBytes += bytes;
570
+ if (indexWireBytes > maxWire)
571
+ fail("provider_unsupported", "Index synchronization exceeded the read's aggregate wire budget");
572
+ },
573
+ });
574
+ if (!current || current.spaceKey !== source.spaceKey)
575
+ fail("forbidden", "The current Team source index is unavailable");
576
+ const access = current;
577
+ const id = Object.keys(access.index.archives).find((candidate) => {
578
+ const facts = access.index.meta?.[candidate]?.archiveRecord;
579
+ return facts && matchesSource(candidate, facts, source);
580
+ });
581
+ if (!id)
582
+ fail("archive_not_found", "This exact source version is absent from the current authorized Team catalog");
583
+ const { createTeamReadGuard, pullTeamCiphertextRange, pullTeamCiphertextStream, unframeObject } = await import("./cloud.js");
584
+ const guard = current.readGuard;
585
+ await guard.assertCurrent([id]);
586
+ const { ensureTeamIdentity } = await import("./team-presence.js");
587
+ const privateRaw = Buffer.from((await ensureTeamIdentity(dataDir, phrase)).privateKey, "base64");
588
+ let wireBytes = indexWireBytes;
589
+ let decodedBytes = current.indexDecodedBytes;
590
+ const used = new Set();
591
+ let headRecord;
592
+ const partial = range.start > 0 || range.end < source.sourceBytes;
593
+ if (partial && options.chunkIndex !== undefined && teamCatalogEntry(access, id).facts.delta) {
594
+ privateRaw.fill(0);
595
+ return fail("provider_unsupported", "This large delta snapshot needs an authenticated logical-chunk locator; use an exact byte cursor or explicit restore", { reason: "partial_read_unsupported" });
596
+ }
597
+ try {
598
+ const privateKey = x25519PrivateKeyFromRaw(privateRaw);
599
+ async function readLink(ref, start, end, depth = 0) {
600
+ if (depth >= 16 || used.has(ref))
601
+ fail("ciphertext_integrity_failed", "The source dependency chain is cyclic or too deep");
602
+ used.add(ref);
603
+ await guard.assertCurrent(used);
604
+ const entry = teamCatalogEntry(access, ref);
605
+ const base = entry.facts.delta?.baseBytes ?? 0;
606
+ const parts = [];
607
+ try {
608
+ if (entry.facts.delta && start < base) {
609
+ const previous = teamCatalogEntry(access, entry.facts.delta.baseArchiveId);
610
+ if (previous.facts.sourceBytes !== base)
611
+ fail("ciphertext_integrity_failed", "The source dependency does not match its prefix");
612
+ parts.push(await readLink(entry.facts.delta.baseArchiveId, start, Math.min(end, base), depth + 1));
613
+ }
614
+ if (end > base) {
615
+ let localStart = Math.max(start, base) - base;
616
+ let localEnd = end - base;
617
+ const rangeRead = partial && entry.facts.envelopeSha256;
618
+ let envelope;
619
+ if (rangeRead) {
620
+ const pull = async (offset, length) => {
621
+ if (length > maxWire - wireBytes)
622
+ fail("provider_unsupported", "The source exceeded its aggregate wire budget");
623
+ const value = await pullTeamCiphertextRange(dataDir, access.spaceKey, ref, offset, length, undefined, { signal: options.signal, maxBytes: maxWire - wireBytes, expectedMembershipVersion: access.membershipVersion, readGuard: guard });
624
+ wireBytes += value.length;
625
+ return value;
626
+ };
627
+ const header = await pull(0, 8);
628
+ if (header.subarray(0, 4).toString("ascii") !== "VLA1")
629
+ fail("ciphertext_integrity_failed", "The shared source frame is invalid");
630
+ const length = header.readUInt32BE(4);
631
+ if (length > MAX_FRAME_METADATA_BYTES || length > maxWire - wireBytes)
632
+ fail("provider_unsupported", "The source envelope exceeds the wire budget");
633
+ const metadata = await pull(8, length);
634
+ try {
635
+ envelope = JSON.parse(metadata.toString("utf8"));
636
+ }
637
+ catch {
638
+ return fail("ciphertext_integrity_failed", "The shared source envelope is invalid");
639
+ }
640
+ verifyEnvelope(ref, envelope, entry);
641
+ if (!supportsChunkAccess(envelope))
642
+ fail("provider_unsupported", "This source predates independent compression", { reason: "partial_read_unsupported" });
643
+ if (ref === id && !entry.facts.delta && options.chunkIndex !== undefined && range.start === 0) {
644
+ if (options.chunkIndex >= envelope.chunks.length)
645
+ fail("invalid_argument", "The chunk hint is outside this source");
646
+ const size = localEnd - localStart;
647
+ localStart = envelope.chunks.slice(0, options.chunkIndex).reduce((n, chunk) => n + chunk.rawBytes, 0);
648
+ localEnd = Math.min(entry.plaintextBytes, localStart + size);
649
+ range.start = localStart;
650
+ range.end = localEnd;
651
+ }
652
+ const window = chunkWindowForRange(envelope, localStart, localEnd);
653
+ const stored = await pull(8 + length + window.storedOffset, window.storedLength);
654
+ try {
655
+ parts.push(openChunkWindow(envelope, window, stored, { privateKey }, { maxStoredBytes: maxWire, maxExpandedBytes: maxDecoded - decodedBytes, signal: options.signal }));
656
+ decodedBytes += envelope.chunks.slice(window.firstIndex, window.lastIndex + 1).reduce((n, chunk) => n + chunk.rawBytes, 0);
657
+ }
658
+ finally {
659
+ stored.fill(0);
660
+ }
661
+ }
662
+ else {
663
+ if (entry.plaintextBytes > maxDecoded - decodedBytes || entry.facts.storedBytes + 8 > maxWire - wireBytes) {
664
+ fail("provider_unsupported", "This source has no authenticated bounded range map within the read budget", { reason: "partial_read_unsupported" });
665
+ }
666
+ const framed = await collectChunks(await pullTeamCiphertextStream(dataDir, access.spaceKey, ref, undefined, { signal: options.signal, maxBytes: maxWire - wireBytes }), maxWire - wireBytes);
667
+ wireBytes += framed.length;
668
+ const opened = unframeObject(framed);
669
+ if (!opened.envelope || opened.ciphertext.length !== entry.facts.storedBytes || digest(opened.ciphertext) !== entry.facts.ciphertextSha256) {
670
+ fail("ciphertext_integrity_failed", "The source object does not match the authenticated catalog");
671
+ }
672
+ envelope = opened.envelope;
673
+ verifyEnvelope(ref, envelope, entry);
674
+ const whole = await openWhole(envelope, opened.ciphertext, { privateKey }, maxDecoded - decodedBytes, options.signal);
675
+ decodedBytes += whole.length;
676
+ parts.push(Buffer.from(whole.subarray(localStart, localEnd)));
677
+ whole.fill(0);
678
+ }
679
+ if (ref === id)
680
+ headRecord = recordFor(dataDir, ref, envelope, entry);
681
+ }
682
+ return Buffer.concat(parts);
683
+ }
684
+ finally {
685
+ for (const part of parts)
686
+ part.fill(0);
687
+ }
688
+ }
689
+ const bytes = await readLink(id, range.start, range.end);
690
+ try {
691
+ if (bytes.length !== range.end - range.start)
692
+ fail("plaintext_integrity_failed", "The source did not reproduce the exact requested byte interval");
693
+ if (!partial && (bytes.length !== source.sourceBytes || digest(bytes) !== source.sourceSha256))
694
+ fail("plaintext_integrity_failed", "The dependency chain did not reproduce the cited source");
695
+ await guard.assertCurrent(used);
696
+ // A range entirely in a base still returns the head's source facts.
697
+ if (!headRecord) {
698
+ const entry = teamCatalogEntry(access, id);
699
+ headRecord = { version: 2, id, createdAt: entry.facts.createdAt,
700
+ source: { path: entry.line[1] ?? `archive ${id}`, agent: entry.line[0] ?? "unknown", bytes: source.sourceBytes, sha256: source.sourceSha256 },
701
+ cipher: { algorithm: entry.facts.algorithm, storedBytes: entry.facts.storedBytes, ciphertextSha256: entry.facts.ciphertextSha256, chunks: entry.facts.chunks },
702
+ objectPath: join(dataDir, "never-materialised", `${id}.skarchive`) };
703
+ }
704
+ return { source, record: headRecord, bytes, byteStart: range.start, byteEnd: range.end, wireBytes, decodedBytes,
705
+ assertCurrent: () => guard.assertCurrent(used) };
706
+ }
707
+ catch (error) {
708
+ bytes.fill(0);
709
+ throw error;
710
+ }
711
+ }
712
+ finally {
713
+ privateRaw.fill(0);
714
+ }
715
+ }