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
package/README.md CHANGED
@@ -15,14 +15,18 @@ sealkeep autopilot
15
15
  ```
16
16
 
17
17
  That is the whole setup. It creates your vault, shows your recovery phrase once, finds the
18
- sessions already on this machine, and installs a background service that starts at login.
18
+ sessions already on this machine, installs the agent memory hooks, and installs a background
19
+ service that starts at login.
19
20
 
20
- From then on you do nothing. New sessions are sealed as they finish. Once you point it at a
21
- bucket, verified copies go up and old local files move to your trash on their own.
21
+ From then on you do nothing. New sessions are sealed, indexed, and uploaded as they finish;
22
+ their original files stay on this machine by default. If you explicitly opt into reclaim,
23
+ verified and searchable old local originals can be permanently reclaimed after every idle,
24
+ grace, identity, and liveness check passes.
22
25
 
23
26
  ```sh
27
+ sealkeep autopilot --reclaim # explicitly allow safe background disk reclaim on this machine
24
28
  sealkeep autopilot status # is it alive, and what has it done
25
- sealkeep autopilot off # stop it — every archive stays
29
+ sealkeep autopilot off # stop it, remove Sealkeep's hooks — every archive stays
26
30
  ```
27
31
 
28
32
  It tells you it is working rather than leaving you to wonder. The service writes a
@@ -63,10 +67,15 @@ a laptop rewraps keys rather than re-encrypting your data.
63
67
  count and checksum must match what was computed locally, or the archive is not marked
64
68
  durable and your local file stays exactly where it is.
65
69
 
66
- **4. Reclaim** — only a verified, idle, out-of-grace-period session becomes eligible, and
67
- then it moves to your trash. There is no code path in Sealkeep that deletes a file.
70
+ **4. Reclaim** — only a remotely verified, searchable, idle, out-of-grace-period session
71
+ whose source still exactly matches its archive becomes eligible. With explicit or opt-in
72
+ automatic approval, Sealkeep durably journals the change, leaves Codex a tiny same-session
73
+ resume pointer, and permanently removes the staged full original. Before the purge decision,
74
+ any failure restores the original; after it, restart reconciliation finishes the committed purge.
68
75
 
69
- Autopilot runs all four on a loop. To unlock the vault without a human present, it keeps
76
+ Autopilot always runs Catch, Seal, and Verify on a loop. Reclaim joins that loop only
77
+ after you explicitly opt this machine in with `--reclaim` or the local Settings switch.
78
+ To unlock the vault without a human present, it keeps
70
79
  your phrase in the OS keystore — macOS Keychain, Freedesktop Secret Service, or Windows
71
80
  DPAPI. That is a real trade-off, stated plainly: anything running as you can read it once
72
81
  you are logged in. Opt out with `--no-remember` and supply
@@ -146,19 +155,20 @@ it stores archives as single objects, and says so.
146
155
 
147
156
  | | |
148
157
  | --- | --- |
149
- | `sealkeep autopilot` | Set it up once and let it run itself |
158
+ | `sealkeep autopilot` | Preserve, index, and upload automatically; keep original sessions by default |
159
+ | `sealkeep autopilot --reclaim` | Explicitly allow this machine to reclaim eligible originals |
150
160
  | `sealkeep autopilot status` · `off` | Check on it, or stop it |
151
161
  | `sealkeep quickstart` | Same setup, no background service |
152
162
  | `sealkeep status` · `doctor` | What is archived; whether this machine is healthy |
153
163
  | `sealkeep queue run` | Encrypt everything waiting |
154
- | `sealkeep daemon` | Watch, seal, upload, and reclaim continuously |
164
+ | `sealkeep daemon` | Watch, seal, index, and upload continuously; reclaim only when enabled |
155
165
  | `sealkeep search <query>` | Metadata search; `--content` searches inside sessions |
156
166
  | `sealkeep recover <id> <dest>` | Restore original bytes; `--native` puts them back where they came from |
157
167
  | `sealkeep recover <filename>` | The file an agent's resume just complained about, back in place |
158
168
  | `sealkeep archive <path> --stream` | Seal straight to the bucket as a chunk folder, resumable mid-upload |
159
169
  | `sealkeep share <id>` | A sealed bundle under a one-time passcode — the phrase never travels |
160
170
  | `sealkeep mcp install` | Register the vault as a tool server with Claude Code and Codex |
161
- | `sealkeep retention apply` | Dry run; `--confirm` moves sources to the trash |
171
+ | `sealkeep retention apply` | Dry run; `--confirm` permanently frees verified local originals |
162
172
  | `sealkeep desktop` · `tui` | The dashboard, in a browser or the terminal |
163
173
  | `sealkeep audit` | Everything Sealkeep has done to your files |
164
174
 
@@ -171,13 +181,86 @@ Codex, so the agent itself can search your sealed history, check vault
171
181
  status, and restore a session it needs — six tools, all local, nothing new
172
182
  exposed to the network.
173
183
 
184
+ Ordinary work does not depend on the agent remembering to call that tool.
185
+ The installed lifecycle hooks identify the current Git project and inject a
186
+ small, labelled context automatically:
187
+
188
+ - **One developer, several machines.** A finished Mac session is sealed and
189
+ its encrypted index syncs in the background. When the same developer opens
190
+ the project on a VPS, the newest preserved handoffs, commits, and touched
191
+ files are prepared automatically—even if the prompt only says “continue.”
192
+ - **Independent accounts working on one project.** One person enables Team mode
193
+ for the project in the Shared page and invites teammates by account email.
194
+ Each machine receives only its own wrapped copy of the random project feed
195
+ key. The background service automatically holds one authenticated realtime
196
+ connection and multiplexes every bound project over it. Normal Claude Code
197
+ and Codex lifecycle hooks publish encrypted start/progress/finish events;
198
+ push writes them to a bounded local inbox, and the next safe agent boundary
199
+ consumes them without polling. HTTP cursor reads happen only at cold start,
200
+ after reconnect, or while push is unhealthy, so an API contract chosen on
201
+ one machine reaches the other invited accounts without sharing a recovery
202
+ phrase or running a Sealkeep command.
203
+
204
+ Recall and cooperation use two hook lanes. The agent-facing lane only claims
205
+ and decrypts already-prepared local context, with a two-second fail-open limit;
206
+ it never searches an archive or waits for the network. A companion hook runs
207
+ asynchronously after startup, prompts, and meaningful tools: it publishes the
208
+ current task first, reconciles the encrypted team inbox, refreshes/searches the
209
+ memory index, then seals the result for the next safe agent boundary. Usually
210
+ startup preparation is ready for the first prompt. If a provider is slow, the
211
+ agent continues immediately and receives the consistent result at a later safe
212
+ boundary instead of freezing the person's work.
213
+
214
+ The hosted realtime route is itself a 256-bit opaque capability derived from
215
+ the team space. A signed-in client that somehow obtains that id can observe
216
+ traffic timing and ciphertext, but cannot publish through the membership-
217
+ checked endpoint or decrypt the feed without a wrapped project key. Durable
218
+ reads and writes always enforce membership. Every displayed team event also
219
+ shows the server-authenticated account id; its friendly label and text remain
220
+ explicitly untrusted because a legitimate member chooses both.
221
+
222
+ There are no daily `sealkeep search`, `sealkeep team`, or bridge commands in
223
+ that flow. Pairing the machine, choosing whether a project is active, and the
224
+ agent's normal one-time hook trust review are setup/security decisions; after
225
+ that, capture, recall, and exchange are background behavior. Recalled text is
226
+ always labelled as untrusted historical data and current facts are checked
227
+ against Git.
228
+
229
+ Autopilot-owned background integrations upgrade automatically on macOS and
230
+ Linux. Foreground `ui`, `desktop`, and `api` processes must be restarted after
231
+ an upgrade; they are not part of the background-service promise.
232
+ On a systemd Linux host, Autopilot also enables lingering for the current
233
+ numeric user when the host permits it, so the user service starts at boot and
234
+ survives the last SSH logout. Sealkeep records whether it changed that
235
+ user-wide setting and reverses only its own change when Autopilot is turned
236
+ off. If `loginctl` is unavailable or policy refuses the change, preservation
237
+ continues for the current login and resumes at the next one; setup reports the
238
+ limitation instead of blocking or silently claiming reboot persistence.
239
+ On its first start after an upgrade, the background service reconciles only
240
+ lifecycle hooks that Sealkeep already owns;
241
+ unrelated settings and user hooks are retained and the pre-edit file is backed
242
+ up. It never opts a previously unhooked agent into integration. Running
243
+ `sealkeep autopilot off` removes those owned hooks while keeping the vault and
244
+ every archive. A disclosed, package-independent integration manager also stays
245
+ next to the vault while Autopilot is enabled. If the npm package remains absent
246
+ for five minutes after a raw package-manager uninstall, it removes only the
247
+ owned service and hook entries, writes a cleanup receipt, and preserves sealed
248
+ archives, the outbox, recovery material, and remote objects.
249
+
250
+ Recovery material is kept in the strongest backend that proves it can perform
251
+ a real reversible write on that machine: macOS Keychain, Linux Secret Service,
252
+ modern systemd user-scoped encrypted credentials on a headless Linux host, or
253
+ Windows DPAPI. If none is usable, setup names the owner-only file fallback
254
+ honestly; the native reboot rehearsal refuses that fallback.
255
+
174
256
  And the failure this whole product could have caused is handled where it
175
257
  would happen: resuming a session whose transcript was archived and reclaimed.
176
- A `SessionStart` hook notices the missing file and restores it — exact path,
177
- exact bytes — before the agent reads it, so the resume proceeds as if the
178
- file had never left. If anything is ever missing anyway, `sealkeep recover
179
- <filename>` with the filename from the agent's error puts it back. Archiving
180
- must never cost you a resume.
258
+ Codex keeps a tiny valid native pointer under the same session id. The resumed
259
+ agent searches relevant preserved memory through MCP without downloading the
260
+ full transcript. For an explicit full restore, run `sealkeep recover
261
+ <archive-id> --native --overwrite backup` and resume the same id again. The
262
+ backup policy preserves anything already at the native path; no SessionStart
263
+ hook automatically replaces an open transcript or downloads gigabytes.
181
264
 
182
265
  ## Your storage
183
266
 
@@ -204,7 +287,7 @@ Format v2: a random per-archive key encrypts the session in chunks under ChaCha2
204
287
  authenticated over its own position, the archive's identity, and the total chunk count — so
205
288
  reordering, truncation, splicing, and suite downgrade are all detectable rather than silent.
206
289
 
207
- The format lives in [`packages/vaultline-crypto/`](./packages/vaultline-crypto/README.md)
290
+ The format lives in [`packages/sealkeep-crypto/`](./packages/sealkeep-crypto/README.md)
208
291
  with deterministic test vectors and a tamper suite that flips every byte of a stored archive
209
292
  in turn. Assumptions and limits are in [`THREAT_MODEL.md`](./THREAT_MODEL.md).
210
293
 
@@ -216,20 +299,20 @@ built on published primitives — not an audited end-to-end-encryption product.
216
299
  | Area | State |
217
300
  | --- | --- |
218
301
  | Capture and sealing | **Shipping** — durable queue, crash-safe, deduplicating, resumable streaming |
219
- | Restore | **Shipping** — byte-identical, verified twice, never overwrites; rehydrates on agent resume |
220
- | Retention | **Shipping** — trash-staged, never deletes |
302
+ | Restore | **Shipping** — byte-identical, verified twice, refuses to overwrite unless told; a reclaimed-session pointer supports MCP search and explicit safe full restore |
303
+ | Retention | **Shipping** — opt-in, transaction-staged permanent reclaim; pre-commit failures restore and committed crashes reconcile |
221
304
  | Search | **Shipping** — sealed content index, synced across your machines as ciphertext |
222
- | Agent integration | **Shipping** — MCP server for Claude Code and Codex, session hooks |
305
+ | Agent integration | **Shipping** — MCP server plus automatic project recall and active-team lifecycle hooks for Claude Code and Codex |
223
306
  | Encryption format | **Unaudited** — vectors and tamper suite, no third-party audit yet |
224
- | Cloud providers | **GCS live in production**; S3, R2, and Drive verified against signature vectors and emulators, without a long production soak yet |
307
+ | Cloud providers | **R2 live in production** (managed storage); S3, GCS, and Drive verified against signature vectors and emulators, without a long production soak yet |
225
308
  | Signed binaries | **Not yet** — npm provenance and a signed manifest; no OS code-signing certificate |
226
309
  | Distribution | **Published** — `npm install -g sealkeep` |
227
310
 
228
311
  ## Self-hosting the control plane
229
312
 
230
313
  ```sh
231
- docker build -t vaultline-control-plane .
232
- docker run -p 8787:8787 -v vaultline-data:/data vaultline-control-plane
314
+ docker build -t sealkeep-control-plane .
315
+ docker run -p 8787:8787 -v sealkeep-data:/data sealkeep-control-plane
233
316
  ```
234
317
 
235
318
  Devices authenticate with Ed25519 request signatures, leases are single-use and expiring,
@@ -241,6 +324,9 @@ and revocation blocks new leases without touching archives. See
241
324
  ```sh
242
325
  npm test # the full suite — crypto vectors, a tamper suite, provider emulators, dashboard rendering
243
326
  npm run build
327
+ npm run rehearsal:install-upgrade # disposable global install plus real 0.9.0 → current upgrade
328
+ npm run rehearsal:automatic-memory # real Mac/VPS Codex; needs documented QA account + SSH VPS env vars
329
+ npm run rehearsal:team-realtime # four real Codex actors; isolated local control-plane lab
244
330
  ```
245
331
 
246
332
  Architecture is in [`ARCHITECTURE.md`](./ARCHITECTURE.md); designs and operations live in
package/THIRD_PARTY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Third-party components
2
2
 
3
- Vaultline ships with the following third-party components, each under its own
3
+ Sealkeep ships with the following third-party components, each under its own
4
4
  licence (see §8 of LICENSE). Nothing else in the published package is
5
5
  third-party code.
6
6
 
@@ -9,7 +9,7 @@ third-party code.
9
9
  - **zod** — TypeScript schema validation.
10
10
  Licence: MIT. https://github.com/colinhacks/zod
11
11
  - **@modelcontextprotocol/sdk** — Model Context Protocol SDK, used by
12
- `vaultline mcp`.
12
+ `sealkeep mcp`.
13
13
  Licence: MIT. https://github.com/modelcontextprotocol/typescript-sdk
14
14
 
15
15
  ## Vendored data
package/THREAT_MODEL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sealkeep Threat Model
2
2
 
3
- **Scope:** the `vaultline-crypto` archive envelope (format v2) and the client that produces it.
3
+ **Scope:** the `sealkeep-crypto` archive envelope (format v2) and the client that produces it.
4
4
  **Status:** written by the implementing engineers. **Not yet independently audited.** See "Open gates".
5
5
 
6
6
  ## 1. What the system protects
@@ -56,7 +56,7 @@ are separately bound to archive id and suite. Consequently these are all detecte
56
56
 
57
57
  Out of scope for confidentiality: an attacker running as the user can read the plaintext
58
58
  transcripts directly from the agent's own directories. Sealkeep reduces blast radius by
59
- keeping the recovery phrase out of files (it is passed by environment to one command), storing
59
+ keeping the recovery phrase out of files (today it is also accepted as a `--recovery-phrase` argument, which lands in shell history and the process list — prefer the keystore or the environment variable; tightening this is an open item), storing
60
60
  its own state at mode `600`, and never writing the phrase to logs, queue jobs, or the API.
61
61
 
62
62
  ### 3.5 Lost or stolen device
@@ -79,6 +79,25 @@ The MCP server is local but model-driven. Search returns metadata only. Archive
79
79
  require the recovery phrase from the local environment, recover refuses to overwrite by default,
80
80
  and native restore refuses any path outside the adapter's own transcript roots.
81
81
 
82
+ ### 3.8 One-time Backup Recovery Kit
83
+
84
+ The ordinary escrow record contains only the permanent 24-word phrase wrap. Each of the five
85
+ backup codes has a separate encrypted capsule, indexed by the first 128 bits of SHA-256 over the
86
+ canonical 128-bit code. The service therefore sees an opaque, non-enumerable locator but never
87
+ the code, phrase, archive key, or plaintext vault material.
88
+
89
+ Redeeming a capsule and recording its random per-machine claim happen in one database
90
+ transaction, scoped to the authenticated account. A different claim receives no capsule. If the
91
+ winning response is lost, only that same claim can retrieve the capsule. Replacing the kit
92
+ atomically removes both the active capsules and their consumption rows before installing the new
93
+ set. The client checks that the target has no physical archive records before redemption and does
94
+ not decrypt or persist permanent vault material until Cloud has awarded the capsule.
95
+
96
+ The control plane can still deny recovery or destroy a capsule. It cannot use a stored capsule
97
+ without the printed code, and a database copy cannot replay a spent code through the service.
98
+ As with any bearer recovery secret, anyone who copies an unused printed code and can sign in to
99
+ the same account may race its owner; exactly one claim wins.
100
+
82
101
  ## 4. Cryptographic design
83
102
 
84
103
  - **Suite:** ChaCha20-Poly1305 (default) or AES-256-GCM, both from OpenSSL via Node. No custom primitive.
@@ -95,11 +114,11 @@ and native restore refuses any path outside the adapter's own transcript roots.
95
114
  3. **No forward secrecy for the phrase.** Anyone who learns the recovery phrase can open every archive it was a recipient for, including old ones.
96
115
  4. **Zeroization is best effort.** JavaScript may retain copies of buffers the implementation cannot reach.
97
116
  5. **scrypt parameters are modest** so that a hook-driven flow stays responsive; they are recorded per archive and can be raised without breaking existing archives.
98
- 6. **No streaming.** Archives are encrypted from an in-memory buffer, which bounds practical archive size to available memory.
117
+ 6. **Streaming.** Archives seal through a chunked stream (64 MB windows); neither sealing nor verification holds a whole archive in memory.
99
118
 
100
119
  ## 6. Open gates before an end-to-end-encryption claim
101
120
 
102
- - [ ] Independent third-party cryptographic audit of `vaultline-crypto` and this document.
121
+ - [ ] Independent third-party cryptographic audit of `sealkeep-crypto` and this document.
103
122
  - [ ] Publication of the package under a public repository with the test vectors.
104
123
  - [ ] Review of the key lifecycle by someone outside the implementing team.
105
124
 
@@ -22,6 +22,26 @@ export type ChunkWindow = {
22
22
  rawStartInWindow: number;
23
23
  rawLengthInWindow: number;
24
24
  };
25
+ export type ChunkAccessOptions = {
26
+ /** Stored bytes in this window, including chunks that pad the requested range. */
27
+ maxStoredBytes?: number;
28
+ /** All expanded bytes in this window, including padding, not just returned bytes. */
29
+ maxExpandedBytes?: number;
30
+ maxExpandedChunkBytes?: number;
31
+ /** Checked before/after synchronous decompression and between chunks. */
32
+ signal?: AbortSignal;
33
+ /**
34
+ * The archive key of this envelope, already unwrapped by the caller (see
35
+ * `unwrapWindowKey`). Unwrapping a phrase recipient is an scrypt derivation
36
+ * of ~70 ms; a reader that opens dozens of windows of one sealed file pays
37
+ * it once instead of per window. The caller owns the key's lifetime and
38
+ * zeroization — it is never zeroized here.
39
+ */
40
+ archiveKey?: Buffer;
41
+ };
42
+ /** The archive key `openChunkWindow` would unwrap for every window; unwrap it
43
+ * once, pass it as `options.archiveKey`, and zeroize it when done. */
44
+ export declare function unwrapWindowKey(envelope: Envelope, unlock: Unlock): Buffer;
25
45
  /** True when an envelope supports random access at all. */
26
46
  export declare function supportsChunkAccess(envelope: Envelope): boolean;
27
47
  /**
@@ -32,8 +52,10 @@ export declare function supportsChunkAccess(envelope: Envelope): boolean;
32
52
  export declare function chunkWindowForRange(envelope: Envelope, rawStart: number, rawEnd: number): ChunkWindow;
33
53
  /**
34
54
  * Opens the chunks inside a fetched stored slice and returns exactly the raw
35
- * bytes the window was asked for. Every chunk is hash-checked and
36
- * authenticated before a byte of it is trusted — a ranged read keeps the
37
- * whole-archive guarantees, chunk by chunk.
55
+ * bytes the window was asked for. The caller MUST authenticate the complete
56
+ * envelope and source-to-range mapping before using this primitive: v2 chunk
57
+ * AAD does not bind rawBytes or the codec, and a partial read cannot verify the
58
+ * whole-source hash or the lengths of chunks outside the window. The checks
59
+ * here prove selected ciphertext integrity and exact expanded chunk lengths.
38
60
  */
39
- export declare function openChunkWindow(envelope: Envelope, window: ChunkWindow, storedSlice: Buffer, unlock: Unlock): Buffer;
61
+ export declare function openChunkWindow(envelope: Envelope, window: ChunkWindow, storedSlice: Buffer, unlock: Unlock, options?: ChunkAccessOptions): Buffer;
@@ -0,0 +1,219 @@
1
+ import { createHash } from "node:crypto";
2
+ import { gunzipSync } from "node:zlib";
3
+ import { unsqueezeSync } from "./squeeze.js";
4
+ import { aeadDecipher } from "./aead.js";
5
+ import { validateEnvelope } from "./envelope.js";
6
+ import { chunkAad, TAG_BYTES } from "./format.js";
7
+ import { CryptoError, unwrapArchiveKey, zeroize } from "./recipients.js";
8
+ const sha256 = (input) => createHash("sha256").update(input).digest("hex");
9
+ /** The archive key `openChunkWindow` would unwrap for every window; unwrap it
10
+ * once, pass it as `options.archiveKey`, and zeroize it when done. */
11
+ export function unwrapWindowKey(envelope, unlock) {
12
+ return unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
13
+ }
14
+ const DEFAULT_WINDOW_BYTES = 128 * 1024 * 1024;
15
+ const DEFAULT_EXPANDED_CHUNK_BYTES = 64 * 1024 * 1024;
16
+ const nonnegativeInteger = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
17
+ function checkedAdd(left, right) {
18
+ const total = left + right;
19
+ if (!nonnegativeInteger(total))
20
+ throw new CryptoError("envelope_malformed", "Chunk lengths exceed the safe integer range");
21
+ return total;
22
+ }
23
+ /** True when an envelope supports random access at all. */
24
+ export function supportsChunkAccess(envelope) {
25
+ // Both chunk codecs qualify. What makes ranged access possible is that each
26
+ // chunk compresses and encrypts ALONE and records its own raw length — that
27
+ // is true of zstd-chunk exactly as it is of gzip-chunk. Naming only gzip here
28
+ // silently disqualified every zstd-chunk archive: they were built chunk-wise
29
+ // precisely so search could quote a line without decompressing a gigabyte,
30
+ // and then reported as un-rangeable, so search decompressed the gigabyte.
31
+ const codec = envelope?.manifest?.compression;
32
+ return (codec === "gzip-chunk" || codec === "zstd-chunk") && Array.isArray(envelope.chunks)
33
+ && envelope.chunks.length > 0 && envelope.chunks.every((chunk) => chunk && nonnegativeInteger(chunk.rawBytes));
34
+ }
35
+ /** Structural validation is not authentication of the envelope or its raw offsets. */
36
+ function validateChunkLayout(envelope, signal) {
37
+ if (!supportsChunkAccess(envelope)) {
38
+ throw new CryptoError("envelope_malformed", "This archive has no valid chunk-independent layout; for a legacy archive, restore it whole instead of by range.");
39
+ }
40
+ if (!nonnegativeInteger(envelope.manifest.originalBytes)
41
+ || !nonnegativeInteger(envelope.manifest.plaintextBytes)) {
42
+ throw new CryptoError("envelope_malformed", "The chunk manifest has invalid byte lengths");
43
+ }
44
+ let rawBytes = 0;
45
+ let storedBytes = 0;
46
+ let plaintextBytes = 0;
47
+ for (const chunk of envelope.chunks) {
48
+ signal?.throwIfAborted();
49
+ if (!nonnegativeInteger(chunk.storedBytes) || !nonnegativeInteger(chunk.plaintextBytes)) {
50
+ throw new CryptoError("envelope_malformed", "A chunk has invalid byte lengths");
51
+ }
52
+ rawBytes = checkedAdd(rawBytes, chunk.rawBytes);
53
+ storedBytes = checkedAdd(storedBytes, chunk.storedBytes);
54
+ plaintextBytes = checkedAdd(plaintextBytes, chunk.plaintextBytes);
55
+ }
56
+ if (rawBytes !== envelope.manifest.originalBytes || plaintextBytes !== envelope.manifest.plaintextBytes
57
+ || envelope.chunks.some((chunk) => chunk.rawBytes === 0 && (rawBytes !== 0 || envelope.chunks.length !== 1))) {
58
+ throw new CryptoError("envelope_malformed", "Chunk lengths do not describe the manifest's original bytes");
59
+ }
60
+ validateEnvelope(envelope, storedBytes);
61
+ }
62
+ /**
63
+ * Maps a raw (original-file) byte range to the chunk window that covers it.
64
+ * Legacy whole-body-gzip archives cannot answer this — a slice of their stream
65
+ * is undecodable alone — and the error says to restore whole instead.
66
+ */
67
+ export function chunkWindowForRange(envelope, rawStart, rawEnd) {
68
+ validateChunkLayout(envelope);
69
+ return windowForValidatedRange(envelope, rawStart, rawEnd);
70
+ }
71
+ function windowForValidatedRange(envelope, rawStart, rawEnd) {
72
+ const total = envelope.manifest.originalBytes;
73
+ // An empty archive still has one authenticated compressed chunk. Explicitly
74
+ // reading its empty interval must verify that chunk rather than skip AEAD.
75
+ if (total === 0 && rawStart === 0 && rawEnd === 0) {
76
+ return { firstIndex: 0, lastIndex: 0, storedOffset: 0, storedLength: envelope.chunks[0].storedBytes,
77
+ rawStartInWindow: 0, rawLengthInWindow: 0 };
78
+ }
79
+ if (!nonnegativeInteger(rawStart) || !nonnegativeInteger(rawEnd) || rawEnd <= rawStart || rawStart >= total) {
80
+ throw new CryptoError("invalid_range", `Range ${rawStart}..${rawEnd} is not a range of this ${total}-byte archive`);
81
+ }
82
+ const end = Math.min(rawEnd, total);
83
+ let rawCursor = 0;
84
+ let storedCursor = 0;
85
+ let firstIndex = -1;
86
+ let lastIndex = -1;
87
+ let storedOffset = 0;
88
+ let storedLength = 0;
89
+ let rawStartInWindow = 0;
90
+ for (const chunk of envelope.chunks) {
91
+ const rawFrom = rawCursor;
92
+ const rawTo = rawCursor + (chunk.rawBytes ?? 0);
93
+ const overlaps = rawTo > rawStart && rawFrom < end;
94
+ if (overlaps && firstIndex === -1) {
95
+ firstIndex = chunk.index;
96
+ storedOffset = storedCursor;
97
+ rawStartInWindow = rawStart - rawFrom;
98
+ }
99
+ if (overlaps) {
100
+ lastIndex = chunk.index;
101
+ storedLength += chunk.storedBytes;
102
+ }
103
+ rawCursor = rawTo;
104
+ storedCursor += chunk.storedBytes;
105
+ }
106
+ return { firstIndex, lastIndex, storedOffset, storedLength, rawStartInWindow, rawLengthInWindow: end - rawStart };
107
+ }
108
+ /**
109
+ * Opens the chunks inside a fetched stored slice and returns exactly the raw
110
+ * bytes the window was asked for. The caller MUST authenticate the complete
111
+ * envelope and source-to-range mapping before using this primitive: v2 chunk
112
+ * AAD does not bind rawBytes or the codec, and a partial read cannot verify the
113
+ * whole-source hash or the lengths of chunks outside the window. The checks
114
+ * here prove selected ciphertext integrity and exact expanded chunk lengths.
115
+ */
116
+ export function openChunkWindow(envelope, window, storedSlice, unlock, options = {}) {
117
+ options.signal?.throwIfAborted();
118
+ validateChunkLayout(envelope, options.signal);
119
+ const maxStoredBytes = options.maxStoredBytes ?? DEFAULT_WINDOW_BYTES;
120
+ const maxExpandedBytes = options.maxExpandedBytes ?? DEFAULT_WINDOW_BYTES;
121
+ const maxExpandedChunkBytes = options.maxExpandedChunkBytes ?? DEFAULT_EXPANDED_CHUNK_BYTES;
122
+ for (const [name, limit] of Object.entries({ maxStoredBytes, maxExpandedBytes, maxExpandedChunkBytes })) {
123
+ if (!nonnegativeInteger(limit))
124
+ throw new CryptoError("resource_limit_invalid", `${name} must be a non-negative safe integer`);
125
+ }
126
+ const fields = ["firstIndex", "lastIndex", "storedOffset", "storedLength", "rawStartInWindow", "rawLengthInWindow"];
127
+ if (!window || fields.some((field) => !nonnegativeInteger(window[field]))
128
+ || window.firstIndex > window.lastIndex || window.lastIndex >= envelope.chunks.length) {
129
+ throw new CryptoError("invalid_range", "The requested chunk window has invalid bounds");
130
+ }
131
+ let rawBeforeWindow = 0;
132
+ for (let index = 0; index < window.firstIndex; index += 1)
133
+ rawBeforeWindow += envelope.chunks[index].rawBytes;
134
+ const rawStart = rawBeforeWindow + window.rawStartInWindow;
135
+ const rawEnd = rawStart + window.rawLengthInWindow;
136
+ const expected = windowForValidatedRange(envelope, rawStart, rawEnd);
137
+ if (fields.some((field) => window[field] !== expected[field])) {
138
+ throw new CryptoError("invalid_range", "The requested chunk window does not match its declared raw range");
139
+ }
140
+ if (storedSlice.length !== window.storedLength) {
141
+ throw new CryptoError("ciphertext_length_mismatch", `The fetched slice is ${storedSlice.length} bytes; the window needs ${window.storedLength}`);
142
+ }
143
+ if (window.storedLength > maxStoredBytes)
144
+ throw new CryptoError("resource_limit_exceeded", "The chunk window exceeds its stored byte limit");
145
+ let expandedBytes = 0;
146
+ for (let index = window.firstIndex; index <= window.lastIndex; index += 1) {
147
+ const rawBytes = envelope.chunks[index].rawBytes;
148
+ expandedBytes += rawBytes;
149
+ if (rawBytes > maxExpandedChunkBytes || expandedBytes > maxExpandedBytes) {
150
+ throw new CryptoError("resource_limit_exceeded", "The chunk window exceeds its expanded byte limit, including padding");
151
+ }
152
+ }
153
+ options.signal?.throwIfAborted();
154
+ const ownsKey = options.archiveKey === undefined;
155
+ const archiveKey = options.archiveKey ?? unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
156
+ let result;
157
+ try {
158
+ result = Buffer.allocUnsafe(window.rawLengthInWindow);
159
+ let offset = 0;
160
+ let rawOffset = 0;
161
+ let written = 0;
162
+ for (let index = window.firstIndex; index <= window.lastIndex; index += 1) {
163
+ options.signal?.throwIfAborted();
164
+ const chunk = envelope.chunks[index];
165
+ const stored = storedSlice.subarray(offset, offset + chunk.storedBytes);
166
+ offset += chunk.storedBytes;
167
+ if (sha256(stored) !== chunk.sha256)
168
+ throw new CryptoError("chunk_hash_mismatch", `Chunk ${chunk.index} does not match its recorded hash`);
169
+ const decipher = aeadDecipher(envelope.suite, archiveKey, Buffer.from(chunk.nonce, "base64"));
170
+ decipher.setAAD(chunkAad({ archiveId: envelope.archiveId, suite: envelope.suite, index: chunk.index, totalChunks: envelope.chunks.length, plaintextBytes: chunk.plaintextBytes, final: chunk.final }));
171
+ decipher.setAuthTag(stored.subarray(stored.length - TAG_BYTES));
172
+ let packed;
173
+ try {
174
+ packed = Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]);
175
+ }
176
+ catch {
177
+ throw new CryptoError("chunk_auth_failed", `Chunk ${chunk.index} failed authentication`);
178
+ }
179
+ let raw;
180
+ try {
181
+ const expand = envelope.manifest.compression === "zstd-chunk" ? unsqueezeSync : gunzipSync;
182
+ // Native output bounds stop expansion during decompression. zlib
183
+ // requires a positive bound even for the one valid empty chunk.
184
+ try {
185
+ raw = expand(packed, { maxOutputLength: Math.max(1, chunk.rawBytes) });
186
+ }
187
+ catch (error) {
188
+ if (error.code === "ERR_BUFFER_TOO_LARGE") {
189
+ throw new CryptoError("plaintext_length_mismatch", `Chunk ${chunk.index} exceeds its declared original length`);
190
+ }
191
+ throw new CryptoError("plaintext_hash_mismatch", `Chunk ${chunk.index} failed decompression after authentication`);
192
+ }
193
+ options.signal?.throwIfAborted();
194
+ if (raw.length !== chunk.rawBytes)
195
+ throw new CryptoError("plaintext_length_mismatch", `Chunk ${chunk.index} does not match its declared original length`);
196
+ const from = Math.max(0, window.rawStartInWindow - rawOffset);
197
+ const to = Math.min(raw.length, window.rawStartInWindow + window.rawLengthInWindow - rawOffset);
198
+ if (to > from)
199
+ written += raw.copy(result, written, from, to);
200
+ rawOffset += raw.length;
201
+ }
202
+ finally {
203
+ packed.fill(0);
204
+ raw?.fill(0);
205
+ }
206
+ }
207
+ if (written !== result.length)
208
+ throw new CryptoError("plaintext_length_mismatch", "The chunk window did not reproduce the complete requested range");
209
+ return result;
210
+ }
211
+ catch (error) {
212
+ result?.fill(0);
213
+ throw error;
214
+ }
215
+ finally {
216
+ if (ownsKey)
217
+ zeroize(archiveKey);
218
+ }
219
+ }
@@ -20,7 +20,7 @@ export type DeterministicSeed = {
20
20
  export type EncryptOptions = {
21
21
  recipients: Recipient[];
22
22
  suite?: Suite;
23
- compression?: "none" | "gzip" | "gzip-chunk";
23
+ compression?: "none" | "gzip" | "gzip-chunk" | "zstd" | "zstd-chunk";
24
24
  chunkBytes?: number;
25
25
  archiveId?: string;
26
26
  createdAt?: string;
@@ -50,6 +50,16 @@ export declare function encryptArchive(plaintext: Buffer, options: EncryptOption
50
50
  * holds the ciphertext, is gated by exactly the same rules.
51
51
  */
52
52
  export declare function validateEnvelope(envelope: Envelope, ciphertextBytes: number): void;
53
+ /**
54
+ * Authenticates and decrypts the stored body without expanding its compression.
55
+ *
56
+ * Keeping the authenticated chunks separate matters for callers that can stream
57
+ * decompression. A highly-compressible search index may be tens of megabytes on
58
+ * disk and more than a gigabyte after gzip; forcing that expanded body through a
59
+ * single Buffer is enough to OOM a long-lived agent process before it can inspect
60
+ * one query term.
61
+ */
62
+ export declare function decryptArchiveBodyChunks(envelope: Envelope, ciphertext: Buffer, unlock: Unlock): Buffer[];
53
63
  /**
54
64
  * Decrypts an archive after verifying its structure and every stored chunk hash.
55
65
  * Every chunk is authenticated against associated data that pins its position and