sealkeep 0.5.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 (180) hide show
  1. package/ARCHITECTURE.md +201 -0
  2. package/CHANGELOG.md +218 -0
  3. package/CONTROL_PLANE.md +86 -0
  4. package/LICENSE +34 -0
  5. package/README.md +249 -0
  6. package/THIRD_PARTY.md +22 -0
  7. package/THREAT_MODEL.md +107 -0
  8. package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
  9. package/dist/packages/vaultline-crypto/src/aead.js +24 -0
  10. package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
  11. package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
  12. package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
  13. package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
  14. package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
  15. package/dist/packages/vaultline-crypto/src/format.js +43 -0
  16. package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
  17. package/dist/packages/vaultline-crypto/src/index.js +5 -0
  18. package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
  19. package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
  20. package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
  21. package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
  22. package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
  23. package/dist/packages/vaultline-crypto/src/stream.js +477 -0
  24. package/dist/site/index.html +1542 -0
  25. package/dist/site.zip +0 -0
  26. package/dist/src/activity.d.ts +22 -0
  27. package/dist/src/activity.js +52 -0
  28. package/dist/src/adapters.d.ts +212 -0
  29. package/dist/src/adapters.js +533 -0
  30. package/dist/src/audit.d.ts +24 -0
  31. package/dist/src/audit.js +41 -0
  32. package/dist/src/autopilot.d.ts +77 -0
  33. package/dist/src/autopilot.js +148 -0
  34. package/dist/src/bip39-wordlist.d.ts +15 -0
  35. package/dist/src/bip39-wordlist.js +272 -0
  36. package/dist/src/branding.d.ts +31 -0
  37. package/dist/src/branding.js +31 -0
  38. package/dist/src/chunk-store.d.ts +142 -0
  39. package/dist/src/chunk-store.js +502 -0
  40. package/dist/src/cli.d.ts +2 -0
  41. package/dist/src/cli.js +2035 -0
  42. package/dist/src/cloud.d.ts +434 -0
  43. package/dist/src/cloud.js +851 -0
  44. package/dist/src/control-plane/auth.d.ts +62 -0
  45. package/dist/src/control-plane/auth.js +123 -0
  46. package/dist/src/control-plane/server.d.ts +31 -0
  47. package/dist/src/control-plane/server.js +263 -0
  48. package/dist/src/control-plane/store.d.ts +101 -0
  49. package/dist/src/control-plane/store.js +82 -0
  50. package/dist/src/control-plane-cli.d.ts +2 -0
  51. package/dist/src/control-plane-cli.js +37 -0
  52. package/dist/src/control-plane-server.d.ts +10 -0
  53. package/dist/src/control-plane-server.js +11 -0
  54. package/dist/src/control-plane.d.ts +78 -0
  55. package/dist/src/control-plane.js +61 -0
  56. package/dist/src/crypto.d.ts +56 -0
  57. package/dist/src/crypto.js +132 -0
  58. package/dist/src/daemon.d.ts +52 -0
  59. package/dist/src/daemon.js +142 -0
  60. package/dist/src/dashboard-cli.d.ts +2 -0
  61. package/dist/src/dashboard-cli.js +20 -0
  62. package/dist/src/disk.d.ts +110 -0
  63. package/dist/src/disk.js +169 -0
  64. package/dist/src/doctor.d.ts +11 -0
  65. package/dist/src/doctor.js +198 -0
  66. package/dist/src/enroll.d.ts +27 -0
  67. package/dist/src/enroll.js +136 -0
  68. package/dist/src/errors.d.ts +26 -0
  69. package/dist/src/errors.js +23 -0
  70. package/dist/src/heartbeat.d.ts +89 -0
  71. package/dist/src/heartbeat.js +120 -0
  72. package/dist/src/index-sync.d.ts +53 -0
  73. package/dist/src/index-sync.js +147 -0
  74. package/dist/src/leakscan.d.ts +48 -0
  75. package/dist/src/leakscan.js +222 -0
  76. package/dist/src/local-api.d.ts +132 -0
  77. package/dist/src/local-api.js +1757 -0
  78. package/dist/src/managed-chunks.d.ts +55 -0
  79. package/dist/src/managed-chunks.js +108 -0
  80. package/dist/src/mcp-install.d.ts +52 -0
  81. package/dist/src/mcp-install.js +140 -0
  82. package/dist/src/mcp.d.ts +1 -0
  83. package/dist/src/mcp.js +59 -0
  84. package/dist/src/migrate.d.ts +35 -0
  85. package/dist/src/migrate.js +88 -0
  86. package/dist/src/mnemonic.d.ts +60 -0
  87. package/dist/src/mnemonic.js +134 -0
  88. package/dist/src/net.d.ts +2 -0
  89. package/dist/src/net.js +16 -0
  90. package/dist/src/notify.d.ts +46 -0
  91. package/dist/src/notify.js +84 -0
  92. package/dist/src/offload.d.ts +117 -0
  93. package/dist/src/offload.js +331 -0
  94. package/dist/src/onboarding.d.ts +10 -0
  95. package/dist/src/onboarding.js +44 -0
  96. package/dist/src/packages.d.ts +126 -0
  97. package/dist/src/packages.js +114 -0
  98. package/dist/src/passkey.d.ts +26 -0
  99. package/dist/src/passkey.js +54 -0
  100. package/dist/src/password-lock.d.ts +19 -0
  101. package/dist/src/password-lock.js +156 -0
  102. package/dist/src/paths.d.ts +9 -0
  103. package/dist/src/paths.js +24 -0
  104. package/dist/src/providers/gcs.d.ts +133 -0
  105. package/dist/src/providers/gcs.js +235 -0
  106. package/dist/src/providers/gdrive.d.ts +156 -0
  107. package/dist/src/providers/gdrive.js +335 -0
  108. package/dist/src/providers/index.d.ts +45 -0
  109. package/dist/src/providers/index.js +74 -0
  110. package/dist/src/providers/s3.d.ts +174 -0
  111. package/dist/src/providers/s3.js +345 -0
  112. package/dist/src/providers/sigv4.d.ts +78 -0
  113. package/dist/src/providers/sigv4.js +112 -0
  114. package/dist/src/queue.d.ts +185 -0
  115. package/dist/src/queue.js +286 -0
  116. package/dist/src/recovery.d.ts +40 -0
  117. package/dist/src/recovery.js +132 -0
  118. package/dist/src/rehydrate.d.ts +43 -0
  119. package/dist/src/rehydrate.js +66 -0
  120. package/dist/src/restore.d.ts +34 -0
  121. package/dist/src/restore.js +80 -0
  122. package/dist/src/retention.d.ts +251 -0
  123. package/dist/src/retention.js +446 -0
  124. package/dist/src/rotate.d.ts +47 -0
  125. package/dist/src/rotate.js +95 -0
  126. package/dist/src/search.d.ts +147 -0
  127. package/dist/src/search.js +677 -0
  128. package/dist/src/secrets.d.ts +86 -0
  129. package/dist/src/secrets.js +220 -0
  130. package/dist/src/service.d.ts +73 -0
  131. package/dist/src/service.js +197 -0
  132. package/dist/src/share.d.ts +34 -0
  133. package/dist/src/share.js +68 -0
  134. package/dist/src/spool.d.ts +97 -0
  135. package/dist/src/spool.js +213 -0
  136. package/dist/src/start-tui.d.ts +17 -0
  137. package/dist/src/start-tui.js +113 -0
  138. package/dist/src/start.d.ts +75 -0
  139. package/dist/src/start.js +101 -0
  140. package/dist/src/storage-setup.d.ts +49 -0
  141. package/dist/src/storage-setup.js +222 -0
  142. package/dist/src/storage-targets.d.ts +40 -0
  143. package/dist/src/storage-targets.js +147 -0
  144. package/dist/src/stream-to-cloud.d.ts +76 -0
  145. package/dist/src/stream-to-cloud.js +820 -0
  146. package/dist/src/sync-rules.d.ts +85 -0
  147. package/dist/src/sync-rules.js +125 -0
  148. package/dist/src/trash.d.ts +15 -0
  149. package/dist/src/trash.js +63 -0
  150. package/dist/src/tui.d.ts +18 -0
  151. package/dist/src/tui.js +179 -0
  152. package/dist/src/types.d.ts +191 -0
  153. package/dist/src/types.js +3 -0
  154. package/dist/src/ui-server.d.ts +187 -0
  155. package/dist/src/ui-server.js +293 -0
  156. package/dist/src/ui.d.ts +41 -0
  157. package/dist/src/ui.js +102 -0
  158. package/dist/src/update.d.ts +30 -0
  159. package/dist/src/update.js +56 -0
  160. package/dist/src/upload.d.ts +46 -0
  161. package/dist/src/upload.js +80 -0
  162. package/dist/src/vault.d.ts +208 -0
  163. package/dist/src/vault.js +812 -0
  164. package/dist/src/watcher.d.ts +34 -0
  165. package/dist/src/watcher.js +121 -0
  166. package/dist/src/worker.d.ts +52 -0
  167. package/dist/src/worker.js +190 -0
  168. package/package.json +65 -0
  169. package/web/app.js +1372 -0
  170. package/web/index.html +476 -0
  171. package/web/rail.js +308 -0
  172. package/web/retention.html +17 -0
  173. package/web/rules-view.js +249 -0
  174. package/web/sessions-view.js +448 -0
  175. package/web/sessions.html +17 -0
  176. package/web/setup-api.js +181 -0
  177. package/web/setup-logic.js +394 -0
  178. package/web/setup.html +419 -0
  179. package/web/setup.js +697 -0
  180. package/web/style.css +990 -0
@@ -0,0 +1,201 @@
1
+ # Sealkeep Architecture and Contracts
2
+
3
+ ## 1. Trust boundaries
4
+
5
+ ```text
6
+ Agent transcript -> local adapter -> local queue -> crypto boundary
7
+ | plaintext ends here
8
+ v
9
+ ciphertext + opaque metadata
10
+ |
11
+ control plane -> scoped provider lease
12
+ |
13
+ S3 / R2 / GCS / managed store
14
+ ```
15
+
16
+ The client is trusted to handle plaintext. The control plane and object store are untrusted for confidentiality. The MCP server is local but model-invoked, so tools must minimize disclosure and mark destructive actions clearly.
17
+
18
+ ## 2. Client modules
19
+
20
+ - `adapters`: discover roots, consume hook payloads, preserve raw files.
21
+ - `queue`: durable jobs, retries, backoff, idempotency.
22
+ - `crypto`: versioned envelope, key generation/wrapping, integrity.
23
+ - `providers`: lease client, multipart/resumable upload, plain object verbs, remote checksum.
24
+ - `chunk-store`: remote layout v2 — one archive as one folder of sealed chunks, path
25
+ segments hashed under a storage-scoped key, readable identity in a sealed sidecar.
26
+ - `index-sync`: the sealed content index, pulled and pushed as ciphertext so every
27
+ machine of the vault searches the whole history.
28
+ - `retention`: active-session detection, policy evaluation, grace/trash staging.
29
+ - `restore`: integrity checks, atomic output, native adapter mapping; `rehydrate`
30
+ restores an archived transcript before an agent's resume reads it.
31
+ - `local-api`: loopback API shared by CLI/TUI/desktop/MCP.
32
+
33
+ Implemented as: `src/adapters.ts`, `src/watcher.ts`, `src/queue.ts`, `src/worker.ts`,
34
+ `packages/vaultline-crypto/`, `src/providers/`, `src/secrets.ts`, `src/upload.ts`,
35
+ `src/chunk-store.ts`, `src/index-sync.ts`, `src/stream-to-cloud.ts`,
36
+ `src/retention.ts` with `src/activity.ts` and `src/trash.ts`, `src/restore.ts` with
37
+ `src/rehydrate.ts`, `src/share.ts`, `src/rotate.ts`,
38
+ `src/local-api.ts` with `web/` and `src/tui.ts`, `src/daemon.ts`, `src/search.ts`,
39
+ `src/audit.ts`, `src/migrate.ts`, `src/doctor.ts`, `src/mcp.ts` with
40
+ `src/mcp-install.ts`, and
41
+ `src/control-plane/`. `src/errors.ts` holds the shared failure codes every surface reports.
42
+
43
+ ## 2.1 Queue and idempotency
44
+
45
+ A job is one atomically created file named by a content-addressed id derived from
46
+ `agent + resolved source path + byte length + mtime`. Two consequences follow:
47
+
48
+ - Repeated lifecycle events for an unchanged transcript collapse to one job, so a
49
+ `PostCompact` immediately followed by a `SessionEnd` archives once.
50
+ - Concurrent hook processes cannot create duplicates: creation uses `link(2)`,
51
+ which fails atomically when the id already exists.
52
+
53
+ Work is handed out under a time-boxed lease. An expired lease is reclaimed on the
54
+ next claim, which is how an interrupted worker recovers. Completion requires a
55
+ matching unexpired lease; a stale worker cannot mark a job done. Failure applies
56
+ exponential backoff and dead-letters after `maxAttempts`, and a dead-lettered job
57
+ only returns to `ready` through an explicit `queue retry`.
58
+
59
+ Archiving is separately idempotent: `archiveFile` returns the existing record when
60
+ the same source path already has an archive with the same plaintext SHA-256. A
61
+ worker that dies between encrypting and completing therefore never writes a second
62
+ archive on retry.
63
+
64
+ ## 2.2 Hook boundary
65
+
66
+ Hook commands hold no secret. They parse the lifecycle payload for a transcript
67
+ path (`transcript_path`, `rollout_path`, and camelCase spellings), verify it is
68
+ readable, enqueue, and exit. Unknown vendor fields are ignored rather than parsed,
69
+ so a schema change cannot break preservation. Encryption happens later in
70
+ `sealkeep queue run`, which is the only process that sees the recovery phrase.
71
+
72
+ ## 3. Archive envelope (implemented, format v2)
73
+
74
+ ```json
75
+ {
76
+ "version": 2,
77
+ "archiveId": "uuid",
78
+ "suite": "chacha20-poly1305",
79
+ "chunkBytes": 4194304,
80
+ "chunks": [{ "index": 0, "plaintextBytes": 0, "storedBytes": 16, "sha256": "...", "nonce": "...", "final": true }],
81
+ "manifest": { "plaintextSha256": "...", "plaintextBytes": 0, "createdAt": "...", "adapter": { "agent": "codex", "version": "1" } },
82
+ "wrappedKeys": [{ "type": "phrase", "id": "...", "kdf": {}, "nonce": "...", "ciphertext": "..." }]
83
+ }
84
+ ```
85
+
86
+ Each chunk is authenticated with associated data covering format version, suite, archive
87
+ id, chunk index, total chunk count, plaintext length, and a final-chunk flag. Wrapped keys
88
+ are separately bound to archive id and suite, so tampering with either is caught at unwrap
89
+ time before any chunk is touched. An archive is durable only after the provider confirms
90
+ the byte count and checksum the client computed; `remote.verifiedAt` records that, and
91
+ retention reads it.
92
+
93
+ Full specification, test vectors, and tamper coverage: `packages/vaultline-crypto/`.
94
+
95
+ ## 4. Control-plane API v1
96
+
97
+ Authentication is on by default. `VAULTLINE_CONTROL_PLANE_DEV=1` disables device signatures
98
+ and keeps state in memory for local interface work; `/health` reports
99
+ `mode: development-interface` so a misconfigured deployment is visible in one request.
100
+
101
+ All non-enrollment endpoints require an Ed25519 device signature over
102
+ `vaultline-request:v1\n<METHOD>\n<path>\n<timestamp>\n<nonce>\n<sha256(body)>`, presented in
103
+ `x-vaultline-device`, `-timestamp`, `-nonce`, and `-signature`. Timestamps outside a
104
+ five-minute window are refused, and a nonce cache rejects verbatim replays.
105
+
106
+ ### `POST /v1/accounts` and `POST /v1/devices`
107
+
108
+ Account creation returns an enrollment token exactly once; only its hash is stored. Device
109
+ enrollment presents that token plus a raw Ed25519 public key. These two endpoints cannot be
110
+ device-signed because they are what establish a device.
111
+
112
+ ### `POST /v1/storage-configs`
113
+
114
+ Request: `{ provider, bucket, prefix, region?, credentialMode }`. Response: opaque config ID. Never accept secrets in this payload.
115
+
116
+ ### `POST /v1/archives/leases`
117
+
118
+ Request: `{ storageConfigId, archiveId, ciphertextSha256, bytes, partCount }`. Response: `{ leaseId, objectKey, expiresAt, method, requiredHeaders, parts|resumableInit, status }`. Object key is server-generated; the client cannot choose it.
119
+
120
+ ### `POST /v1/archives/:id/complete`
121
+
122
+ Request: provider object checksum, byte count, lease ID, manifest hash. Server performs provider HEAD/checksum validation before marking durable.
123
+
124
+ ### `PUT /v1/archives/:id/manifest`
125
+
126
+ Stores opaque encrypted manifest plus safe operational metadata. Never accepts source transcript content.
127
+
128
+ ### Device endpoints
129
+
130
+ `GET /v1/devices`, `POST /v1/devices/:id/revoke`. Revocation blocks new leases and never
131
+ deletes an archive; the response reports how many archives were retained. Revocation is not
132
+ retroactive for ciphertext an attacker already holds — the client-side counterpart is
133
+ `sealkeep recipients remove` followed by `sealkeep rewrap`.
134
+
135
+ ### `GET /v1/audit`
136
+
137
+ Account-scoped audit events: action, outcome, identifiers, and counts. No content.
138
+
139
+ ## 4.1 Local API v1 (loopback)
140
+
141
+ `sealkeep api` binds `127.0.0.1` only. Every state endpoint requires
142
+ `Authorization: Bearer <token>` compared in constant time; the token is minted once
143
+ into `<data-dir>/local-api-token` at mode `600` and is never logged or returned.
144
+ Non-loopback peers and unexpected `Host` headers are rejected with `forbidden`, so a
145
+ browsed page cannot reach the API by DNS rebinding.
146
+
147
+ | Endpoint | Purpose |
148
+ | --- | --- |
149
+ | `GET /health` | Unauthenticated readiness; returns no vault state |
150
+ | `GET /v1/status` | Vault identity, archive counts, queue stats |
151
+ | `GET /v1/agents` | Detected agent installs |
152
+ | `GET /v1/archives` | Archive metadata; wrapped key material is omitted |
153
+ | `GET /v1/queue` | Job list |
154
+ | `POST /v1/queue` | Enqueue `{ sourcePath, agent, event?, sessionId? }` |
155
+ | `GET /v1/retention` | Policy evaluation with every blocker per candidate |
156
+ | `POST /v1/retention/preview` | Read-only reclamation preview |
157
+ | `POST /v1/retention/apply` | Reclaims only when `confirm: true`; dry run otherwise |
158
+ | `POST /v1/upload` | Uploads pending archives (no phrase: the ciphertext already exists) |
159
+ | `GET /v1/doctor` | Environment report |
160
+ | `GET /v1/recipients` | Registered X25519 recipients |
161
+ | `GET /` and static assets | The dashboard shell, served before authentication because it holds no data |
162
+
163
+ The API deliberately cannot decrypt. No endpoint accepts a recovery phrase, and
164
+ `/v1/recover` answers `403 forbidden` pointing at the local `sealkeep recover`
165
+ command. Bodies are capped at 1 MB, validated with strict schemas, and rejected as
166
+ `invalid_argument`.
167
+
168
+ ## 5. Local MCP contract
169
+
170
+ MCP tools call local APIs, not provider SDKs directly. `vaultline_create_upload` returns a lease object; it must never receive a cloud secret. Search defaults to metadata. Recover and archive require explicit local authorization/recovery secret.
171
+
172
+ ## 6. Failure behavior
173
+
174
+ - Offline: retain encrypted job and retry with exponential backoff.
175
+ - Expired lease: request a new lease; never reuse a bearer URL.
176
+ - Hash mismatch: quarantine object/job, do not reclaim source, surface high-priority error.
177
+ - Hook timeout: hook exits quickly; daemon continues independently.
178
+ - Provider unavailable: local state remains recoverable.
179
+ - Interrupted restore: temporary file is removed or retained as a clearly named partial artifact; destination is never half-overwritten.
180
+
181
+ ## 7. Current boundary
182
+
183
+ Implemented and tested end to end: capture (hooks, watcher, durable queue), encryption
184
+ (format v2 with vectors and a tamper suite), provider clients (SigV4 verified against AWS's
185
+ published example; multipart, GCS resumable, and the plain object verbs exercised against
186
+ in-process emulators), verified upload, the chunk-folder remote layout with hashed naming
187
+ and sealed identity, retention with trash staging, restore including native adapter paths
188
+ and rehydrate-on-resume, the sealed index sync, the loopback API behind the dashboard and
189
+ TUI, the MCP server and its installer, the control plane with device auth and audit,
190
+ search, and the local audit log.
191
+
192
+ Four gates remain open, none closable by writing more code in this repository:
193
+
194
+ 1. **Independent cryptographic audit.** Required before any end-to-end-encryption claim.
195
+ 2. **A live cloud drill for the bring-your-own providers.** GCS carries production
196
+ traffic through the managed tier; S3, R2, and Drive have not yet been driven
197
+ against a real account.
198
+ 3. **OS code-signing certificates.** Releases carry npm provenance and an Ed25519-signed manifest only.
199
+ 4. **Browser and accessibility QA** of the dashboard.
200
+
201
+ Uploads remain off unless `VAULTLINE_ENABLE_SIGNER=1` is set explicitly.
package/CHANGELOG.md ADDED
@@ -0,0 +1,218 @@
1
+ # Changelog
2
+
3
+ Notable changes, by published version. Sealkeep is pre-1.0: minor versions
4
+ may change behavior, and say so here when they do.
5
+
6
+ ## 0.5.0 — 2026-08-18
7
+
8
+ **Vaultline is now Sealkeep** (by SPALA AI) — the old name is used by several
9
+ unrelated companies, and this one says what the product does: seal it, keep it.
10
+
11
+ Nothing breaks:
12
+ - the `vaultline` command still works (aliased to `sealkeep`)
13
+ - existing `~/.vaultline` vaults keep working forever; new vaults use `~/.sealkeep`
14
+ - `VAULTLINE_*` environment variables are unchanged
15
+ - every file format, keystore entry, and cloud object stays exactly as it was
16
+
17
+ Install as `npm install -g sealkeep`; the `vaultline` npm package is deprecated
18
+ and points here.
19
+
20
+ ## 0.4.0 — 2026-08-18
21
+
22
+ Several storages, one vault — proven live against a real Google Drive.
23
+
24
+ - **Storage targets**: keep the managed cloud AND your own Drive (or bucket)
25
+ connected at once, with rules — a byte cap per target ("Drive holds at
26
+ most 50 GB"), projects pinned to a target, priority ordering in which the
27
+ managed cloud wins by default. `sealkeep storage targets [add|remove]`,
28
+ `archive --target <id>`. Reads never consult the rules: every archive's
29
+ record routes its own restore.
30
+ - **The rules live on your account.** Every machine of the vault syncs the
31
+ same routing rules down (and caches them for offline), so a cap set on
32
+ one laptop holds on the next.
33
+ - **Connecting Drive is one click.** Consent runs on the account's shared
34
+ Google client through a remote callback: the browser approves, the server
35
+ exchanges the code with its own secret, and the machine claims its tokens
36
+ once — nothing Google-related ships in this package, and the flow works
37
+ even for headless machines whose browser lives elsewhere. `drive.file`
38
+ scope only: Sealkeep sees nothing in a Drive but its own sealed files.
39
+ - **Drive reads back.** Offload's stale gate thought Drive was write-only;
40
+ the live drill sealed a 58.8 MB session to a real Drive, freed the local
41
+ copy only after downloading and hash-proving the Drive copy, and restored
42
+ it byte-identical from cold.
43
+
44
+ ## 0.3.5 — 2026-08-18
45
+
46
+ Proven on two live machines sharing one storage: knowledge really is
47
+ collective now.
48
+
49
+ - **A second machine sees everything.** A hit whose archive this machine
50
+ never made is reconstructed from the cloud alone — the envelope sidecar
51
+ opens with the phrase, the sealed identity restores agent and path, and
52
+ snippets fetch like any cold archive. A fresh vault signed into the
53
+ account searched a 1.67 GB session it had no record of, with snippets,
54
+ in 12 seconds; the first machine found the second machine's session in
55
+ under 5.
56
+ - **Snippet fetches are bounded and parallel**: one small window per tagged
57
+ chunk instead of a single window spanning between tags (two far tags used
58
+ to fetch 349 chunks of a cold archive). Multi-term search dropped from
59
+ 153 s to 5.5 s; single-term from 60 s to 5.7 s.
60
+ - **Compounds index by their segments** — "chunk-store.ts" answers for
61
+ "store", and an over-long hyphenated identifier is findable through every
62
+ piece of itself; query terms the index could never hold are not required.
63
+
64
+ ## 0.3.4 — 2026-08-18
65
+
66
+ Search quality, earned the hard way: a real 1.67 GB session was archived,
67
+ searched, and every miss chased to its cause.
68
+
69
+ - **The token budget scales with the archive** (72 tokens/MB, capped at
70
+ 120k). A flat 4000 starved everything past the first chunks of a big seal
71
+ — "retention" was findable, "chunk" (141 honest mentions) was not.
72
+ - **Every chunk gets a fair seat**: per-chunk vocabularies merge round-robin
73
+ instead of first-chunks-win, so a word living only in chunk 350 is as
74
+ findable as one from chunk 0.
75
+ - **Junk never takes a slot**: hex digests, base64 runs, and over-long
76
+ identifiers are filtered before they can crowd out words.
77
+ - **Prefix search**: "chunk" also finds "chunks" and "chunkbytes".
78
+ - **Fixed prototype pollution via transcripts**: a session containing the
79
+ word "constructor" crashed indexing — token maps are own-property-safe
80
+ everywhere now.
81
+ - **The index actually syncs**: the managed index slot refused every blob
82
+ PUT (the signed x-goog-if-generation-match header was never forwarded, and
83
+ the slot was signed create-only). The plane now returns the signer's own
84
+ headers and signs with overwrite — pushes verified live, twice.
85
+ - `archive --stream --json` reports `indexed`.
86
+
87
+ ## 0.3.3 — 2026-08-18
88
+
89
+ - **You can always see what search covers.** `sealkeep index status` (and a
90
+ `search-index` doctor check) reports how many archives are searchable,
91
+ names the ones that are not yet indexed, and needs no phrase to answer —
92
+ coverage lives in an unsealed sidecar of archive ids only, while every
93
+ token stays sealed. New seals index themselves; `index build` adds the
94
+ rest whenever you choose.
95
+
96
+ ## 0.3.2 — 2026-08-18
97
+
98
+ Found by running the real loop — archive a quarter-gigabyte production
99
+ session, index it, search it, restore it — and fixing what it hit.
100
+
101
+ - **Sessions are searchable the moment they seal.** The seal already holds
102
+ every plaintext slice, so tokens are collected during sealing and merged
103
+ into the content index (and synced to the vault's slot) as the seal
104
+ returns. The old way uploaded an archive and then downloaded it back just
105
+ to read it.
106
+ - **Chunks upload four at a time.** The seal loop stays sequential; the wire
107
+ no longer is. A 282 MB session that took 5 minutes uploads in a fraction.
108
+ - **`index build` survives the real world**: offloaded archives are fetched
109
+ from the cloud instead of ENOENT-ing on the reclaimed local blob; archives
110
+ past Node's ~512 MB string ceiling are scanned in windows; a version-1
111
+ index merges instead of crashing (unusable entries re-index once); one
112
+ flaky download skips one archive with a report instead of killing the
113
+ build; token collection is capped so hex-noise transcripts cannot balloon
114
+ memory.
115
+
116
+ ## 0.3.1 — 2026-08-18
117
+
118
+ - **Fixed: a real managed vault could not stream.** Enrolled vaults carry a
119
+ cloud token and no `remoteStorage` entry; 0.3.0's chunk paths (and index
120
+ sync) gated on the entry alone, so streaming answered "no remote target
121
+ configured" on exactly the vaults the feature was for. Managed-ness now
122
+ follows the same rule everywhere: an own bucket wins when configured,
123
+ otherwise the signed-in account is the target. Found by a live drill on a
124
+ production vault; covered by a token-only regression test.
125
+
126
+ ## 0.3.0 — 2026-08-18
127
+
128
+ The chunk-folder layout is now the whole product's answer, not a CLI flag.
129
+
130
+ - **Managed vaults stream into chunk folders** — no new control-plane
131
+ capability: the client already chunks, so each sealed chunk is delivered as
132
+ an ordinary file through the ordinary upload lease (`<archiveId>.chunk-NNNNNN`
133
+ plus an `<archiveId>.envelope.vlmeta` sidecar). Resume probes storage, not
134
+ metering rows; restore, search, check-copy, listings, and delete all treat
135
+ the family as one archive.
136
+ - **The background worker streams under pressure.** A transcript the disk
137
+ cannot hold a sealed copy of no longer waits in the queue for a reclaim:
138
+ the worker seals it straight into a chunk folder — peak local cost is one
139
+ chunk — and the archive is born offloaded and verified. The ordinary path
140
+ (local seal, delta chains, offline archiving) is untouched.
141
+ - **Google Drive is single-object by design.** Chunk folders need an object
142
+ store; Drive archives keep the whole-object path and say so.
143
+ - **Fixed: restoring a chunk-folder archive refused good bytes.** 0.2.0
144
+ recorded a double-hashed whole-ciphertext digest for `--stream` chunk-folder
145
+ seals (own-bucket only), so sealing verified but every restore failed its
146
+ integrity check. Records sealed with 0.2.0's `--stream` carry the bad
147
+ digest and need a fresh seal; per-chunk hashes were always correct.
148
+
149
+ ## 0.2.0 — 2026-08-18
150
+
151
+ Everything since the first publish. Version 0.1.1 was packed for internal
152
+ smoke tests and never reached the registry; its work is listed here.
153
+
154
+ ### The vault runs itself
155
+ - `sealkeep autopilot`: one command installs the login service that catches,
156
+ seals, uploads, verifies, and reclaims on a loop, with a heartbeat that
157
+ `status`, the dashboard, and the local API all read.
158
+ - Sessions seal in parallel behind a disk-space reservation; repeat archives
159
+ of a growing session are deltas, not re-archives.
160
+ - Retention finishes the job: archives the bucket verifiably holds can be
161
+ offloaded locally, and the disk reserve scales with the disk.
162
+
163
+ ### What the bucket sees became a design surface
164
+ - Remote layout v2: one archive is one folder of sealed chunks plus a small
165
+ envelope. Uploads are plain known-length PUTs, resume is deterministic,
166
+ delete is the folder.
167
+ - Path segments (project, date, session) are hashed under a storage-scoped
168
+ key by default; a sealed sidecar carries the readable identity, so the
169
+ phrase rebuilds the friendly view and the provider gets noise.
170
+ `remoteNaming: "readable"` opts out.
171
+ - `sealkeep archive --stream` seals straight to the bucket — the ciphertext
172
+ never touches the disk — resumable mid-object, with journaling ordered so
173
+ an encryption nonce is never reused across a crash.
174
+
175
+ ### Search that travels
176
+ - The content index is sealed with the archive machinery and synced beside
177
+ the data (own bucket or the managed tier), so every machine of the vault
178
+ searches the whole history without rebuilding it.
179
+ - Index builds are incremental and merging; `search --content` answers with
180
+ ranged chunk fetches, not bulk restores.
181
+
182
+ ### Agents are first-class users
183
+ - `sealkeep mcp` is a local MCP server (status, search, archive, recover,
184
+ upload leases, retention preview); `sealkeep mcp install` registers it
185
+ with Claude Code and Codex, with a byte-identical backup of any file it
186
+ touches.
187
+ - Rehydrate-on-resume: a `SessionStart` hook restores an archived transcript
188
+ to its exact path before the agent reads it. Resuming a session Sealkeep
189
+ reclaimed is no longer a failure mode — and the hook can never break a
190
+ session start.
191
+ - `sealkeep recover <filename>` accepts the filename from an agent's error
192
+ message; `sealkeep doctor` warns when a resumable session's file is gone
193
+ and names the command that puts it back.
194
+
195
+ ### Keys, phrases, sharing
196
+ - Phrase rotation and password change re-key every archive by re-wrapping —
197
+ nothing is re-encrypted.
198
+ - `sealkeep share <id>` seals one archive into a `.vlshare` bundle under a
199
+ one-time passcode; the vault phrase deliberately does not open it.
200
+ - Escrow blobs can carry more than one way in.
201
+
202
+ ### Storage and accounts
203
+ - Google Drive joins S3, R2, GCS, and S3-compatibles as a first-class home.
204
+ - Managed tier: accounts, device auth, quotas, upload leases, and returning
205
+ machines joining an account — with the control plane never receiving
206
+ content, keys, or the phrase.
207
+ - A secrets leak-scan runs before anything leaves the machine.
208
+
209
+ ### The panel is one product
210
+ - One SPA: sessions, retention rules, memory pool, and the vault merged into
211
+ a single navigable dashboard; every archive's verbs live on its row; bulk
212
+ actions state what they will remove before they act.
213
+
214
+ ## 0.1.0 — 2026-08-16
215
+
216
+ First publish: encrypted session vault core — capture hooks, durable queue,
217
+ envelope format v2 (vectors and tamper suite), verified upload, trash-staged
218
+ retention, byte-identical restore.
@@ -0,0 +1,86 @@
1
+ # Sealkeep control-plane contract
2
+
3
+ The control plane stores device identities, encrypted manifests, and provider configuration. It never accepts transcript plaintext.
4
+
5
+ ## Running it
6
+
7
+ ```sh
8
+ npm run control-plane # authenticated, durable
9
+ VAULTLINE_CONTROL_PLANE_DEV=1 npm run control-plane # unauthenticated, in memory
10
+ docker build -t vaultline-control-plane . && docker run -p 8787:8787 -v vaultline-data:/data vaultline-control-plane
11
+ ```
12
+
13
+ Endpoints:
14
+
15
+ - `GET /health` — reports `mode: control-plane` or `development-interface`
16
+ - `POST /v1/accounts` — returns an enrollment token once; only its hash is stored
17
+ - `POST /v1/devices` — enrols an Ed25519 device key using that token
18
+ - `GET /v1/devices`, `POST /v1/devices/:id/revoke`
19
+ - `POST /v1/storage-configs`, `GET /v1/storage-configs/:id`
20
+ - `POST /v1/archives/leases`, `POST /v1/archives/:id/complete`
21
+ - `PUT /v1/archives/:id/manifest`, `GET /v1/archives`
22
+ - `GET /v1/audit`
23
+
24
+ ## Authentication
25
+
26
+ Every endpoint except account creation and device enrollment requires an Ed25519 signature
27
+ over:
28
+
29
+ ```text
30
+ vaultline-request:v1\n<METHOD>\n<path>\n<timestamp>\n<nonce>\n<sha256(body)>
31
+ ```
32
+
33
+ presented in `x-vaultline-device`, `x-vaultline-timestamp`, `x-vaultline-nonce`, and
34
+ `x-vaultline-signature`. A timestamp more than five minutes from the server's clock is
35
+ refused, and a nonce cache rejects verbatim replays. Binding method, path, and body hash
36
+ into the signature means a captured signature cannot be redirected to another endpoint.
37
+
38
+ `VAULTLINE_CONTROL_PLANE_DEV=1` disables this and keeps state in memory. It exists for
39
+ local interface development. If `/health` reports `development-interface` on anything you
40
+ deployed, stop it.
41
+
42
+ ## Provider configuration
43
+
44
+ ```json
45
+ {
46
+ "provider": "s3 | r2 | gcs | vaultline",
47
+ "bucket": "vaultline-archives",
48
+ "region": "us-east-1",
49
+ "prefix": "v1/accounts/<account-id>",
50
+ "credentialMode": "server-managed | byo-role | byo-service-account"
51
+ }
52
+ ```
53
+
54
+ Provider credentials live only in the control plane's secret manager. BYO S3/R2 configurations use a prefix-restricted role/token; GCS uses a service account restricted to the archive prefix.
55
+
56
+ ## Lease endpoint
57
+
58
+ `POST /v1/archives/leases`
59
+
60
+ Input is ciphertext-only metadata: archive ID, encrypted byte length, SHA-256 of ciphertext, and desired part count. The response includes a short-lived, single-object upload lease. Leases are bearer capabilities; default expiry is 15 minutes and every issued lease is audit logged.
61
+
62
+ Every lease carries `status` and `uploadable`. In this build they are always `pending-signer` and `false`. `assertLeaseUsable` is the single gate an upload path must pass: it checks expiry first, so a replayed lease is refused as `lease_expired` rather than as an unsigned one, and it then refuses anything that is not an `active` uploadable lease with `signer_not_configured`.
63
+
64
+ `VAULTLINE_ENABLE_SIGNER=1` permits registering a `ProviderUploadClient`, but no implementation ships. With the flag off, registration and lookup both fail; turning it off again immediately withdraws every registered client.
65
+
66
+ For S3/R2 the response contains `createMultipart`, per-part presigned PUT URLs, and a completion endpoint. For GCS it contains a signed initiation URL plus the resumable session URI returned to the local agent. The client validates the remote checksum and then calls `POST /v1/archives/complete`.
67
+
68
+ ## Manifest endpoint
69
+
70
+ `PUT /v1/archives/:id/manifest` stores an opaque encrypted manifest. It may contain encrypted source path, agent adapter version, chunk object keys, and wrapped archive keys. The server only indexes account, device, timestamps, byte counts, and object status unless private-metadata mode is disabled.
71
+
72
+ ## Required security checks
73
+
74
+ - The lease object key is allocated by the server, never accepted from the client. *(Implemented: the key is derived from the account's configured prefix and the archive id.)*
75
+ - Content length, checksum, content type, and upload method are signed/restricted. *(Implemented client-side: the SHA-256 header is signed, which pins the bytes. `content-length` is a forbidden header for `fetch` and cannot be sent verbatim, so it is not signed.)*
76
+ - Completion verifies the reported byte count against the lease before marking an archive durable, and the client independently verifies the provider's checksum before writing `remote.verifiedAt`. *(A server-side provider HEAD is still to do; it needs the signer service.)*
77
+ - Device revocation prevents new leases but does not destroy user archives. *(Implemented and tested.)*
78
+ - Leases are single use: completing one marks it consumed, and a replay is refused with `lease_replayed`. *(Implemented and tested.)*
79
+ - Quotas are checked before a lease is issued. *(Implemented and tested.)*
80
+ - Archive deletion has no endpoint. Removing an account is a deliberate manual operation; see `docs/RUNBOOK.md` §8.
81
+
82
+ ## Storage limits
83
+
84
+ State is one JSON document rewritten atomically per mutation — appropriate for a
85
+ team-sized deployment, not a large multi-tenant service. Replace `ControlPlaneStore` with a
86
+ database before that point; nothing above it depends on the file layout.
package/LICENSE ADDED
@@ -0,0 +1,34 @@
1
+ Vaultline Software License
2
+
3
+ Copyright (c) Spala.
4
+ All rights reserved.
5
+
6
+ Permission is granted to install, copy, and run this package to archive,
7
+ encrypt, verify, and restore your own coding-agent session data, on machines
8
+ you own or control, including for commercial purposes. No fee is charged for
9
+ use with storage you provide yourself. Use of the managed storage service
10
+ operated by Spala is charged separately and is governed by the managed-service
11
+ terms in force at the time of purchase.
12
+
13
+ You may not sell, relicense, redistribute this package as a standalone or
14
+ competing product, or use it to provide an archiving service to third parties
15
+ without prior written permission from Spala.
16
+
17
+ Your data is encrypted on your machine before it is written or transmitted.
18
+ Spala does not receive, and cannot decrypt, the contents of your archives —
19
+ including archives stored in the managed service. You are solely responsible
20
+ for the recovery phrase this software generates: Spala cannot recover it,
21
+ cannot reset it, and cannot decrypt your archives without it. Losing the
22
+ recovery phrase means losing access to your archives permanently. This is a
23
+ deliberate property of the design, not a defect.
24
+
25
+ This package includes third-party open-source components, each governed by its
26
+ own licence; see THIRD_PARTY.md.
27
+
28
+ This package is provided "as is", without warranty of any kind, express or
29
+ implied, including but not limited to warranties of merchantability, fitness
30
+ for a particular purpose, and noninfringement. Spala is not liable for any
31
+ claim, damages, or other liability — including loss of data — arising from use
32
+ of this package, except liability that cannot lawfully be excluded.
33
+
34
+ Questions: paul@spala.ai