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
@@ -0,0 +1,232 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, mkdir, open, readdir, readFile, rename, rm } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ const CLAIM_NAME = /^(\d{20})\.json$/;
5
+ const MAX_TICKET = BigInt("99999999999999999999");
6
+ function errorCode(error) {
7
+ return typeof error === "object" && error !== null && "code" in error
8
+ ? String(error.code)
9
+ : undefined;
10
+ }
11
+ function codedError(message, code) {
12
+ const error = new Error(message);
13
+ error.code = code;
14
+ return error;
15
+ }
16
+ async function syncDirectory(path) {
17
+ let handle;
18
+ try {
19
+ handle = await open(path, "r");
20
+ await handle.sync();
21
+ }
22
+ catch (error) {
23
+ if (process.platform !== "win32")
24
+ throw error;
25
+ }
26
+ finally {
27
+ await handle?.close();
28
+ }
29
+ }
30
+ async function ensureDirectory(path) {
31
+ const created = await mkdir(path, { recursive: true, mode: 0o700 });
32
+ if (created) {
33
+ await syncDirectory(dirname(resolve(created)));
34
+ await syncDirectory(path);
35
+ }
36
+ }
37
+ async function publishJsonExclusive(path, value) {
38
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
39
+ let handle;
40
+ try {
41
+ handle = await open(temporary, "wx", 0o600);
42
+ await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
43
+ await handle.sync();
44
+ await handle.close();
45
+ handle = undefined;
46
+ await link(temporary, path);
47
+ await syncDirectory(dirname(path));
48
+ }
49
+ finally {
50
+ await handle?.close().catch(() => undefined);
51
+ await rm(temporary, { force: true }).catch(() => undefined);
52
+ }
53
+ }
54
+ async function replaceJsonDurably(path, value) {
55
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
56
+ let handle;
57
+ try {
58
+ handle = await open(temporary, "wx", 0o600);
59
+ await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
60
+ await handle.sync();
61
+ await handle.close();
62
+ handle = undefined;
63
+ await rename(temporary, path);
64
+ await syncDirectory(dirname(path));
65
+ }
66
+ finally {
67
+ await handle?.close().catch(() => undefined);
68
+ await rm(temporary, { force: true }).catch(() => undefined);
69
+ }
70
+ }
71
+ function parseClaim(raw, expectedTicket) {
72
+ try {
73
+ const value = JSON.parse(raw);
74
+ if (value.version !== 1
75
+ || typeof value.ticket !== "string"
76
+ || !/^\d{20}$/.test(value.ticket)
77
+ || (expectedTicket !== undefined && value.ticket !== expectedTicket)
78
+ || typeof value.token !== "string"
79
+ || typeof value.pid !== "number"
80
+ || !Number.isSafeInteger(value.pid)
81
+ || value.pid <= 0
82
+ || typeof value.resource !== "string"
83
+ || typeof value.createdAt !== "string"
84
+ || (value.state !== "waiting" && value.state !== "released"))
85
+ return null;
86
+ return value;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ async function processIsAlive(pid) {
93
+ try {
94
+ process.kill(pid, 0);
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ return errorCode(error) === "EPERM";
99
+ }
100
+ }
101
+ async function wait(ms, signal) {
102
+ signal?.throwIfAborted();
103
+ await new Promise((resolveWait, reject) => {
104
+ const timer = setTimeout(done, ms);
105
+ function done() {
106
+ signal?.removeEventListener("abort", abort);
107
+ resolveWait();
108
+ }
109
+ function abort() {
110
+ clearTimeout(timer);
111
+ signal?.removeEventListener("abort", abort);
112
+ reject(signal?.reason ?? codedError("Lock acquisition aborted", "ABORT_ERR"));
113
+ }
114
+ signal?.addEventListener("abort", abort, { once: true });
115
+ });
116
+ }
117
+ async function allocateClaim(queuePath, resource) {
118
+ for (;;) {
119
+ const names = await readdir(queuePath);
120
+ let maximum = 0n;
121
+ for (const name of names) {
122
+ const match = CLAIM_NAME.exec(name);
123
+ if (match)
124
+ maximum = maximum > BigInt(match[1]) ? maximum : BigInt(match[1]);
125
+ }
126
+ const next = maximum + 1n;
127
+ if (next > MAX_TICKET)
128
+ throw codedError(`Lock ticket space exhausted for ${resource}`, "DURABLE_LOCK_EXHAUSTED");
129
+ const ticket = next.toString().padStart(20, "0");
130
+ const path = join(queuePath, `${ticket}.json`);
131
+ const wire = {
132
+ version: 1,
133
+ ticket,
134
+ token: randomUUID(),
135
+ pid: process.pid,
136
+ resource,
137
+ createdAt: new Date().toISOString(),
138
+ state: "waiting",
139
+ };
140
+ try {
141
+ await publishJsonExclusive(path, wire);
142
+ return { path, wire };
143
+ }
144
+ catch (error) {
145
+ if (errorCode(error) !== "EEXIST")
146
+ throw error;
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Cross-process FIFO lock without stale-owner unlink.
152
+ *
153
+ * Every contender atomically allocates a monotonically increasing immutable
154
+ * ticket. Released and dead-owner tickets stay as tiny tombstones, so no later
155
+ * process can recreate an older pathname or race a compare-then-unlink stale
156
+ * takeover. A dead process is skipped by later tickets; a malformed earlier
157
+ * ticket fails closed until an operator repairs it.
158
+ */
159
+ export async function acquireDurableTicketLock(requestedQueuePath, resource, options = {}) {
160
+ const queuePath = resolve(requestedQueuePath);
161
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 5_000);
162
+ const pollMs = Math.max(1, options.pollMs ?? 25);
163
+ const alive = options.isProcessAlive ?? processIsAlive;
164
+ const started = Date.now();
165
+ await ensureDirectory(queuePath);
166
+ const claim = await allocateClaim(queuePath, resource);
167
+ let released = false;
168
+ const release = async () => {
169
+ if (released)
170
+ return;
171
+ const current = parseClaim(await readFile(claim.path, "utf8").catch(() => ""), claim.wire.ticket);
172
+ if (!current || current.token !== claim.wire.token || current.resource !== resource) {
173
+ throw codedError(`Durable lock claim changed unexpectedly: ${claim.path}`, "DURABLE_LOCK_CONFLICT");
174
+ }
175
+ if (current.state !== "released") {
176
+ await replaceJsonDurably(claim.path, {
177
+ ...current,
178
+ state: "released",
179
+ releasedAt: new Date().toISOString(),
180
+ });
181
+ }
182
+ released = true;
183
+ };
184
+ try {
185
+ for (;;) {
186
+ options.signal?.throwIfAborted();
187
+ const own = parseClaim(await readFile(claim.path, "utf8").catch(() => ""), claim.wire.ticket);
188
+ if (!own || own.token !== claim.wire.token || own.resource !== resource || own.state !== "waiting") {
189
+ throw codedError(`Durable lock claim ownership was lost: ${claim.path}`, "DURABLE_LOCK_CONFLICT");
190
+ }
191
+ const names = (await readdir(queuePath)).filter((name) => CLAIM_NAME.test(name)).sort();
192
+ let blocked = false;
193
+ for (const name of names) {
194
+ const ticket = name.slice(0, 20);
195
+ if (ticket >= claim.wire.ticket)
196
+ break;
197
+ const prior = parseClaim(await readFile(join(queuePath, name), "utf8").catch(() => ""), ticket);
198
+ // A malformed ticket might be a durability fault in a live owner. It
199
+ // cannot be skipped safely, so acquisition times out without mutation.
200
+ if (!prior || (prior.state === "waiting" && await alive(prior.pid))) {
201
+ blocked = true;
202
+ break;
203
+ }
204
+ }
205
+ if (!blocked)
206
+ break;
207
+ if (Date.now() - started >= timeoutMs) {
208
+ throw codedError(`Timed out waiting for durable lock: ${resource}`, "DURABLE_LOCK_TIMEOUT");
209
+ }
210
+ await wait(Math.min(pollMs, Math.max(1, timeoutMs - (Date.now() - started))), options.signal);
211
+ }
212
+ return {
213
+ queuePath,
214
+ claimPath: claim.path,
215
+ ticket: claim.wire.ticket,
216
+ token: claim.wire.token,
217
+ assertOwned: async () => {
218
+ if (released)
219
+ throw codedError(`Durable lock was already released: ${resource}`, "DURABLE_LOCK_CONFLICT");
220
+ const current = parseClaim(await readFile(claim.path, "utf8").catch(() => ""), claim.wire.ticket);
221
+ if (!current || current.token !== claim.wire.token || current.resource !== resource || current.state !== "waiting") {
222
+ throw codedError(`Durable lock ownership was lost: ${resource}`, "DURABLE_LOCK_CONFLICT");
223
+ }
224
+ },
225
+ release,
226
+ };
227
+ }
228
+ catch (error) {
229
+ await release().catch(() => undefined);
230
+ throw error;
231
+ }
232
+ }
@@ -1,7 +1,7 @@
1
1
  import { type SyncRules } from "./sync-rules.js";
2
2
  import { type RetentionSettings } from "./retention.js";
3
3
  export type EnrollOptions = {
4
- /** The short-lived code shown in the account panel under Devices > Add device. */
4
+ /** The short-lived code shown in the account panel under Machines > Show a pairing code. */
5
5
  code: string;
6
6
  /** How this machine will show up in the device list. */
7
7
  label: string;
@@ -1,7 +1,7 @@
1
1
  import { fail } from "./errors.js";
2
2
  import { chooseBackend } from "./secrets.js";
3
3
  import { readConfig } from "./vault.js";
4
- import { DEFAULT_CLOUD_URL, tokenRefFor } from "./cloud.js";
4
+ import { commitCloudAuthCredentials, DEFAULT_CLOUD_URL, tokenRefFor } from "./cloud.js";
5
5
  import { cacheSyncRules, cachedSyncRules, DEFAULT_SYNC_RULES } from "./sync-rules.js";
6
6
  import { setRetentionPolicy, retentionSettings, DEFAULT_RETENTION } from "./retention.js";
7
7
  /**
@@ -58,7 +58,7 @@ export async function enroll(dataDir, options) {
58
58
  const code = options.code?.trim() ?? "";
59
59
  const label = options.label?.trim() ?? "";
60
60
  if (!code)
61
- fail("invalid_argument", "An enrollment code is required. Find it in the account panel under Devices > Add device.");
61
+ fail("invalid_argument", "An enrollment code is required. Find it in the account panel under Machines > Show a pairing code.");
62
62
  if (!label)
63
63
  fail("invalid_argument", `A device label is required, e.g. --label "work-laptop", so devices can be told apart later.`);
64
64
  // No vault means nowhere to keep the token or the settings it unlocks,
@@ -97,11 +97,17 @@ export async function enroll(dataDir, options) {
97
97
  fail("internal", `Sealkeep Cloud accepted code ${code} but did not return a device token. Nothing was stored; run \`sealkeep enroll ${code}\` again.`);
98
98
  }
99
99
  const enrolled = parsed;
100
- // Store the token before anything else. It is the one part of this that
101
- // cannot be recomputed locally, and every later cloud.ts call —
102
- // accountStatus, pushArchive, resolveSyncRules — reads it back from
103
- // exactly this service/account pair.
104
- await backend.set(ref, enrolled.token);
100
+ // Publish the token and its cache generation as one fail-closed transition.
101
+ // The pairing code is single-use, so a full disk must be detected before the
102
+ // credential changes; otherwise the machine could retain the new account
103
+ // under the previous account's cache scope.
104
+ await commitCloudAuthCredentials(dataDir, backend, [{ ref, value: enrolled.token }]);
105
+ // Claim a machine identity too. Without it this machine can seal and upload
106
+ // but cannot delete anything another machine sealed, because the control
107
+ // plane has no way to tell it apart from every other laptop on the account.
108
+ // Best-effort: enrolment succeeding matters more than being nameable.
109
+ const { registerMachine } = await import("./cloud.js");
110
+ await registerMachine(dataDir, enrolled.token);
105
111
  let settingsCached = true;
106
112
  let cacheWarning;
107
113
  let sync;
package/dist/src/env.d.ts CHANGED
@@ -6,8 +6,17 @@
6
6
  * break because we renamed a brand — but everything the product prints, writes,
7
7
  * and documents now says SEALKEEP_. When both are set the new name wins, so a
8
8
  * machine can be migrated one variable at a time.
9
+ *
10
+ * This fallback was deleted once, on the reasoning that one machine had no
11
+ * VAULTLINE_ variables set. That reasoning was wrong twice over: it generalised
12
+ * from a single machine, and it missed that `mcp install` WROTE the old names
13
+ * into agent config files that are still on disk. An agent spawned from one of
14
+ * those configs would have silently ignored its own configured vault and used
15
+ * the default instead — two vaults, no error, which is the exact failure this
16
+ * product has an acceptance test for.
9
17
  */
10
- /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era name. */
18
+ export declare const PREFIX = "SEALKEEP_";
19
+ /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era spelling. */
11
20
  export declare function envVar(name: string, env?: NodeJS.ProcessEnv): string | undefined;
12
21
  /** True when either spelling is set to something truthy. */
13
22
  export declare function envFlag(name: string, env?: NodeJS.ProcessEnv): boolean;
package/dist/src/env.js CHANGED
@@ -6,16 +6,24 @@
6
6
  * break because we renamed a brand — but everything the product prints, writes,
7
7
  * and documents now says SEALKEEP_. When both are set the new name wins, so a
8
8
  * machine can be migrated one variable at a time.
9
+ *
10
+ * This fallback was deleted once, on the reasoning that one machine had no
11
+ * VAULTLINE_ variables set. That reasoning was wrong twice over: it generalised
12
+ * from a single machine, and it missed that `mcp install` WROTE the old names
13
+ * into agent config files that are still on disk. An agent spawned from one of
14
+ * those configs would have silently ignored its own configured vault and used
15
+ * the default instead — two vaults, no error, which is the exact failure this
16
+ * product has an acceptance test for.
9
17
  */
18
+ export const PREFIX = "SEALKEEP_";
10
19
  const LEGACY_PREFIX = "VAULTLINE_";
11
- const PREFIX = "SEALKEEP_";
12
- /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era name. */
20
+ /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era spelling. */
13
21
  export function envVar(name, env = process.env) {
14
22
  const current = env[PREFIX + name];
15
23
  if (current !== undefined && current !== "")
16
24
  return current;
17
25
  const legacy = env[LEGACY_PREFIX + name];
18
- return legacy === "" ? undefined : legacy;
26
+ return legacy === undefined || legacy === "" ? undefined : legacy;
19
27
  }
20
28
  /** True when either spelling is set to something truthy. */
21
29
  export function envFlag(name, env = process.env) {
@@ -3,24 +3,24 @@
3
3
  * between releases; these codes are a contract for the CLI, local API, and MCP.
4
4
  * Details are operational only: never place plaintext, phrases, or keys here.
5
5
  */
6
- export type VaultlineErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "archive_not_found" | "source_unreadable" | "destination_exists" | "destination_unwritable" | "insufficient_disk_space" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "storage_not_configured" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "unauthorized" | "forbidden" | "internal";
7
- export declare class VaultlineError extends Error {
8
- readonly code: VaultlineErrorCode;
6
+ export type SealkeepErrorCode = "invalid_argument" | "vault_not_initialized" | "vault_already_initialized" | "config_unsupported_version" | "recovery_phrase_missing" | "recovery_phrase_mismatch" | "recipient_key_mismatch" | "payment_required" | "archive_not_found" | "source_unreadable" | "destination_exists" | "destination_unwritable" | "insufficient_disk_space" | "scratch_space_busy" | "path_not_permitted" | "native_restore_unavailable" | "ciphertext_integrity_failed" | "plaintext_integrity_failed" | "hook_payload_invalid" | "queue_job_not_found" | "queue_lease_invalid" | "queue_lease_expired" | "queue_job_exhausted" | "daemon_already_running" | "storage_not_configured" | "storage_read_timeout" | "operation_timeout" | "cloud_account_unavailable" | "provider_unsupported" | "signer_not_configured" | "lease_expired" | "upload_incomplete" | "team_state_changed" | "team_membership_revoked" | "rate_limited" | "unauthorized" | "forbidden" | "internal";
7
+ export declare class SealkeepError extends Error {
8
+ readonly code: SealkeepErrorCode;
9
9
  readonly details: Record<string, unknown>;
10
- constructor(code: VaultlineErrorCode, message: string, details?: Record<string, unknown>);
10
+ constructor(code: SealkeepErrorCode, message: string, details?: Record<string, unknown>);
11
11
  toJSON(): {
12
12
  error: {
13
- code: VaultlineErrorCode;
13
+ code: SealkeepErrorCode;
14
14
  message: string;
15
15
  };
16
16
  };
17
17
  }
18
- export declare function fail(code: VaultlineErrorCode, message: string, details?: Record<string, unknown>): never;
19
- export declare function isVaultlineError(value: unknown): value is VaultlineError;
18
+ export declare function fail(code: SealkeepErrorCode, message: string, details?: Record<string, unknown>): never;
19
+ export declare function isSealkeepError(value: unknown): value is SealkeepError;
20
20
  /** Safe wire/log shape. Unknown failures are never echoed with a stack. */
21
21
  export declare function errorPayload(error: unknown): {
22
22
  error: {
23
- code: VaultlineErrorCode;
23
+ code: SealkeepErrorCode;
24
24
  message: string;
25
25
  };
26
26
  };
@@ -1,23 +1,23 @@
1
- export class VaultlineError extends Error {
1
+ export class SealkeepError extends Error {
2
2
  code;
3
3
  details;
4
4
  constructor(code, message, details = {}) {
5
5
  super(message);
6
- this.name = "VaultlineError";
6
+ this.name = "SealkeepError";
7
7
  this.code = code;
8
8
  this.details = details;
9
9
  }
10
10
  toJSON() { return { error: { code: this.code, message: this.message, ...this.details } }; }
11
11
  }
12
12
  export function fail(code, message, details) {
13
- throw new VaultlineError(code, message, details);
13
+ throw new SealkeepError(code, message, details);
14
14
  }
15
- export function isVaultlineError(value) {
16
- return value instanceof VaultlineError;
15
+ export function isSealkeepError(value) {
16
+ return value instanceof SealkeepError;
17
17
  }
18
18
  /** Safe wire/log shape. Unknown failures are never echoed with a stack. */
19
19
  export function errorPayload(error) {
20
- if (isVaultlineError(error))
20
+ if (isSealkeepError(error))
21
21
  return error.toJSON();
22
22
  return { error: { code: "internal", message: error instanceof Error ? error.message : "Unexpected error" } };
23
23
  }
@@ -0,0 +1,12 @@
1
+ import { type FileHandle } from "node:fs/promises";
2
+ /** Flags that reject a symlink where a regular file is expected. */
3
+ export declare const FLUSH_NO_FOLLOW: number;
4
+ export type FlushHandle = {
5
+ handle: FileHandle;
6
+ writable: boolean;
7
+ };
8
+ /**
9
+ * A handle suitable for `handle.sync()`. `extraFlags` carries the caller's own
10
+ * guards (typically `FLUSH_NO_FOLLOW`); pass 0 for none.
11
+ */
12
+ export declare function openForFlush(path: string, extraFlags?: number): Promise<FlushHandle>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Opening a file in order to flush it.
3
+ *
4
+ * `fsync` on POSIX works through any descriptor, so durability code here used
5
+ * to open read-only — often with `O_NOFOLLOW`, because the thing being made
6
+ * durable must be the exact file that was just written and never a symlink
7
+ * planted in its place. Windows implements the same call as
8
+ * FlushFileBuffers, which requires write access: every one of those flushes
9
+ * failed there with EPERM, and with them every archive record write, every
10
+ * reclaim transaction and the Codex resume pointer.
11
+ *
12
+ * So: ask for write access, keep the caller's guard flags, and fall back to a
13
+ * read-only handle when the file genuinely cannot be opened for writing (a
14
+ * restored archive made read-only, a read-only mount). On POSIX the fallback
15
+ * still flushes; on Windows nothing more can be done for such a file, and a
16
+ * caller that must not silently lose durability checks the returned mode.
17
+ */
18
+ import { constants as fsConstants } from "node:fs";
19
+ import { open } from "node:fs/promises";
20
+ const NO_FOLLOW = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
21
+ /** Flags that reject a symlink where a regular file is expected. */
22
+ export const FLUSH_NO_FOLLOW = NO_FOLLOW;
23
+ /**
24
+ * A handle suitable for `handle.sync()`. `extraFlags` carries the caller's own
25
+ * guards (typically `FLUSH_NO_FOLLOW`); pass 0 for none.
26
+ */
27
+ export async function openForFlush(path, extraFlags = 0) {
28
+ try {
29
+ return { handle: await open(path, fsConstants.O_RDWR | extraFlags), writable: true };
30
+ }
31
+ catch (error) {
32
+ const code = error.code;
33
+ if (code !== "EACCES" && code !== "EPERM" && code !== "EROFS" && code !== "EISDIR")
34
+ throw error;
35
+ return { handle: await open(path, fsConstants.O_RDONLY | extraFlags), writable: false };
36
+ }
37
+ }
@@ -1,5 +1,5 @@
1
1
  import type { UploadOptions, UploadOutcome } from "./upload.js";
2
- import type { offloadArchives } from "./offload.js";
2
+ import type { FetchClient, offloadArchives } from "./offload.js";
3
3
  /**
4
4
  * The background service's sign of life.
5
5
  *
@@ -18,17 +18,69 @@ export type Heartbeat = {
18
18
  intervalMs: number;
19
19
  totals: {
20
20
  archived: number;
21
+ indexed?: number;
21
22
  uploaded: number;
22
23
  reclaimed: number;
23
24
  failed: number;
24
25
  };
25
26
  last: {
26
27
  archived: number;
28
+ indexed?: number;
27
29
  uploaded: number;
28
30
  reclaimed: number;
29
31
  failed: number;
30
32
  notes: string[];
31
33
  };
34
+ /** What this running build can do; absent on heartbeats written by older versions. */
35
+ capabilities?: {
36
+ uploads: boolean;
37
+ reclaim: boolean;
38
+ };
39
+ /** Derived search catch-up state; optional for heartbeats written by older versions. */
40
+ indexing?: {
41
+ state: "idle" | "building" | "retry-wait";
42
+ /** Current build attempt, or consecutive failed attempts while waiting. */
43
+ attempts: number;
44
+ startedAt?: string;
45
+ nextAttemptAt?: string;
46
+ /** Stable error name/code only — never an exception message or secret. */
47
+ lastError?: string;
48
+ };
49
+ /** Cloud access requests are independent of archive preservation. */
50
+ teamAccess?: {
51
+ state: "idle" | "working" | "attention";
52
+ failures: number;
53
+ lastAttemptAt?: string;
54
+ lastSuccessAt?: string;
55
+ /** Stable error code only; never an exception message, path, or token. */
56
+ lastError?: string;
57
+ };
58
+ /**
59
+ * The automatic recall lane (hook-context passes). Hooks fail open by
60
+ * design, so this is the one place a person can see that recall is dying
61
+ * instead of merely absent.
62
+ */
63
+ context?: {
64
+ state: "idle" | "working" | "attention";
65
+ /** Consecutive failed passes; reset by the next success. */
66
+ failures: number;
67
+ lastAttemptAt?: string;
68
+ lastSuccessAt?: string;
69
+ /** Stable reason only: stalled, absolute, or an error code; never request contents. */
70
+ lastError?: string;
71
+ };
72
+ /** Existing private history becoming usable by a newly connected team. */
73
+ backfill?: {
74
+ state: "idle" | "working" | "attention";
75
+ total: number;
76
+ indexed: number;
77
+ registered: number;
78
+ pending: number;
79
+ failed: number;
80
+ blocked: number;
81
+ /** Stable error name/code only; archive content and paths never appear. */
82
+ lastError?: string;
83
+ };
32
84
  paused: boolean;
33
85
  watching: string[];
34
86
  };
@@ -63,27 +115,49 @@ export type UploadOffloadOutcome = {
63
115
  offloaded: string[];
64
116
  /** Local bytes given back by offloading verified copies as the pass went. */
65
117
  freedBytes: number;
118
+ /** Missed managed team grants recovered after the upload loop. */
119
+ teamRegistered: number;
66
120
  };
121
+ type ArchiveCacheSweepOutcome = {
122
+ offloaded: string[];
123
+ freedBytes: number;
124
+ };
125
+ /**
126
+ * Gives the daemon a bounded, retry-cooled cleanup lane for encrypted local
127
+ * duplicates. This runs even when the upload backlog is empty, so a service
128
+ * upgrade or an earlier provider outage cannot strand old `.skarchive` files.
129
+ */
130
+ export declare function sweepExpiredArchiveCache(dataDir: string, options: {
131
+ cacheDays: number | null;
132
+ offload?: typeof offloadArchives;
133
+ maxArchives?: number;
134
+ now?: number;
135
+ freshlyVerified?: ReadonlySet<string>;
136
+ signal?: AbortSignal;
137
+ client?: FetchClient;
138
+ maxBytesPerSecond?: number;
139
+ }): Promise<ArchiveCacheSweepOutcome>;
67
140
  /**
68
141
  * The daemon's upload pass, finished properly: upload, verify, then offload.
69
142
  *
70
143
  * `uploadPending` alone leaves a sealed local copy of every archive it just
71
- * proved the bucket holds — on the machines this product is for, that is the
72
- * disk staying exactly as full as before. The dashboard's push already does
73
- * the whole job (see `runUploads` in local-api.ts): the moment an archive's
74
- * verify lands, its local blob is a duplicate, and offloading it right then
75
- * lets a full disk breathe with each verify instead of never.
144
+ * proved the bucket holds — on the machines this product is for, that cache
145
+ * must not grow forever. The visible per-machine cache setting decides when a
146
+ * verified duplicate leaves; a value of zero completes that cleanup in this
147
+ * same pass.
76
148
  *
77
149
  * Offload failure is tolerated per archive: the upload is still verified and
78
- * recorded, and the next offload sweep gets another chance. `freshlyVerified`
79
- * is what keeps this cheap — the verify happened milliseconds ago, so the
80
- * usual fetch-and-hash re-proof would double the egress of every push.
150
+ * recorded, and a bounded, backed-off sweep gets another chance. A fresh
151
+ * verify still receives one final streamed whole-chain proof at the actual
152
+ * local purge boundary when the cache setting is zero.
81
153
  *
82
- * Integration note: src/daemon.ts's tick still calls bare `uploadPending`;
83
- * switching it to this function is the remaining wiring (daemon.ts is outside
84
- * this change's file list).
154
+ * The daemon calls this combined pass so routing, remote verification, retry
155
+ * fairness, and reclaim all share one ordering boundary.
85
156
  */
86
157
  export declare function uploadAndOffloadPending(dataDir: string, options?: UploadOptions & {
87
158
  offload?: typeof offloadArchives;
159
+ maxArchives?: number;
160
+ maxOffloads?: number;
88
161
  }): Promise<UploadOffloadOutcome>;
89
162
  export declare function describeGap(seconds: number): string;
163
+ export {};