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.
- package/ARCHITECTURE.md +201 -0
- package/CHANGELOG.md +218 -0
- package/CONTROL_PLANE.md +86 -0
- package/LICENSE +34 -0
- package/README.md +249 -0
- package/THIRD_PARTY.md +22 -0
- package/THREAT_MODEL.md +107 -0
- package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
- package/dist/packages/vaultline-crypto/src/aead.js +24 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
- package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
- package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
- package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
- package/dist/packages/vaultline-crypto/src/format.js +43 -0
- package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
- package/dist/packages/vaultline-crypto/src/index.js +5 -0
- package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
- package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
- package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
- package/dist/packages/vaultline-crypto/src/stream.js +477 -0
- package/dist/site/index.html +1542 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +22 -0
- package/dist/src/activity.js +52 -0
- package/dist/src/adapters.d.ts +212 -0
- package/dist/src/adapters.js +533 -0
- package/dist/src/audit.d.ts +24 -0
- package/dist/src/audit.js +41 -0
- package/dist/src/autopilot.d.ts +77 -0
- package/dist/src/autopilot.js +148 -0
- package/dist/src/bip39-wordlist.d.ts +15 -0
- package/dist/src/bip39-wordlist.js +272 -0
- package/dist/src/branding.d.ts +31 -0
- package/dist/src/branding.js +31 -0
- package/dist/src/chunk-store.d.ts +142 -0
- package/dist/src/chunk-store.js +502 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2035 -0
- package/dist/src/cloud.d.ts +434 -0
- package/dist/src/cloud.js +851 -0
- package/dist/src/control-plane/auth.d.ts +62 -0
- package/dist/src/control-plane/auth.js +123 -0
- package/dist/src/control-plane/server.d.ts +31 -0
- package/dist/src/control-plane/server.js +263 -0
- package/dist/src/control-plane/store.d.ts +101 -0
- package/dist/src/control-plane/store.js +82 -0
- package/dist/src/control-plane-cli.d.ts +2 -0
- package/dist/src/control-plane-cli.js +37 -0
- package/dist/src/control-plane-server.d.ts +10 -0
- package/dist/src/control-plane-server.js +11 -0
- package/dist/src/control-plane.d.ts +78 -0
- package/dist/src/control-plane.js +61 -0
- package/dist/src/crypto.d.ts +56 -0
- package/dist/src/crypto.js +132 -0
- package/dist/src/daemon.d.ts +52 -0
- package/dist/src/daemon.js +142 -0
- package/dist/src/dashboard-cli.d.ts +2 -0
- package/dist/src/dashboard-cli.js +20 -0
- package/dist/src/disk.d.ts +110 -0
- package/dist/src/disk.js +169 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +198 -0
- package/dist/src/enroll.d.ts +27 -0
- package/dist/src/enroll.js +136 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/heartbeat.d.ts +89 -0
- package/dist/src/heartbeat.js +120 -0
- package/dist/src/index-sync.d.ts +53 -0
- package/dist/src/index-sync.js +147 -0
- package/dist/src/leakscan.d.ts +48 -0
- package/dist/src/leakscan.js +222 -0
- package/dist/src/local-api.d.ts +132 -0
- package/dist/src/local-api.js +1757 -0
- package/dist/src/managed-chunks.d.ts +55 -0
- package/dist/src/managed-chunks.js +108 -0
- package/dist/src/mcp-install.d.ts +52 -0
- package/dist/src/mcp-install.js +140 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +59 -0
- package/dist/src/migrate.d.ts +35 -0
- package/dist/src/migrate.js +88 -0
- package/dist/src/mnemonic.d.ts +60 -0
- package/dist/src/mnemonic.js +134 -0
- package/dist/src/net.d.ts +2 -0
- package/dist/src/net.js +16 -0
- package/dist/src/notify.d.ts +46 -0
- package/dist/src/notify.js +84 -0
- package/dist/src/offload.d.ts +117 -0
- package/dist/src/offload.js +331 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +44 -0
- package/dist/src/packages.d.ts +126 -0
- package/dist/src/packages.js +114 -0
- package/dist/src/passkey.d.ts +26 -0
- package/dist/src/passkey.js +54 -0
- package/dist/src/password-lock.d.ts +19 -0
- package/dist/src/password-lock.js +156 -0
- package/dist/src/paths.d.ts +9 -0
- package/dist/src/paths.js +24 -0
- package/dist/src/providers/gcs.d.ts +133 -0
- package/dist/src/providers/gcs.js +235 -0
- package/dist/src/providers/gdrive.d.ts +156 -0
- package/dist/src/providers/gdrive.js +335 -0
- package/dist/src/providers/index.d.ts +45 -0
- package/dist/src/providers/index.js +74 -0
- package/dist/src/providers/s3.d.ts +174 -0
- package/dist/src/providers/s3.js +345 -0
- package/dist/src/providers/sigv4.d.ts +78 -0
- package/dist/src/providers/sigv4.js +112 -0
- package/dist/src/queue.d.ts +185 -0
- package/dist/src/queue.js +286 -0
- package/dist/src/recovery.d.ts +40 -0
- package/dist/src/recovery.js +132 -0
- package/dist/src/rehydrate.d.ts +43 -0
- package/dist/src/rehydrate.js +66 -0
- package/dist/src/restore.d.ts +34 -0
- package/dist/src/restore.js +80 -0
- package/dist/src/retention.d.ts +251 -0
- package/dist/src/retention.js +446 -0
- package/dist/src/rotate.d.ts +47 -0
- package/dist/src/rotate.js +95 -0
- package/dist/src/search.d.ts +147 -0
- package/dist/src/search.js +677 -0
- package/dist/src/secrets.d.ts +86 -0
- package/dist/src/secrets.js +220 -0
- package/dist/src/service.d.ts +73 -0
- package/dist/src/service.js +197 -0
- package/dist/src/share.d.ts +34 -0
- package/dist/src/share.js +68 -0
- package/dist/src/spool.d.ts +97 -0
- package/dist/src/spool.js +213 -0
- package/dist/src/start-tui.d.ts +17 -0
- package/dist/src/start-tui.js +113 -0
- package/dist/src/start.d.ts +75 -0
- package/dist/src/start.js +101 -0
- package/dist/src/storage-setup.d.ts +49 -0
- package/dist/src/storage-setup.js +222 -0
- package/dist/src/storage-targets.d.ts +40 -0
- package/dist/src/storage-targets.js +147 -0
- package/dist/src/stream-to-cloud.d.ts +76 -0
- package/dist/src/stream-to-cloud.js +820 -0
- package/dist/src/sync-rules.d.ts +85 -0
- package/dist/src/sync-rules.js +125 -0
- package/dist/src/trash.d.ts +15 -0
- package/dist/src/trash.js +63 -0
- package/dist/src/tui.d.ts +18 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/types.d.ts +191 -0
- package/dist/src/types.js +3 -0
- package/dist/src/ui-server.d.ts +187 -0
- package/dist/src/ui-server.js +293 -0
- package/dist/src/ui.d.ts +41 -0
- package/dist/src/ui.js +102 -0
- package/dist/src/update.d.ts +30 -0
- package/dist/src/update.js +56 -0
- package/dist/src/upload.d.ts +46 -0
- package/dist/src/upload.js +80 -0
- package/dist/src/vault.d.ts +208 -0
- package/dist/src/vault.js +812 -0
- package/dist/src/watcher.d.ts +34 -0
- package/dist/src/watcher.js +121 -0
- package/dist/src/worker.d.ts +52 -0
- package/dist/src/worker.js +190 -0
- package/package.json +65 -0
- package/web/app.js +1372 -0
- package/web/index.html +476 -0
- package/web/rail.js +308 -0
- package/web/retention.html +17 -0
- package/web/rules-view.js +249 -0
- package/web/sessions-view.js +448 -0
- package/web/sessions.html +17 -0
- package/web/setup-api.js +181 -0
- package/web/setup-logic.js +394 -0
- package/web/setup.html +419 -0
- package/web/setup.js +697 -0
- package/web/style.css +990 -0
package/ARCHITECTURE.md
ADDED
|
@@ -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.
|
package/CONTROL_PLANE.md
ADDED
|
@@ -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
|