sealkeep 0.9.0 → 0.11.1

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 +252 -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.d.ts +1 -0
  16. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
  17. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  18. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  20. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  21. package/dist/site/index.html +1808 -1904
  22. package/dist/site/llms.txt +67 -0
  23. package/dist/site/trust/architecture-data-flow.html +53 -0
  24. package/dist/site/trust/audit-roadmap.html +37 -0
  25. package/dist/site/trust/deployment-responsibility.html +11 -0
  26. package/dist/site/trust/dpa-sample.html +30 -0
  27. package/dist/site/trust/release-provenance.html +21 -0
  28. package/dist/site/trust/subprocessors.html +15 -0
  29. package/dist/site/trust/threat-model.html +34 -0
  30. package/dist/site/trust/toms.html +41 -0
  31. package/dist/site/trust-document.css +32 -0
  32. package/dist/site/trust.html +73 -0
  33. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  34. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  35. package/dist/site/visual/index.html +18 -0
  36. package/dist/site.zip +0 -0
  37. package/dist/src/activity.d.ts +9 -0
  38. package/dist/src/activity.js +90 -1
  39. package/dist/src/adapters.d.ts +175 -5
  40. package/dist/src/adapters.js +961 -73
  41. package/dist/src/agent-context.d.ts +135 -0
  42. package/dist/src/agent-context.js +1059 -0
  43. package/dist/src/archive-copies.d.ts +47 -0
  44. package/dist/src/archive-copies.js +179 -0
  45. package/dist/src/audit.d.ts +1 -1
  46. package/dist/src/audit.js +29 -4
  47. package/dist/src/autopilot.d.ts +52 -7
  48. package/dist/src/autopilot.js +143 -25
  49. package/dist/src/background-bandwidth.d.ts +46 -0
  50. package/dist/src/background-bandwidth.js +301 -0
  51. package/dist/src/background-cpu.d.ts +82 -0
  52. package/dist/src/background-cpu.js +212 -0
  53. package/dist/src/background-worker-error.d.ts +12 -0
  54. package/dist/src/background-worker-error.js +18 -0
  55. package/dist/src/branding.d.ts +24 -1
  56. package/dist/src/branding.js +31 -1
  57. package/dist/src/bridge.d.ts +233 -0
  58. package/dist/src/bridge.js +604 -0
  59. package/dist/src/byte-stream.d.ts +91 -0
  60. package/dist/src/byte-stream.js +385 -0
  61. package/dist/src/chunk-store.d.ts +41 -8
  62. package/dist/src/chunk-store.js +161 -65
  63. package/dist/src/cli.js +1698 -163
  64. package/dist/src/cloud.d.ts +841 -31
  65. package/dist/src/cloud.js +3196 -277
  66. package/dist/src/context-background.d.ts +37 -0
  67. package/dist/src/context-background.js +309 -0
  68. package/dist/src/context-drain-child.d.ts +1 -0
  69. package/dist/src/context-drain-child.js +98 -0
  70. package/dist/src/context-reader.d.ts +118 -0
  71. package/dist/src/context-reader.js +447 -0
  72. package/dist/src/control-plane/auth.d.ts +32 -4
  73. package/dist/src/control-plane/auth.js +85 -24
  74. package/dist/src/control-plane/server.js +19 -6
  75. package/dist/src/control-plane.d.ts +17 -1
  76. package/dist/src/control-plane.js +32 -6
  77. package/dist/src/crypto.d.ts +1 -1
  78. package/dist/src/crypto.js +5 -5
  79. package/dist/src/daemon-lease.d.ts +70 -0
  80. package/dist/src/daemon-lease.js +420 -0
  81. package/dist/src/daemon.d.ts +94 -1
  82. package/dist/src/daemon.js +1082 -105
  83. package/dist/src/darwin-service-policy.d.ts +41 -0
  84. package/dist/src/darwin-service-policy.js +60 -0
  85. package/dist/src/dashboard-cli.js +15 -15
  86. package/dist/src/device-authorization.d.ts +37 -0
  87. package/dist/src/device-authorization.js +199 -0
  88. package/dist/src/device-enrollment.d.ts +91 -0
  89. package/dist/src/device-enrollment.js +349 -0
  90. package/dist/src/disk.d.ts +17 -12
  91. package/dist/src/disk.js +43 -17
  92. package/dist/src/doctor.d.ts +35 -1
  93. package/dist/src/doctor.js +332 -41
  94. package/dist/src/durable-ticket-lock.d.ts +24 -0
  95. package/dist/src/durable-ticket-lock.js +232 -0
  96. package/dist/src/enroll.d.ts +1 -1
  97. package/dist/src/enroll.js +13 -7
  98. package/dist/src/env.d.ts +10 -1
  99. package/dist/src/env.js +11 -3
  100. package/dist/src/errors.d.ts +8 -8
  101. package/dist/src/errors.js +6 -6
  102. package/dist/src/flush.d.ts +12 -0
  103. package/dist/src/flush.js +37 -0
  104. package/dist/src/heartbeat.d.ts +86 -12
  105. package/dist/src/heartbeat.js +415 -29
  106. package/dist/src/index-background-watchdog.d.ts +1 -0
  107. package/dist/src/index-background-watchdog.js +94 -0
  108. package/dist/src/index-background-work.d.ts +21 -0
  109. package/dist/src/index-background-work.js +25 -0
  110. package/dist/src/index-background.d.ts +64 -0
  111. package/dist/src/index-background.js +394 -0
  112. package/dist/src/index-build-child.d.ts +1 -0
  113. package/dist/src/index-build-child.js +109 -0
  114. package/dist/src/index-manifest.d.ts +52 -0
  115. package/dist/src/index-manifest.js +444 -0
  116. package/dist/src/index-publication-proof.d.ts +84 -0
  117. package/dist/src/index-publication-proof.js +380 -0
  118. package/dist/src/index-publication-state.d.ts +149 -0
  119. package/dist/src/index-publication-state.js +696 -0
  120. package/dist/src/index-publication-verifier.d.ts +89 -0
  121. package/dist/src/index-publication-verifier.js +341 -0
  122. package/dist/src/index-publish.d.ts +62 -0
  123. package/dist/src/index-publish.js +540 -0
  124. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  125. package/dist/src/index-scratch-cleanup.js +166 -0
  126. package/dist/src/index-segment-types.d.ts +132 -0
  127. package/dist/src/index-segment-types.js +21 -0
  128. package/dist/src/index-segments.d.ts +9 -0
  129. package/dist/src/index-segments.js +516 -0
  130. package/dist/src/index-store.d.ts +123 -0
  131. package/dist/src/index-store.js +495 -0
  132. package/dist/src/index-sync.d.ts +91 -15
  133. package/dist/src/index-sync.js +286 -53
  134. package/dist/src/index-upgrade-publication.d.ts +30 -0
  135. package/dist/src/index-upgrade-publication.js +179 -0
  136. package/dist/src/integration-manager.d.ts +32 -0
  137. package/dist/src/integration-manager.js +394 -0
  138. package/dist/src/leakscan.js +1 -1
  139. package/dist/src/local-api.d.ts +56 -15
  140. package/dist/src/local-api.js +4979 -541
  141. package/dist/src/machine-settings.d.ts +51 -0
  142. package/dist/src/machine-settings.js +166 -0
  143. package/dist/src/managed-chunks.d.ts +5 -2
  144. package/dist/src/managed-chunks.js +14 -14
  145. package/dist/src/mcp-install.d.ts +11 -9
  146. package/dist/src/mcp-install.js +73 -28
  147. package/dist/src/mcp-workspace.d.ts +18 -0
  148. package/dist/src/mcp-workspace.js +50 -0
  149. package/dist/src/mcp.js +294 -25
  150. package/dist/src/migrate.js +27 -21
  151. package/dist/src/notify.d.ts +1 -1
  152. package/dist/src/notify.js +5 -5
  153. package/dist/src/offload.d.ts +201 -14
  154. package/dist/src/offload.js +1848 -140
  155. package/dist/src/onboarding.d.ts +8 -1
  156. package/dist/src/onboarding.js +4 -4
  157. package/dist/src/packages.d.ts +2 -2
  158. package/dist/src/packages.js +10 -2
  159. package/dist/src/passkey.d.ts +0 -1
  160. package/dist/src/passkey.js +2 -7
  161. package/dist/src/password-lock.d.ts +2 -2
  162. package/dist/src/password-lock.js +6 -6
  163. package/dist/src/paths.d.ts +2 -0
  164. package/dist/src/paths.js +2 -0
  165. package/dist/src/presence.d.ts +86 -0
  166. package/dist/src/presence.js +240 -0
  167. package/dist/src/progress-deadline.d.ts +21 -0
  168. package/dist/src/progress-deadline.js +91 -0
  169. package/dist/src/project-repair.d.ts +55 -0
  170. package/dist/src/project-repair.js +131 -0
  171. package/dist/src/providers/gcs.d.ts +28 -7
  172. package/dist/src/providers/gcs.js +35 -24
  173. package/dist/src/providers/gdrive.d.ts +71 -8
  174. package/dist/src/providers/gdrive.js +223 -50
  175. package/dist/src/providers/index.d.ts +11 -3
  176. package/dist/src/providers/index.js +30 -10
  177. package/dist/src/providers/s3.d.ts +30 -8
  178. package/dist/src/providers/s3.js +41 -30
  179. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  180. package/dist/src/providers/safe-storage-fetch.js +72 -0
  181. package/dist/src/queue.d.ts +136 -19
  182. package/dist/src/queue.js +862 -96
  183. package/dist/src/reclaim-transaction.d.ts +156 -0
  184. package/dist/src/reclaim-transaction.js +1027 -0
  185. package/dist/src/recovery-codes.d.ts +32 -0
  186. package/dist/src/recovery-codes.js +338 -0
  187. package/dist/src/recovery.js +12 -9
  188. package/dist/src/rehydrate.d.ts +25 -22
  189. package/dist/src/rehydrate.js +319 -23
  190. package/dist/src/restore.d.ts +57 -4
  191. package/dist/src/restore.js +272 -36
  192. package/dist/src/resume-stub.d.ts +92 -0
  193. package/dist/src/resume-stub.js +417 -0
  194. package/dist/src/retention.d.ts +98 -7
  195. package/dist/src/retention.js +1066 -62
  196. package/dist/src/rotate.js +3 -3
  197. package/dist/src/search.d.ts +566 -8
  198. package/dist/src/search.js +5940 -297
  199. package/dist/src/secrets.d.ts +51 -7
  200. package/dist/src/secrets.js +316 -24
  201. package/dist/src/service.d.ts +49 -11
  202. package/dist/src/service.js +776 -35
  203. package/dist/src/share.js +3 -3
  204. package/dist/src/shared-spaces.d.ts +98 -0
  205. package/dist/src/shared-spaces.js +214 -0
  206. package/dist/src/source-reader.d.ts +73 -0
  207. package/dist/src/source-reader.js +715 -0
  208. package/dist/src/spool.d.ts +1 -1
  209. package/dist/src/spool.js +1 -1
  210. package/dist/src/start-tui.js +2 -1
  211. package/dist/src/start.js +2 -2
  212. package/dist/src/storage-endpoint.d.ts +21 -0
  213. package/dist/src/storage-endpoint.js +122 -0
  214. package/dist/src/storage-setup.js +12 -12
  215. package/dist/src/storage-targets.d.ts +109 -6
  216. package/dist/src/storage-targets.js +975 -67
  217. package/dist/src/stream-to-cloud.d.ts +5 -1
  218. package/dist/src/stream-to-cloud.js +34 -14
  219. package/dist/src/sync-rules.d.ts +31 -6
  220. package/dist/src/sync-rules.js +153 -14
  221. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  222. package/dist/src/team-backfill-scheduling.js +33 -0
  223. package/dist/src/team-backfill.d.ts +116 -0
  224. package/dist/src/team-backfill.js +1429 -0
  225. package/dist/src/team-index-cache.d.ts +16 -0
  226. package/dist/src/team-index-cache.js +152 -0
  227. package/dist/src/team-offboarding.d.ts +38 -0
  228. package/dist/src/team-offboarding.js +1043 -0
  229. package/dist/src/team-presence.d.ts +127 -0
  230. package/dist/src/team-presence.js +904 -0
  231. package/dist/src/team-publication-policy.d.ts +20 -0
  232. package/dist/src/team-publication-policy.js +140 -0
  233. package/dist/src/team-realtime.d.ts +68 -0
  234. package/dist/src/team-realtime.js +816 -0
  235. package/dist/src/team-source-facts-cache.d.ts +23 -0
  236. package/dist/src/team-source-facts-cache.js +255 -0
  237. package/dist/src/trash.d.ts +1 -1
  238. package/dist/src/trash.js +2 -2
  239. package/dist/src/tui.js +11 -12
  240. package/dist/src/types.d.ts +173 -7
  241. package/dist/src/types.js +20 -0
  242. package/dist/src/ui-server.d.ts +163 -35
  243. package/dist/src/ui-server.js +712 -72
  244. package/dist/src/ui.d.ts +1 -2
  245. package/dist/src/ui.js +1 -2
  246. package/dist/src/upload.d.ts +27 -0
  247. package/dist/src/upload.js +383 -43
  248. package/dist/src/vault.d.ts +226 -30
  249. package/dist/src/vault.js +1776 -192
  250. package/dist/src/watcher.d.ts +7 -1
  251. package/dist/src/watcher.js +198 -55
  252. package/dist/src/worker.d.ts +27 -3
  253. package/dist/src/worker.js +274 -55
  254. package/package.json +33 -12
  255. package/scripts/native-reboot-rehearsal.mjs +90 -0
  256. package/web/app.js +6032 -343
  257. package/web/bootstrap.js +17 -0
  258. package/web/index.html +255 -57
  259. package/web/rail.js +317 -40
  260. package/web/retention.html +2 -2
  261. package/web/rules-view.js +188 -16
  262. package/web/sessions-view.js +485 -62
  263. package/web/sessions.html +2 -2
  264. package/web/setup-api.js +152 -29
  265. package/web/setup-logic.js +68 -9
  266. package/web/setup.html +113 -44
  267. package/web/setup.js +604 -71
  268. package/web/style.css +513 -98
  269. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +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,41 @@
1
+ type PolicyExec = (file: string, args: string[]) => Promise<unknown>;
2
+ export type DarwinServicePolicyOptions = {
3
+ /** True only for the daemon child of Sealkeep's package-independent manager. */
4
+ serviceOwned: boolean;
5
+ /** Dependency seams keep non-Darwin and live-process tests hermetic. */
6
+ platform?: NodeJS.Platform;
7
+ pid?: number;
8
+ exec?: PolicyExec;
9
+ };
10
+ export type LegacyDarwinManagerHandoffOptions = {
11
+ serviceOwned: boolean;
12
+ platform?: NodeJS.Platform;
13
+ environment?: NodeJS.ProcessEnv;
14
+ /** Rewrites the exact already-owned manager, receipt, and native unit. */
15
+ reconcile: () => Promise<boolean>;
16
+ };
17
+ /**
18
+ * Prepare the one upgrade generation still launched by a pre-policy manager.
19
+ *
20
+ * Returning true means durable reconciliation completed and the caller must
21
+ * exit 75 before acquiring a UI or worker lease. The old manager already
22
+ * propagates that non-zero child result, so launchd starts the newly written
23
+ * manager, whose generation marker makes this gate a one-shot operation.
24
+ */
25
+ export declare function prepareLegacyDarwinManagerHandoff(options: LegacyDarwinManagerHandoffOptions): Promise<boolean>;
26
+ /**
27
+ * Remove the legacy launchd `ProcessType=Background` runtime policy from the
28
+ * already-running daemon without restarting it.
29
+ *
30
+ * Older plist generations marked the whole service as background even though
31
+ * that process also owns the user-facing loopback UI. Reconciliation writes
32
+ * the corrected plist for future launches, but launchd does not apply a plist
33
+ * edit to a process it already started. `taskpolicy -B` changes only Darwin's
34
+ * PRIO_DARWIN_BG flag for this exact process; it does not change the separate
35
+ * Nice=5 or LowPriorityIO settings which keep archive work deferential.
36
+ *
37
+ * This is deliberately best-effort. A missing or restricted taskpolicy must
38
+ * never prevent memory preservation or the recovery/settings UI from opening.
39
+ */
40
+ export declare function normalizeDarwinServicePolicy(options: DarwinServicePolicyOptions): Promise<boolean>;
41
+ export {};
@@ -0,0 +1,60 @@
1
+ import { execFile } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { promisify } from "node:util";
4
+ import { INTEGRATION_MANAGER_GENERATION, INTEGRATION_MANAGER_GENERATION_ENV, } from "./integration-manager.js";
5
+ const run = promisify(execFile);
6
+ /**
7
+ * Prepare the one upgrade generation still launched by a pre-policy manager.
8
+ *
9
+ * Returning true means durable reconciliation completed and the caller must
10
+ * exit 75 before acquiring a UI or worker lease. The old manager already
11
+ * propagates that non-zero child result, so launchd starts the newly written
12
+ * manager, whose generation marker makes this gate a one-shot operation.
13
+ */
14
+ export async function prepareLegacyDarwinManagerHandoff(options) {
15
+ const environment = options.environment ?? process.env;
16
+ if (!options.serviceOwned
17
+ || (options.platform ?? platform()) !== "darwin"
18
+ || environment[INTEGRATION_MANAGER_GENERATION_ENV] === INTEGRATION_MANAGER_GENERATION)
19
+ return false;
20
+ try {
21
+ return await options.reconcile();
22
+ }
23
+ catch {
24
+ // The runtime normalization below is still a safe fallback. Never turn a
25
+ // service-definition inspection failure into an unavailable local UI.
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * Remove the legacy launchd `ProcessType=Background` runtime policy from the
31
+ * already-running daemon without restarting it.
32
+ *
33
+ * Older plist generations marked the whole service as background even though
34
+ * that process also owns the user-facing loopback UI. Reconciliation writes
35
+ * the corrected plist for future launches, but launchd does not apply a plist
36
+ * edit to a process it already started. `taskpolicy -B` changes only Darwin's
37
+ * PRIO_DARWIN_BG flag for this exact process; it does not change the separate
38
+ * Nice=5 or LowPriorityIO settings which keep archive work deferential.
39
+ *
40
+ * This is deliberately best-effort. A missing or restricted taskpolicy must
41
+ * never prevent memory preservation or the recovery/settings UI from opening.
42
+ */
43
+ export async function normalizeDarwinServicePolicy(options) {
44
+ if (!options.serviceOwned || (options.platform ?? platform()) !== "darwin")
45
+ return false;
46
+ const pid = options.pid ?? process.pid;
47
+ if (!Number.isSafeInteger(pid) || pid <= 0)
48
+ return false;
49
+ const exec = options.exec ?? ((file, args) => run(file, args, {
50
+ timeout: 2_000,
51
+ killSignal: "SIGTERM",
52
+ }));
53
+ try {
54
+ await exec("/usr/sbin/taskpolicy", ["-B", "-p", String(pid)]);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
- // `npm run dashboard` is a thin alias for the local API, which serves the dashboard
3
- // from the same origin. Keeping one server means one authentication path and no
4
- // second copy of the business logic.
2
+ // `npm run dashboard` is a thin alias for the singleton local UI. Keeping one
3
+ // owner per vault means the development shortcut cannot create a second process
4
+ // that races the installed service's upload/reclaim journals.
5
5
  import { defaultDataDir } from "./vault.js";
6
- import { createLocalApiServer, localApiToken, localApiTokenPath } from "./local-api.js";
7
- import { freePort } from "./net.js";
6
+ import { localApiTokenPath } from "./local-api.js";
8
7
  import { envVar } from "./env.js";
8
+ import { startUi } from "./ui-server.js";
9
9
  const dataDir = envVar("DATA_DIR") ?? defaultDataDir();
10
10
  async function listen() {
11
- const token = await localApiToken(dataDir);
12
- const server = createLocalApiServer(dataDir, token);
13
- const port = await freePort(Number(process.env.PORT ?? "4173"));
14
- server.once("error", (error) => { console.error(`Unable to start dashboard on port ${port}: ${error.message}`); process.exitCode = 1; });
15
- server.listen(port, "127.0.0.1", () => {
16
- // The token travels in the URL fragment, which the browser never sends to a server.
17
- console.log(`Sealkeep dashboard: http://127.0.0.1:${port}/#token=${token}`);
18
- console.log(`Token file (do not share): ${localApiTokenPath(dataDir)}`);
19
- });
11
+ const requestedPort = Number(process.env.PORT ?? "4173");
12
+ const ui = await startUi(dataDir, { port: requestedPort, openBrowser: false, owner: "interactive" });
13
+ console.log(`Sealkeep dashboard: ${ui.url}`);
14
+ console.log(`Token file (do not share): ${localApiTokenPath(dataDir)}`);
15
+ if (ui.reused)
16
+ console.log("Using the Sealkeep local UI that already owns this vault.");
20
17
  }
21
- void listen();
18
+ void listen().catch((error) => {
19
+ console.error(`Unable to start dashboard: ${error instanceof Error ? error.message : String(error)}`);
20
+ process.exitCode = 1;
21
+ });
@@ -0,0 +1,37 @@
1
+ import { type CloudAccount, type CloudDeviceAuthorizationMemory } from "./cloud.js";
2
+ import { type DeviceEnrollmentDraft } from "./device-enrollment.js";
3
+ import { type BackendName } from "./secrets.js";
4
+ export type DeviceOsFamily = "macos" | "linux" | "windows";
5
+ export type DeviceAuthorizationStart = {
6
+ requestId: string;
7
+ approvalUrl: string;
8
+ expiresAt: string;
9
+ pollAfterMs: number;
10
+ status: "waiting_for_approval";
11
+ };
12
+ /** The browser needs a comparison code and timing, never even the target's public key. */
13
+ type PublicEnrollment = Pick<DeviceEnrollmentDraft, "requestId" | "label" | "verificationCode" | "createdAt" | "expiresAt"> & {
14
+ status: "waiting_for_approval";
15
+ };
16
+ export type DeviceAuthorizationCompletion = {
17
+ status: "waiting_for_approval" | "denied" | "expired" | "complete";
18
+ expiresAt?: string;
19
+ retryAfterMs?: number;
20
+ account?: {
21
+ plan: CloudAccount["plan"];
22
+ email: string;
23
+ quotaBytes: number;
24
+ usedBytes: number;
25
+ };
26
+ memory?: CloudDeviceAuthorizationMemory;
27
+ enrollment?: PublicEnrollment;
28
+ };
29
+ export declare function currentOsFamily(value?: NodeJS.Platform): DeviceOsFamily;
30
+ export declare function publicDetectedAgents(values: readonly string[]): string[];
31
+ export declare function beginDeviceAuthorization(dataDir: string, input: {
32
+ label: string;
33
+ osFamily?: DeviceOsFamily;
34
+ agents?: string[];
35
+ }, preferred?: BackendName): Promise<DeviceAuthorizationStart>;
36
+ export declare function completeDeviceAuthorization(dataDir: string, requestId: string, preferred?: BackendName): Promise<DeviceAuthorizationCompletion>;
37
+ export {};
@@ -0,0 +1,199 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { platform } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { claimCloudDeviceAuthorization, cloudUrl, rememberCloudDeviceAuthorization, startCloudDeviceAuthorization, } from "./cloud.js";
5
+ import { generateDeviceEnrollmentKeys, rememberHostedDeviceEnrollment, } from "./device-enrollment.js";
6
+ import { fail } from "./errors.js";
7
+ import { chooseBackend } from "./secrets.js";
8
+ const STATE_SERVICE = "sealkeep-device-authorization";
9
+ const REQUEST_ID = /^[A-Za-z0-9._~-]{1,200}$/;
10
+ const SAFE_AGENT_IDS = new Set(["codex", "claude", "cursor", "gemini", "windsurf", "antigravity"]);
11
+ function publicEnrollment(draft) {
12
+ return {
13
+ requestId: draft.requestId,
14
+ label: draft.label,
15
+ verificationCode: draft.verificationCode,
16
+ createdAt: draft.createdAt,
17
+ expiresAt: draft.expiresAt,
18
+ status: "waiting_for_approval",
19
+ };
20
+ }
21
+ export function currentOsFamily(value = platform()) {
22
+ if (value === "darwin")
23
+ return "macos";
24
+ if (value === "win32")
25
+ return "windows";
26
+ return "linux";
27
+ }
28
+ export function publicDetectedAgents(values) {
29
+ return [...new Set(values.filter((value) => SAFE_AGENT_IDS.has(value)))].sort();
30
+ }
31
+ function stateRef(dataDir) {
32
+ const scope = createHash("sha256").update(resolve(dataDir)).digest("hex");
33
+ return { service: STATE_SERVICE, account: `${scope}@${cloudUrl()}` };
34
+ }
35
+ const s256 = (verifier) => createHash("sha256").update(verifier).digest("base64url");
36
+ function normalPoll(value) {
37
+ const number = Number(value);
38
+ return Number.isFinite(number) ? Math.min(10_000, Math.max(1_000, Math.round(number))) : 3_000;
39
+ }
40
+ function assertApprovalUrl(value) {
41
+ if (typeof value !== "string")
42
+ return fail("internal", "Sealkeep Cloud did not return an account approval page");
43
+ let url;
44
+ try {
45
+ url = new URL(value);
46
+ }
47
+ catch {
48
+ return fail("internal", "Sealkeep Cloud returned an invalid account approval page");
49
+ }
50
+ if (url.protocol !== "https:" || url.username || url.password) {
51
+ return fail("internal", "Sealkeep Cloud returned an unsafe account approval page");
52
+ }
53
+ // The opaque request id may identify the browser approval, but credentials,
54
+ // verifier material and one-time codes never belong in a browser URL.
55
+ if (/(?:token|secret|verifier|machine_key|account_token|pairing_code)=/i.test(`${url.search}&${url.hash}`)) {
56
+ return fail("internal", "Sealkeep Cloud put secret material in the account approval URL");
57
+ }
58
+ return url.toString();
59
+ }
60
+ async function rememberDraft(dataDir, draft, preferred) {
61
+ const backend = await chooseBackend(dataDir, preferred);
62
+ await backend.set(stateRef(dataDir), JSON.stringify(draft));
63
+ }
64
+ async function readDraft(dataDir, preferred) {
65
+ const backend = await chooseBackend(dataDir, preferred);
66
+ const raw = await backend.get(stateRef(dataDir));
67
+ if (!raw)
68
+ return null;
69
+ try {
70
+ const value = JSON.parse(raw);
71
+ if (value.version !== 1 || typeof value.requestId !== "string" || !REQUEST_ID.test(value.requestId))
72
+ return null;
73
+ if (typeof value.verifier !== "string" || value.verifier.length < 32)
74
+ return null;
75
+ if (!value.keys || typeof value.keys.publicKey !== "string" || typeof value.keys.privateKey !== "string")
76
+ return null;
77
+ if (typeof value.label !== "string" || !value.label.trim())
78
+ return null;
79
+ if (typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)))
80
+ return null;
81
+ return value;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ async function forgetDraft(dataDir, preferred) {
88
+ const backend = await chooseBackend(dataDir, preferred);
89
+ await backend.delete(stateRef(dataDir)).catch(() => undefined);
90
+ }
91
+ export async function beginDeviceAuthorization(dataDir, input, preferred) {
92
+ const label = input.label.trim();
93
+ if (!label || label.length > 80)
94
+ fail("invalid_argument", "Name this machine with 1 to 80 characters");
95
+ const verifier = randomBytes(32).toString("base64url");
96
+ const keys = generateDeviceEnrollmentKeys();
97
+ const created = await startCloudDeviceAuthorization({
98
+ challenge: s256(verifier),
99
+ label,
100
+ osFamily: input.osFamily ?? currentOsFamily(),
101
+ agents: publicDetectedAgents(input.agents ?? []),
102
+ publicKey: keys.publicKey,
103
+ });
104
+ const requestId = String(created?.request_id ?? "");
105
+ if (!REQUEST_ID.test(requestId))
106
+ fail("internal", "Sealkeep Cloud returned an invalid device authorization request");
107
+ const approvalUrl = assertApprovalUrl(created.approval_url);
108
+ const expiresAt = String(created.expires_at ?? "");
109
+ if (!Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now()) {
110
+ fail("internal", "Sealkeep Cloud returned an expired device authorization request");
111
+ }
112
+ const draft = {
113
+ version: 1,
114
+ requestId,
115
+ verifier,
116
+ label,
117
+ keys,
118
+ createdAt: created.created_at && Number.isFinite(Date.parse(created.created_at)) ? created.created_at : new Date().toISOString(),
119
+ expiresAt,
120
+ pollAfterMs: normalPoll(created.poll_after_ms),
121
+ };
122
+ try {
123
+ await rememberDraft(dataDir, draft, preferred);
124
+ }
125
+ catch (error) {
126
+ fail("internal", `This machine could not protect its authorization request locally: ${error instanceof Error ? error.message : "secret backend refused it"}`);
127
+ }
128
+ return { requestId, approvalUrl, expiresAt, pollAfterMs: draft.pollAfterMs, status: "waiting_for_approval" };
129
+ }
130
+ function publicAccount(account) {
131
+ return {
132
+ plan: account.plan,
133
+ email: account.email,
134
+ quotaBytes: account.quota_bytes,
135
+ usedBytes: account.used_bytes,
136
+ };
137
+ }
138
+ export async function completeDeviceAuthorization(dataDir, requestId, preferred) {
139
+ if (!REQUEST_ID.test(requestId))
140
+ fail("invalid_argument", "That device authorization request is not valid");
141
+ const draft = await readDraft(dataDir, preferred);
142
+ if (!draft || draft.requestId !== requestId) {
143
+ fail("archive_not_found", "This machine no longer holds that account authorization request. Start a new one.");
144
+ }
145
+ if (Date.parse(draft.expiresAt) <= Date.now()) {
146
+ await forgetDraft(dataDir, preferred);
147
+ return { status: "expired", expiresAt: draft.expiresAt };
148
+ }
149
+ const claimed = await claimCloudDeviceAuthorization({ requestId, verifier: draft.verifier });
150
+ if (claimed.status === "pending") {
151
+ return {
152
+ status: "waiting_for_approval",
153
+ expiresAt: claimed.expires_at ?? draft.expiresAt,
154
+ retryAfterMs: normalPoll(claimed.poll_after_ms ?? draft.pollAfterMs),
155
+ };
156
+ }
157
+ if (claimed.status === "denied" || claimed.status === "declined" || claimed.status === "expired") {
158
+ await forgetDraft(dataDir, preferred);
159
+ return {
160
+ // Older hosted requests could still answer `declined`; the loopback
161
+ // contract intentionally has one public denial state.
162
+ status: claimed.status === "expired" ? "expired" : "denied",
163
+ expiresAt: claimed.expires_at ?? draft.expiresAt,
164
+ };
165
+ }
166
+ const accountToken = claimed.account_token ?? claimed.token;
167
+ if (claimed.status !== "approved" || typeof accountToken !== "string" || !accountToken || !claimed.machine || !claimed.account || !claimed.memory) {
168
+ fail("internal", "Sealkeep Cloud returned an incomplete device authorization");
169
+ }
170
+ if (!Number.isSafeInteger(claimed.machine.device_id) || claimed.machine.device_id < 1 || !claimed.machine.machine_key) {
171
+ fail("internal", "Sealkeep Cloud returned an invalid machine credential");
172
+ }
173
+ if (typeof claimed.memory.preserved !== "boolean" || !Number.isFinite(claimed.memory.bytes) || claimed.memory.bytes < 0) {
174
+ fail("internal", "Sealkeep Cloud returned an invalid preserved-memory summary");
175
+ }
176
+ await rememberCloudDeviceAuthorization(dataDir, {
177
+ accountToken,
178
+ machine: claimed.machine,
179
+ }, preferred);
180
+ let enrollment;
181
+ if (claimed.memory.preserved && claimed.enrollment) {
182
+ const started = await rememberHostedDeviceEnrollment(dataDir, {
183
+ requestId: claimed.enrollment.request_id,
184
+ requestSecret: claimed.enrollment.request_secret,
185
+ label: draft.label,
186
+ createdAt: claimed.enrollment.created_at,
187
+ expiresAt: claimed.enrollment.expires_at,
188
+ keys: draft.keys,
189
+ }, preferred);
190
+ enrollment = publicEnrollment(started);
191
+ }
192
+ await forgetDraft(dataDir, preferred);
193
+ return {
194
+ status: "complete",
195
+ account: publicAccount(claimed.account),
196
+ memory: claimed.memory,
197
+ ...(enrollment ? { enrollment } : {}),
198
+ };
199
+ }
@@ -0,0 +1,91 @@
1
+ import { type Envelope } from "../packages/sealkeep-crypto/src/index.js";
2
+ import { type CloudDeviceEnrollmentRequest } from "./cloud.js";
3
+ import { type BackendName } from "./secrets.js";
4
+ import type { VaultConfig } from "./types.js";
5
+ export type DeviceEnrollmentDraft = {
6
+ version: 1;
7
+ requestId: number;
8
+ label: string;
9
+ publicKey: string;
10
+ privateKey: string;
11
+ requestSecret: string;
12
+ verificationCode: string;
13
+ createdAt: string;
14
+ expiresAt: string;
15
+ };
16
+ export type PersonalVaultMaterial = {
17
+ vaultId: string;
18
+ recoveryPhrase: string;
19
+ storageMode: "sealed" | "plain";
20
+ recipients?: VaultConfig["recipients"];
21
+ remoteNaming?: VaultConfig["remoteNaming"];
22
+ };
23
+ export type DeviceEnrollmentCompletion = {
24
+ requestId: number;
25
+ vaultId: string;
26
+ verificationCode: string;
27
+ cloudReceiptCleared: boolean;
28
+ };
29
+ /** A short comparison code derived from the key itself, never supplied by Cloud. */
30
+ export declare function deviceEnrollmentVerificationCode(publicKey: string): string;
31
+ export type DeviceEnrollmentKeyPair = {
32
+ publicKey: string;
33
+ privateKey: string;
34
+ };
35
+ export declare function generateDeviceEnrollmentKeys(): DeviceEnrollmentKeyPair;
36
+ export declare function readDeviceEnrollmentDraft(dataDir: string, requestId: number, preferred?: BackendName): Promise<DeviceEnrollmentDraft | null>;
37
+ /**
38
+ * Starts the target side after ordinary Cloud sign-in. The request secret is a
39
+ * capability for claiming this one response; it is not a vault key and cannot
40
+ * decrypt any archive.
41
+ */
42
+ export declare function beginDeviceEnrollment(dataDir: string, label: string, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
43
+ /**
44
+ * Starts the existing-vault approval lane with a key pair prepared before
45
+ * hosted account authorization. Reusing that exact public key means the
46
+ * account page and the existing device approve the same target, instead of
47
+ * silently changing identity between the two steps.
48
+ */
49
+ export declare function beginDeviceEnrollmentWithKeys(dataDir: string, label: string, keys: DeviceEnrollmentKeyPair, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
50
+ /**
51
+ * Persists a request the hosted approval endpoint already created from the
52
+ * public key supplied at authorization start. The returned request secret is
53
+ * consumed only here, over the verifier-protected claim response, and then
54
+ * remains in the native secret backend with the private key.
55
+ */
56
+ export declare function rememberHostedDeviceEnrollment(dataDir: string, input: {
57
+ requestId: number;
58
+ requestSecret: string;
59
+ label: string;
60
+ createdAt: string;
61
+ expiresAt: string;
62
+ keys: DeviceEnrollmentKeyPair;
63
+ }, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
64
+ export declare function sealDeviceEnrollmentPayload(config: VaultConfig, phrase: string, targetPublicKey: string): {
65
+ envelope: Envelope;
66
+ sealed: string;
67
+ };
68
+ /**
69
+ * Existing-device side. Requiring the independently displayed code prevents a
70
+ * Cloud-side public-key substitution from being approved silently.
71
+ */
72
+ export declare function approveDeviceEnrollment(dataDir: string, requestId: number, displayedCode: string, preferred?: BackendName): Promise<{
73
+ approved: true;
74
+ requestId: number;
75
+ label: string;
76
+ }>;
77
+ /**
78
+ * Read-only adoption gate. Recovery codes call this before atomically spending
79
+ * a code, so an occupied data directory or a lost Cloud login cannot burn a
80
+ * valid code and then fail locally.
81
+ */
82
+ export declare function assertPersonalVaultAdoptable(dataDir: string, payload: PersonalVaultMaterial, preferred?: BackendName): Promise<void>;
83
+ /**
84
+ * Checks everything a fresh recovery target can check before a one-time code
85
+ * is redeemed. This deliberately does not need the encrypted vault payload.
86
+ */
87
+ export declare function assertFreshPersonalVaultTarget(dataDir: string, preferred?: BackendName): Promise<VaultConfig | null>;
88
+ export declare function adoptPersonalVaultMaterial(dataDir: string, payload: PersonalVaultMaterial, preferred?: BackendName): Promise<void>;
89
+ /** Target side: claim ciphertext, decrypt locally, adopt, then acknowledge. */
90
+ export declare function completeDeviceEnrollment(dataDir: string, requestId: number, preferred?: BackendName): Promise<DeviceEnrollmentCompletion>;
91
+ export type { CloudDeviceEnrollmentRequest };