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
@@ -3,6 +3,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
3
  import { aeadCipher, aeadDecipher } from "./aead.js";
4
4
  import { CryptoError, unwrapArchiveKey, wrapForKey, wrapForPhrase, x25519PublicKeyFromRaw, zeroize } from "./recipients.js";
5
5
  import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, NONCE_BYTES, SUITES, TAG_BYTES, chunkAad, chunkNonce } from "./format.js";
6
+ import { squeezeSync, unsqueezeSync } from "./squeeze.js";
6
7
  const sha256 = (input) => createHash("sha256").update(input).digest("hex");
7
8
  function toPublicKey(value) {
8
9
  return Buffer.isBuffer(value) ? x25519PublicKeyFromRaw(value) : value;
@@ -38,9 +39,10 @@ export function encryptArchive(plaintext, options) {
38
39
  const noncePrefix = options.deterministic?.noncePrefix ?? randomBytes(4);
39
40
  if (archiveKey.length !== KEY_BYTES)
40
41
  throw new CryptoError("key_malformed", `An archive key must be ${KEY_BYTES} bytes`);
41
- if ((options.compression ?? "none") === "gzip-chunk")
42
- throw new CryptoError("envelope_malformed", "gzip-chunk sealing is streaming-only; use sealArchiveToFile");
43
- const body = (options.compression ?? "none") === "gzip" ? gzipSync(plaintext, { level: 6 }) : plaintext;
42
+ if (/-chunk$/.test(options.compression ?? "none"))
43
+ throw new CryptoError("envelope_malformed", `${options.compression} sealing is streaming-only; use sealArchiveToFile`);
44
+ const codecIn = options.compression ?? "none";
45
+ const body = codecIn === "gzip" ? gzipSync(plaintext, { level: 6 }) : codecIn === "zstd" ? squeezeSync(plaintext) : plaintext;
44
46
  const totalChunks = Math.max(1, Math.ceil(body.length / chunkBytes));
45
47
  const chunks = [];
46
48
  const blocks = [];
@@ -58,8 +60,8 @@ export function encryptArchive(plaintext, options) {
58
60
  const envelope = {
59
61
  version: ENVELOPE_VERSION, archiveId, suite, chunkBytes, chunks,
60
62
  manifest: { plaintextSha256: sha256(body), plaintextBytes: body.length,
61
- ...((options.compression ?? "none") === "gzip"
62
- ? { compression: "gzip", originalBytes: plaintext.length, originalSha256: sha256(plaintext) }
63
+ ...(codecIn === "gzip" || codecIn === "zstd"
64
+ ? { compression: codecIn, originalBytes: plaintext.length, originalSha256: sha256(plaintext) }
63
65
  : {}), createdAt: options.createdAt ?? new Date().toISOString(), adapter: options.adapter },
64
66
  wrappedKeys: wrapAll(archiveKey, options.recipients, suite, archiveId, options.deterministic)
65
67
  };
@@ -100,20 +102,25 @@ export function validateEnvelope(envelope, ciphertextBytes) {
100
102
  throw new CryptoError("envelope_malformed", "Chunk lengths do not sum to the manifest plaintext length");
101
103
  }
102
104
  /**
103
- * Decrypts an archive after verifying its structure and every stored chunk hash.
104
- * Every chunk is authenticated against associated data that pins its position and
105
- * the total chunk count, so a reordered or truncated archive fails rather than
106
- * returning partial plaintext.
105
+ * Authenticates and decrypts the stored body without expanding its compression.
106
+ *
107
+ * Keeping the authenticated chunks separate matters for callers that can stream
108
+ * decompression. A highly-compressible search index may be tens of megabytes on
109
+ * disk and more than a gigabyte after gzip; forcing that expanded body through a
110
+ * single Buffer is enough to OOM a long-lived agent process before it can inspect
111
+ * one query term.
107
112
  */
108
- export function decryptArchive(envelope, ciphertext, unlock) {
113
+ export function decryptArchiveBodyChunks(envelope, ciphertext, unlock) {
109
114
  validateEnvelope(envelope, ciphertext.length);
110
115
  const archiveKey = unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
111
116
  if (archiveKey.length !== KEY_BYTES) {
112
117
  zeroize(archiveKey);
113
118
  throw new CryptoError("key_malformed", "Unwrapped archive key has the wrong length");
114
119
  }
120
+ const parts = [];
115
121
  try {
116
- const parts = [];
122
+ const bodyHash = createHash("sha256");
123
+ let bodyBytes = 0;
117
124
  let offset = 0;
118
125
  for (const chunk of envelope.chunks) {
119
126
  const stored = ciphertext.subarray(offset, offset + chunk.storedBytes);
@@ -124,30 +131,57 @@ export function decryptArchive(envelope, ciphertext, unlock) {
124
131
  decipher.setAAD(chunkAad({ archiveId: envelope.archiveId, suite: envelope.suite, index: chunk.index, totalChunks: envelope.chunks.length, plaintextBytes: chunk.plaintextBytes, final: chunk.final }));
125
132
  decipher.setAuthTag(stored.subarray(stored.length - TAG_BYTES));
126
133
  try {
127
- parts.push(Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]));
134
+ const part = Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]);
135
+ parts.push(part);
136
+ bodyHash.update(part);
137
+ bodyBytes += part.length;
128
138
  }
129
139
  catch {
130
140
  throw new CryptoError("chunk_auth_failed", `Chunk ${chunk.index} failed authentication`);
131
141
  }
132
142
  }
133
- const plaintext = Buffer.concat(parts);
134
- if (plaintext.length !== envelope.manifest.plaintextBytes)
143
+ if (bodyBytes !== envelope.manifest.plaintextBytes)
135
144
  throw new CryptoError("plaintext_length_mismatch", "Decrypted length does not match the manifest");
136
- if (sha256(plaintext) !== envelope.manifest.plaintextSha256)
145
+ if (bodyHash.digest("hex") !== envelope.manifest.plaintextSha256)
137
146
  throw new CryptoError("plaintext_hash_mismatch", "Decrypted bytes do not match the manifest hash");
147
+ return parts;
148
+ }
149
+ catch (error) {
150
+ for (const part of parts)
151
+ zeroize(part);
152
+ throw error;
153
+ }
154
+ finally {
155
+ zeroize(archiveKey);
156
+ }
157
+ }
158
+ /**
159
+ * Decrypts an archive after verifying its structure and every stored chunk hash.
160
+ * Every chunk is authenticated against associated data that pins its position and
161
+ * the total chunk count, so a reordered or truncated archive fails rather than
162
+ * returning partial plaintext.
163
+ */
164
+ export function decryptArchive(envelope, ciphertext, unlock) {
165
+ const parts = decryptArchiveBodyChunks(envelope, ciphertext, unlock);
166
+ const plaintext = Buffer.concat(parts);
167
+ try {
138
168
  // Absent on archives written without compression, where the authenticated
139
169
  // body already is the caller's plaintext.
140
170
  if (envelope.manifest.compression === undefined)
141
171
  return plaintext;
142
- if (envelope.manifest.compression !== "gzip" && envelope.manifest.compression !== "gzip-chunk")
172
+ const codec = envelope.manifest.compression;
173
+ if (codec !== "gzip" && codec !== "gzip-chunk" && codec !== "zstd" && codec !== "zstd-chunk")
143
174
  throw new CryptoError("envelope_malformed", "Unsupported compression");
175
+ // Which compressor sealed this archive is recorded in its own envelope,
176
+ // so gzip archives written before zstd existed keep opening unchanged.
177
+ const expand = codec === "zstd" || codec === "zstd-chunk" ? unsqueezeSync : gunzipSync;
144
178
  let original;
145
179
  try {
146
- original = envelope.manifest.compression === "gzip-chunk"
180
+ original = codec.endsWith("-chunk")
147
181
  // Each part is one chunk's compressed slice, independently made and
148
182
  // independently undone — concatenating the results reproduces the file.
149
- ? Buffer.concat(parts.map((part) => gunzipSync(part)))
150
- : gunzipSync(plaintext);
183
+ ? Buffer.concat(parts.map((part) => expand(part)))
184
+ : expand(plaintext);
151
185
  }
152
186
  catch {
153
187
  throw new CryptoError("plaintext_hash_mismatch", "Decompression failed after authentication");
@@ -160,8 +194,11 @@ export function decryptArchive(envelope, ciphertext, unlock) {
160
194
  throw new CryptoError("plaintext_hash_mismatch", "Decompressed bytes do not match the manifest hash");
161
195
  return original;
162
196
  }
163
- finally {
164
- zeroize(archiveKey);
197
+ catch (error) {
198
+ zeroize(plaintext);
199
+ for (const part of parts)
200
+ zeroize(part);
201
+ throw error;
165
202
  }
166
203
  }
167
204
  /**
@@ -45,7 +45,7 @@ export type ChunkHeader = {
45
45
  index: number;
46
46
  /** Bytes this chunk's ciphertext decrypts to. Under "gzip-chunk" that is the compressed slice; the raw length lives in `rawBytes`. */
47
47
  plaintextBytes: number;
48
- /** Original (pre-compression) bytes this chunk covers. Present only under "gzip-chunk", where it is what makes byte-ranged retrieval computable without decompressing anything. */
48
+ /** Original (pre-compression) bytes this chunk covers. Present under either chunked codec, where it is what makes byte-ranged retrieval computable without decompressing anything. */
49
49
  rawBytes?: number;
50
50
  /** Stored bytes for this chunk: ciphertext followed by its authentication tag. */
51
51
  storedBytes: number;
@@ -66,7 +66,7 @@ export type Envelope = {
66
66
  * every chunk decrypts AND decompresses on its own — what makes streaming
67
67
  * seals and fetch-only-the-chunks retrieval possible. Absent means none.
68
68
  */
69
- compression?: "gzip" | "gzip-chunk";
69
+ compression?: "gzip" | "gzip-chunk" | "zstd" | "zstd-chunk";
70
70
  originalBytes?: number;
71
71
  originalSha256?: string;
72
72
  plaintextSha256: string;
@@ -3,3 +3,4 @@ export * from "./recipients.js";
3
3
  export * from "./envelope.js";
4
4
  export * from "./stream.js";
5
5
  export * from "./sha256-stream.js";
6
+ export * from "./squeeze.js";
@@ -3,3 +3,4 @@ export * from "./recipients.js";
3
3
  export * from "./envelope.js";
4
4
  export * from "./stream.js";
5
5
  export * from "./sha256-stream.js";
6
+ export * from "./squeeze.js";
@@ -38,10 +38,10 @@ function openKey(suite, kek, nonce, sealed, aad) {
38
38
  }
39
39
  }
40
40
  export function phraseRecipientId(salt) {
41
- return createHash("sha256").update(Buffer.concat([Buffer.from("vaultline-phrase-recipient:v2"), salt])).digest("hex").slice(0, 32);
41
+ return createHash("sha256").update(Buffer.concat([Buffer.from("sealkeep-phrase-recipient:v2"), salt])).digest("hex").slice(0, 32);
42
42
  }
43
43
  export function keyRecipientId(publicKeyRaw) {
44
- return createHash("sha256").update(Buffer.concat([Buffer.from("vaultline-key-recipient:v2"), publicKeyRaw])).digest("hex").slice(0, 32);
44
+ return createHash("sha256").update(Buffer.concat([Buffer.from("sealkeep-key-recipient:v2"), publicKeyRaw])).digest("hex").slice(0, 32);
45
45
  }
46
46
  export function rawPublicKey(key) {
47
47
  // The last 32 bytes of an X25519 SPKI DER encoding are the raw public key.
@@ -67,7 +67,7 @@ export function wrapForKey(archiveKey, recipientPublicKey, suite, archiveId, opt
67
67
  const recipientRaw = rawPublicKey(recipientPublicKey);
68
68
  const ephemeralRaw = rawPublicKey(ephemeral.publicKey);
69
69
  const shared = diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipientPublicKey });
70
- const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([recipientRaw, ephemeralRaw]), Buffer.from("vaultline-recipient:v2"), KEY_BYTES));
70
+ const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([recipientRaw, ephemeralRaw]), Buffer.from("sealkeep-recipient:v2"), KEY_BYTES));
71
71
  const id = keyRecipientId(recipientRaw);
72
72
  try {
73
73
  return { type: "x25519", id, recipientPublicKey: b64(recipientRaw), ephemeralPublicKey: b64(ephemeralRaw), nonce: b64(options.nonce), ciphertext: b64(sealKey(suite, kek, options.nonce, archiveKey, wrapAad(archiveId, id))) };
@@ -112,7 +112,7 @@ export function unwrapArchiveKey(wrappedKeys, suite, archiveId, unlock) {
112
112
  if ("privateKey" in unlock && wrapped.type === "x25519") {
113
113
  const privateKey = typeof unlock.privateKey === "string" ? createPrivateKey(unlock.privateKey) : unlock.privateKey;
114
114
  const shared = diffieHellman({ privateKey, publicKey: x25519PublicKeyFromRaw(unb64(wrapped.ephemeralPublicKey)) });
115
- const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([unb64(wrapped.recipientPublicKey), unb64(wrapped.ephemeralPublicKey)]), Buffer.from("vaultline-recipient:v2"), KEY_BYTES));
115
+ const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([unb64(wrapped.recipientPublicKey), unb64(wrapped.ephemeralPublicKey)]), Buffer.from("sealkeep-recipient:v2"), KEY_BYTES));
116
116
  try {
117
117
  return openKey(suite, kek, unb64(wrapped.nonce), unb64(wrapped.ciphertext), wrapAad(archiveId, wrapped.id));
118
118
  }
@@ -0,0 +1,6 @@
1
+ export declare const squeezeStream: () => import("zlib").ZstdCompress;
2
+ export declare const unsqueezeStream: () => import("zlib").ZstdDecompress;
3
+ export declare const squeezeSync: (input: Buffer) => NonSharedBuffer;
4
+ export declare const unsqueezeSync: (input: Buffer, options?: {
5
+ maxOutputLength?: number;
6
+ }) => NonSharedBuffer;
@@ -0,0 +1,39 @@
1
+ import { constants as Z, createZstdCompress, createZstdDecompress, zstdCompressSync, zstdDecompressSync } from "node:zlib";
2
+ /**
3
+ * The compressor, and why it is not gzip.
4
+ *
5
+ * An agent transcript is not ordinary text. Every time the agent compacts its
6
+ * context it writes the whole conversation back to disk again — screenshots
7
+ * included — so one session file can hold the same 300 KB PNG fifty-odd times.
8
+ * Measured on a real 1.9 GB Codex session: 28 distinct images, each stored an
9
+ * average of 56 times, 90% of the file.
10
+ *
11
+ * gzip cannot see any of that. Its window is 32 KB, so a duplicate 300 KB image
12
+ * a hundred megabytes upstream is invisible and gets stored again in full; on
13
+ * that file it managed 1.5x. zstd with long-distance matching keeps a 128 MB
14
+ * window and collapses the repeats: the same file goes to 36 MB — 54x — and it
15
+ * does it faster than the gzip it replaces, because most of the input turns
16
+ * into a back-reference instead of being scanned.
17
+ *
18
+ * WINDOW_LOG is the whole trick and the one number worth guarding. At 4 MB
19
+ * chunks the same compressor gets 2x, because nothing repeats inside a 4 MB
20
+ * window; at whole-file scope it gets 59x. Compression here has to see the
21
+ * whole file, or it is not worth changing.
22
+ *
23
+ * Built into node:zlib since Node 22.15, so this costs no dependency and no
24
+ * new licence. Nothing decides to *read* zstd from these helpers: the archive's
25
+ * own envelope records which compressor sealed it, and gzip archives written
26
+ * before this existed keep opening through the gzip path, forever.
27
+ */
28
+ const WINDOW_LOG = 27;
29
+ const COMPRESS = {
30
+ [Z.ZSTD_c_compressionLevel]: 9,
31
+ [Z.ZSTD_c_enableLongDistanceMatching]: 1,
32
+ [Z.ZSTD_c_windowLog]: WINDOW_LOG,
33
+ };
34
+ /** Readers must be told to allocate the same window the writer used. */
35
+ const DECOMPRESS = { [Z.ZSTD_d_windowLogMax]: WINDOW_LOG };
36
+ export const squeezeStream = () => createZstdCompress({ params: { ...COMPRESS } });
37
+ export const unsqueezeStream = () => createZstdDecompress({ params: { ...DECOMPRESS } });
38
+ export const squeezeSync = (input) => zstdCompressSync(input, { params: { ...COMPRESS } });
39
+ export const unsqueezeSync = (input, options = {}) => zstdDecompressSync(input, { ...options, params: { ...DECOMPRESS } });
@@ -36,6 +36,20 @@ export type StreamEncryptOptions = EncryptOptions & {
36
36
  scratchDir?: string;
37
37
  /** Called as the source is read, with cumulative bytes so far — a person watching a 1.6 GB seal deserves a number that moves. Throttling is the caller's business. */
38
38
  onProgress?: (originalBytes: number) => void;
39
+ /** Cancels source reads, compression, sink writes and unpublished output. */
40
+ signal?: AbortSignal;
41
+ /**
42
+ * Cooperative scheduling checkpoint for low-priority callers. It is awaited
43
+ * after each bounded amount of compression/encryption work, so a streaming
44
+ * seal cannot monopolise a core merely because its memory use is bounded.
45
+ */
46
+ onYield?: () => void | Promise<void>;
47
+ /**
48
+ * Each chunk's plaintext, as it is sealed (chunk-compressed layouts only —
49
+ * a whole-file body has no chunks to report). The caller that indexes what
50
+ * it seals reads the source once, here, instead of decrypting it again.
51
+ */
52
+ onPlaintext?: (chunkIndex: number, slice: Buffer) => void;
39
53
  };
40
54
  export type SealResult = {
41
55
  envelope: Envelope;
@@ -58,6 +72,16 @@ export type OpenResult = {
58
72
  bytes: number;
59
73
  sha256: string;
60
74
  };
75
+ export type StreamOpenOptions = {
76
+ /** Reject before allocating the reusable ciphertext buffer if any stored chunk exceeds this many bytes. */
77
+ maxStoredChunkBytes?: number;
78
+ /** Reject both a larger declared original and actual decompression output beyond this many bytes. */
79
+ maxOriginalBytes?: number;
80
+ /** Reject one independently-compressed chunk after this many expanded bytes. */
81
+ maxExpandedChunkBytes?: number;
82
+ /** Expand independently-compressed chunks through a stream instead of a whole-chunk sync allocation. */
83
+ streamChunkDecompression?: boolean;
84
+ };
61
85
  /**
62
86
  * Opens an archive straight to a file, holding one chunk at a time.
63
87
  *
@@ -68,7 +92,16 @@ export type OpenResult = {
68
92
  * a rejection can leave a partial file behind, which is why `restoreArchive`
69
93
  * renames into place rather than writing the destination directly.
70
94
  */
71
- export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: string, destPath: string, unlock: Unlock): Promise<OpenResult>;
95
+ /**
96
+ * `destPath` may be a Writable instead of a path. Verification needs to prove an
97
+ * archive decrypts to its recorded bytes, and materialising the plaintext to do
98
+ * so costs as much free disk as the original — on the nearly-full disk that
99
+ * makes someone reach for this product in the first place, that is exactly the
100
+ * space they do not have, so the proof became impossible precisely when it
101
+ * mattered. Every integrity check below runs the same either way; only the
102
+ * destination changes.
103
+ */
104
+ export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: string, destPath: string | NodeJS.WritableStream, unlock: Unlock, options?: StreamOpenOptions): Promise<OpenResult>;
72
105
  /**
73
106
  * SHA-256 of several prefixes of a file, in one read and in constant memory.
74
107
  *
@@ -80,7 +113,7 @@ export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: st
80
113
  *
81
114
  * An offset past the end of the file is simply absent from the result.
82
115
  */
83
- export declare function hashFilePrefixes(path: string, offsets: readonly number[]): Promise<Map<number, string>>;
116
+ export declare function hashFilePrefixes(path: string, offsets: readonly number[], onProgress?: (bytesRead: number) => void, signal?: AbortSignal, onYield?: () => void | Promise<void>): Promise<Map<number, string>>;
84
117
  /**
85
118
  * SHA-256 of a byte range of a file, read in constant memory.
86
119
  *
@@ -88,7 +121,7 @@ export declare function hashFilePrefixes(path: string, offsets: readonly number[
88
121
  * live file and comparing it with what an archive recorded is the only evidence
89
122
  * that the archive's bytes are still a prefix of it.
90
123
  */
91
- export declare function hashFileRange(path: string, start?: number, end?: number): Promise<{
124
+ export declare function hashFileRange(path: string, start?: number, end?: number, onProgress?: (bytesRead: number) => void, signal?: AbortSignal, onYield?: () => void | Promise<void>): Promise<{
92
125
  sha256: string;
93
126
  bytes: number;
94
127
  }>;