sealkeep 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/web/sessions.html CHANGED
@@ -9,10 +9,10 @@
9
9
  <body>
10
10
  <!-- The Memory pool is a dashboard view now (index.html#sessions) — one page,
11
11
  no reloads between views. This stub keeps old bookmarks and links working,
12
- preserving a #token=… fragment when one is carried. -->
12
+ preserving a #ticket=… fragment when one is carried. -->
13
13
  <script>
14
14
  const h = location.hash.replace(/^#/, "");
15
- location.replace(h.includes("token=") ? `./index.html#${h}` : "./index.html#sessions");
15
+ location.replace(new URLSearchParams(h).has("ticket") ? `./index.html#${h}` : "./index.html#sessions");
16
16
  </script>
17
17
  </body>
18
18
  </html>
package/web/setup-api.js CHANGED
@@ -20,9 +20,10 @@
20
20
  /**
21
21
  * Every path this page may ever request. Nothing is concatenated onto them.
22
22
  *
23
- * The agents and hooks routes left with the screen that used them: the
24
- * background service finds sessions by watching the agent folders itself, so
25
- * setup no longer edits another program's configuration at all.
23
+ * Agent integration is committed only by Finish. The key screen discloses that
24
+ * this updates Sealkeep-owned Codex/Claude settings, and the service endpoint
25
+ * applies capture hooks plus the local MCP registration together so no later
26
+ * command is required.
26
27
  */
27
28
  export const ROUTES = {
28
29
  state: "/v1/setup/state",
@@ -31,52 +32,115 @@ export const ROUTES = {
31
32
  storage: "/v1/setup/storage",
32
33
  storageTest: "/v1/setup/storage/test",
33
34
  enroll: "/v1/setup/enroll",
35
+ deviceAuthorization: "/v1/setup/account-authorization",
36
+ deviceAuthorizationComplete: "/v1/setup/account-authorization/complete",
37
+ deviceEnrollment: "/v1/setup/device-enrollment",
38
+ recoveryCode: "/v1/setup/recovery-code",
34
39
  service: "/v1/setup/service"
35
40
  };
36
41
 
37
- const TOKEN_KEY = "vaultline.localApiToken";
42
+ const DEVICE_ENROLLMENT_COMPLETE = /^\/v1\/setup\/device-enrollment\/[1-9]\d*\/complete$/;
43
+
44
+ const TOKEN_KEY = "sealkeep.localApiToken";
45
+ const BROWSER_HANDOFF_KEY = "sealkeep.browserHandoff.v1";
46
+ const BROWSER_HANDOFF_CLIENT_KEY = "sealkeep.browserHandoffClient.v1";
38
47
 
39
48
  export class SetupError extends Error {
40
- constructor(message, { status = 0, route = "", missing = false } = {}) {
49
+ constructor(message, { status = 0, route = "", missing = false, code = "" } = {}) {
41
50
  super(message);
42
51
  this.name = "SetupError";
43
52
  this.status = status;
44
53
  this.route = route;
54
+ this.code = code;
45
55
  /** True when this build of Sealkeep has no such endpoint yet. */
46
56
  this.missing = missing;
47
57
  }
48
58
  }
49
59
 
50
60
  let token = "";
61
+ // Older releases used persistent browser storage. A trusted local page deletes
62
+ // that legacy bearer during upgrade; current credentials remain tab-scoped.
63
+ try { localStorage.removeItem(TOKEN_KEY); } catch { /* storage can be disabled */ }
51
64
 
52
65
  /**
53
- * The token arrives in the URL fragment, which browsers never send to a server,
54
- * and is kept in sessionStorage — the same key and the same rules as the
55
- * dashboard, so opening the dashboard after setup needs no second sign-in.
56
- * The persistent half of web storage is never touched: it outlives the window,
57
- * and a token that outlives the window is a token nobody remembers to revoke.
66
+ * A secure launch URL carries a short-lived ticket in its fragment. The page
67
+ * scrubs the fragment first, then redeems the ticket in a same-origin POST.
68
+ * Only the returned bearer enters sessionStorage — the same key and rules as
69
+ * the dashboard. Neither credential is persisted in localStorage, a cookie,
70
+ * or another URL; the short-lived fragment is scrubbed before redemption.
58
71
  */
59
- export function readTokenFromLocation() {
72
+ export async function readTokenFromLocation() {
60
73
  if (typeof location === "undefined") return token;
61
74
  const hash = new URLSearchParams(String(location.hash ?? "").replace(/^#/, ""));
62
- const fromHash = hash.get("token");
63
- if (fromHash) {
64
- sessionStorage.setItem(TOKEN_KEY, fromHash);
65
- if (typeof history !== "undefined") history.replaceState(null, "", location.pathname);
75
+ const hasTicket = hash.has("ticket");
76
+ const ticket = hash.get("ticket") ?? "";
77
+ if (hasTicket && typeof history !== "undefined") {
78
+ const search = new URLSearchParams(location.search ?? "");
79
+ search.delete("__sealkeep_bootstrap");
80
+ const cleanSearch = search.toString();
81
+ history.replaceState(null, "", location.pathname + (cleanSearch ? `?${cleanSearch}` : ""));
66
82
  }
67
83
  token = sessionStorage.getItem(TOKEN_KEY) ?? "";
84
+ if (!hasTicket) return token;
85
+
86
+ try {
87
+ const response = await fetch("/v1/auth/bootstrap", {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json", accept: "application/json" },
90
+ body: JSON.stringify({ ticket })
91
+ });
92
+ const payload = await response.json().catch(() => ({}));
93
+ if (!response.ok || typeof payload.token !== "string" || payload.token.length === 0) throw new Error("bootstrap refused");
94
+ setToken(payload.token);
95
+ } catch {
96
+ setToken("");
97
+ }
68
98
  return token;
69
99
  }
70
100
 
101
+ /** Public, non-secret fallback for a browser without Fetch Metadata support. */
102
+ export async function beginBrowserHandoff() {
103
+ let requestId = sessionStorage.getItem(BROWSER_HANDOFF_KEY) ?? "";
104
+ let clientRequestId = sessionStorage.getItem(BROWSER_HANDOFF_CLIENT_KEY) ?? "";
105
+ if (!clientRequestId) {
106
+ const bytes = new Uint8Array(32);
107
+ crypto.getRandomValues(bytes);
108
+ clientRequestId = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 43);
109
+ sessionStorage.setItem(BROWSER_HANDOFF_CLIENT_KEY, clientRequestId);
110
+ }
111
+ if (!requestId) {
112
+ const response = await fetch("/v1/auth/bootstrap-request", {
113
+ method: "POST",
114
+ headers: { "content-type": "application/json", accept: "application/json" },
115
+ body: JSON.stringify({ label: "setup browser", requestId: clientRequestId })
116
+ });
117
+ const payload = await response.json().catch(() => ({}));
118
+ if (!response.ok || typeof payload.requestId !== "string") throw new SetupError("Sealkeep could not start a browser sign-in request.", { status: response.status });
119
+ requestId = payload.requestId;
120
+ sessionStorage.setItem(BROWSER_HANDOFF_KEY, requestId);
121
+ }
122
+ return requestId;
123
+ }
124
+
125
+ export async function checkBrowserHandoff(requestId) {
126
+ const response = await fetch(`/v1/auth/bootstrap-request/${encodeURIComponent(requestId)}`, { headers: { accept: "application/json" } });
127
+ const payload = await response.json().catch(() => ({}));
128
+ if (response.status === 410 || payload.status === "expired") {
129
+ sessionStorage.removeItem(BROWSER_HANDOFF_KEY);
130
+ sessionStorage.removeItem(BROWSER_HANDOFF_CLIENT_KEY);
131
+ }
132
+ if (!response.ok && response.status !== 410) throw new SetupError("Sealkeep could not check the browser sign-in request.", { status: response.status });
133
+ return payload;
134
+ }
135
+
136
+ export function clearBrowserHandoff() {
137
+ sessionStorage.removeItem(BROWSER_HANDOFF_KEY);
138
+ sessionStorage.removeItem(BROWSER_HANDOFF_CLIENT_KEY);
139
+ }
140
+
71
141
  /**
72
- * Accepts a token from anywhere, and keeps it where the fragment path keeps
73
- * one.
74
- *
75
- * Setting only the module variable meant a token pasted by a person was
76
- * forgotten the moment anything called `readTokenFromLocation` again — which
77
- * `boot` does first thing — so signing a second browser in appeared to do
78
- * nothing at all. Session storage, not local: a token that outlives the window
79
- * is a token nobody remembers to revoke.
142
+ * Keeps a redeemed bearer in this tab only. Exported for the setup transport's
143
+ * tests and sign-out path; the UI never accepts a bearer from a person.
80
144
  */
81
145
  export function setToken(value) {
82
146
  token = String(value ?? "");
@@ -86,8 +150,18 @@ export function setToken(value) {
86
150
  }
87
151
  export function hasToken() { return token.length > 0; }
88
152
 
153
+ function isLocalAuthChallenge(response) {
154
+ const challenge = response?.headers?.get?.("www-authenticate") ?? "";
155
+ return response?.status === 401 && /^Bearer(?:\s|$)/i.test(challenge);
156
+ }
157
+
89
158
  async function request(route, { method = "GET", body } = {}) {
90
- if (!Object.values(ROUTES).includes(route)) throw new SetupError(`Refusing to request ${route}: not a setup route.`, { route });
159
+ if (!Object.values(ROUTES).includes(route) && !DEVICE_ENROLLMENT_COMPLETE.test(route)) {
160
+ throw new SetupError(`Refusing to request ${route}: not a setup route.`, { route });
161
+ }
162
+ if (!token) {
163
+ throw new SetupError("This browser is not signed in yet. Reload this local page to sign in automatically.", { status: 401, route });
164
+ }
91
165
 
92
166
  let response;
93
167
  try {
@@ -100,15 +174,16 @@ async function request(route, { method = "GET", body } = {}) {
100
174
  throw new SetupError("Sealkeep on this machine stopped answering. Is it still running in the terminal you started it from?", { route });
101
175
  }
102
176
 
103
- if (response.status === 401 || response.status === 403) {
104
- throw new SetupError("This page is not signed in to Sealkeep on this machine. Start it again with sealkeep ui, which opens the page already signed in.", { status: response.status, route });
177
+ if (isLocalAuthChallenge(response) || response.status === 403) {
178
+ setToken("");
179
+ throw new SetupError("This browser's Sealkeep access expired. Reload this local page to sign in again.", { status: response.status, route });
105
180
  }
106
181
  if (response.status === 404) {
107
182
  throw new SetupError(`This build of Sealkeep has no ${route} endpoint yet.`, { status: 404, route, missing: true });
108
183
  }
109
184
 
110
185
  const payload = await response.json().catch(() => ({}));
111
- if (!response.ok) throw new SetupError(payload?.error?.message ?? `${response.status} ${response.statusText || "request failed"}`, { status: response.status, route });
186
+ if (!response.ok) throw new SetupError(payload?.error?.message ?? `${response.status} ${response.statusText || "request failed"}`, { status: response.status, route, code: payload?.error?.code ?? "" });
112
187
  return payload;
113
188
  }
114
189
 
@@ -167,8 +242,8 @@ export const saveStorage = (config) => request(ROUTES.storage, { method: "POST",
167
242
  export const testStorage = (config) => request(ROUTES.storageTest, { method: "POST", body: config });
168
243
 
169
244
  // `reclaim` is deliberately left at its server default of false: the service
170
- // starts, but does not get standing permission to move a person's files to the
171
- // trash from a single wizard click. That permission is granted knowingly, later,
245
+ // starts, but does not get standing permission to permanently reclaim a person's
246
+ // local originals from a single wizard click. That permission is granted knowingly, later,
172
247
  // from Settings. The old body sent `{ enabled: true }`, a field the strict
173
248
  // schema does not have, so the call was rejected and the service never started.
174
249
  export const startService = () => request(ROUTES.service, { method: "POST", body: { confirm: true } });
@@ -179,3 +254,51 @@ export const startService = () => request(ROUTES.service, { method: "POST", body
179
254
  * for why that distinction is load-bearing.
180
255
  */
181
256
  export const enrollDevice = (email, code) => request(ROUTES.enroll, { method: "POST", body: { email, code } });
257
+
258
+ /**
259
+ * Starts browser approval without putting the verifier, account token, machine
260
+ * credential or enrollment private key in this page. Only the machine label
261
+ * crosses loopback; OS family and detected agent enums are added locally.
262
+ */
263
+ export const startDeviceAuthorization = (label) => request(ROUTES.deviceAuthorization, {
264
+ method: "POST",
265
+ body: { label }
266
+ });
267
+
268
+ /** The opaque request handle is a body field, so even it stays out of access logs. */
269
+ export const completeDeviceAuthorization = (requestId) => request(ROUTES.deviceAuthorizationComplete, {
270
+ method: "POST",
271
+ body: { requestId: String(requestId ?? "") }
272
+ });
273
+
274
+ /**
275
+ * Starts a fresh-device request after ordinary account linking succeeds.
276
+ * The response contains public display state only. Its private key and claim
277
+ * capability remain in this machine's native secret backend.
278
+ */
279
+ export const startDeviceEnrollment = (label) => request(ROUTES.deviceEnrollment, {
280
+ method: "POST",
281
+ body: { label }
282
+ });
283
+
284
+ /**
285
+ * Poll and completion are deliberately one operation. While the source has
286
+ * not approved, the server answers 202 with `waiting_for_approval`; after
287
+ * approval the same call claims, decrypts and adopts the vault locally.
288
+ *
289
+ * Only a validated integer is interpolated. Callers cannot name a route, and
290
+ * no code, key, token or recovery material can enter the URL.
291
+ */
292
+ export function completeDeviceEnrollment(requestId) {
293
+ const id = Number(requestId);
294
+ if (!Number.isSafeInteger(id) || id < 1) {
295
+ throw new SetupError("Refusing to poll an invalid device enrollment request.", { route: ROUTES.deviceEnrollment });
296
+ }
297
+ return request(`${ROUTES.deviceEnrollment}/${id}/complete`, { method: "POST", body: { confirm: true } });
298
+ }
299
+
300
+ /** Recover a lost-device vault locally after Google account authorization. */
301
+ export const recoverWithBackupCode = (code) => request(ROUTES.recoveryCode, {
302
+ method: "POST",
303
+ body: { code: String(code ?? "") }
304
+ });
@@ -209,7 +209,7 @@ export function validateStorage(providerId, values) {
209
209
  */
210
210
  export function buildStoragePayload(providerId, values) {
211
211
  const bucket = String(values.bucket ?? "").trim();
212
- const prefix = normalisePrefix(values.prefix) || "vaultline";
212
+ const prefix = normalisePrefix(values.prefix) || "sealkeep";
213
213
  const base = { provider: providerId, bucket, prefix };
214
214
 
215
215
  if (providerId === "gcs") {
@@ -332,8 +332,8 @@ export function summariseDone(result) {
332
332
  label: "Vault",
333
333
  badge: { text: "created", kind: "verified" },
334
334
  detail: result.password
335
- ? `${result.vault?.dataDir ?? "this machine"}`
336
- : `${result.vault?.dataDir ?? "this machine"} — opened by the recovery phrase you wrote down, and by nothing else.`
335
+ ? "Sealkeep data folder on this machine"
336
+ : "Sealkeep data folder on this machine — opened by the recovery phrase you wrote down, and by nothing else."
337
337
  });
338
338
 
339
339
  // Named again even though it was chosen two screens ago, because this is the
@@ -373,14 +373,14 @@ export function summariseDone(result) {
373
373
  const managed = Boolean(account && !account.error);
374
374
  if (account) {
375
375
  rows.push(managed
376
- ? { label: "Sealkeep account", badge: { text: "linked", kind: "verified" }, detail: `This machine is linked to your account${account.plan && account.plan !== "unknown" ? ` (${account.plan})` : ""}. Managed storage is on — it receives ${copies}.` }
377
- : { label: "Sealkeep account", badge: { text: "not linked", kind: "alert" }, detail: `${account.error} This machine is not linked to your account yet, so archives are staying on this disk only. Re-enter your pairing code from the dashboard to turn on managed storage — your vault and the key that opens it are fine.` });
376
+ ? { label: "Sealkeep account", badge: { text: "linked", kind: "verified" }, detail: `This machine is linked to your account${account.plan && account.plan !== "unknown" ? ` (${account.plan})` : ""}. Managed storage is on — it receives ${copies}. Approved devices can bring this same vault identity onto a fresh machine without exposing its key to the browser or Sealkeep Cloud.` }
377
+ : { label: "Sealkeep account", badge: { text: "not linked", kind: "alert" }, detail: `${account.error} This machine is not linked to your account yet, so archives are staying on this disk only. Your vault and key are fine. Open setup again (sealkeep ui, then \u201cWhere they go\u201d) and paste a fresh pairing code to turn on managed storage.` });
378
378
  }
379
379
 
380
380
  if (result.storage) {
381
381
  rows.push(result.storage.error
382
382
  ? { label: "Storage", badge: { text: "not connected", kind: "alert" }, detail: `${result.storage.error} Your vault is fine and is keeping archives on this disk. Try the storage settings again from the dashboard.` }
383
- : { label: "Storage", badge: { text: result.storage.verified ? "verified" : "saved", kind: result.storage.verified ? "verified" : "held" }, detail: `${result.storage.provider}://${result.storage.bucket}/${result.storage.prefix} — ${copies}. The credentials went to this machine's keychain and cannot be read back.` });
383
+ : { label: "Storage", badge: { text: result.storage.verified ? "verified" : "saved", kind: result.storage.verified ? "verified" : "held" }, detail: `${({ gdrive: "Google Drive", s3: "S3-compatible", r2: "Cloudflare R2", b2: "Backblaze B2", gcs: "Google Cloud Storage" }[result.storage.provider] ?? "Your storage")} — ${copies}. The credentials went to this machine's keychain and cannot be read back.` });
384
384
  } else if (managed) {
385
385
  rows.push({ label: "Storage", badge: { text: "Sealkeep managed", kind: "verified" }, detail: `Your account's storage receives ${copies}. Nothing else to set up.` });
386
386
  } else if (!account) {
@@ -390,9 +390,68 @@ export function summariseDone(result) {
390
390
  // storage row would only repeat it.
391
391
 
392
392
  const service = result.service;
393
- rows.push(service?.running
394
- ? { label: "Background service", badge: { text: "running", kind: "verified" }, detail: service.detail ?? "It checks for new sessions on its own. Nothing to keep open." }
395
- : { label: "Background service", badge: { text: "not running", kind: "held" }, detail: `${service?.error ?? service?.detail ?? "It did not start."} You can turn it on any time from Settings › Automatic archiving on the dashboard.` });
393
+ rows.push(service?.waitingForUnlock
394
+ ? { label: "Background service", badge: { text: "waiting for unlock", kind: "held" }, detail: "Installed at login, but it cannot preserve or index sessions until this machine stores the recovery phrase. Open Settings › Unlocking on the dashboard." }
395
+ : service?.running
396
+ ? { label: "Background service", badge: { text: "running", kind: "verified" }, detail: service.detail ?? "It checks for new sessions on its own. Nothing to keep open." }
397
+ : service?.installed
398
+ ? { label: "Background service", badge: { text: "installed, not running", kind: "held" }, detail: `${service?.error ?? service?.detail ?? "The service manager did not confirm a running process."} Use Settings › Automatic archiving to repair it.` }
399
+ : { label: "Background service", badge: { text: "not running", kind: "held" }, detail: `${service?.error ?? service?.detail ?? "It did not start."} You can turn it on any time from Settings › Automatic archiving on the dashboard.` });
400
+
401
+ // Codex separates a hook definition from permission to execute that exact
402
+ // definition. A successful file write can therefore still be waiting for
403
+ // review, or can remain explicitly disabled by the user. Keep that distinct
404
+ // from the watcher: the watcher preserves session files independently.
405
+ if (Array.isArray(result.hooks) && result.hooks.length > 0) {
406
+ const label = (hook) => ({ codex: "Codex", claude: "Claude Code" }[hook.agent] ?? hook.agent ?? "Agent");
407
+ const active = result.hooks.filter((hook) => hook.installed && hook.active !== false);
408
+ const inactive = result.hooks.filter((hook) => hook.installed && hook.active === false);
409
+ const failed = result.hooks.filter((hook) => !hook.installed);
410
+ if (inactive.length > 0 || failed.length > 0) {
411
+ const details = [
412
+ ...inactive.map((hook) => hook.attention || `${label(hook)} has the lifecycle configured, but it is not active.`),
413
+ ...failed.map((hook) => `${label(hook)}: ${hook.reason || "the lifecycle could not be configured."}`),
414
+ "The running background watcher still preserves supported session files automatically."
415
+ ];
416
+ rows.push({
417
+ label: "Agent automation",
418
+ badge: { text: active.length > 0 ? "partly active" : "review needed", kind: "held" },
419
+ detail: details.join(" ")
420
+ });
421
+ } else {
422
+ rows.push({
423
+ label: "Agent automation",
424
+ badge: { text: "active", kind: "verified" },
425
+ detail: `${active.map(label).join(" and ")} lifecycle hooks are configured, enabled and trusted.`
426
+ });
427
+ }
428
+ }
429
+
430
+ const mcp = result.mcp;
431
+ if (mcp) {
432
+ const agents = Array.isArray(mcp.agents) ? mcp.agents : [];
433
+ const installed = agents.filter((agent) => agent.installed);
434
+ const failed = agents.filter((agent) => !agent.installed);
435
+ rows.push(installed.length > 0 && failed.length === 0
436
+ ? {
437
+ label: "Agent memory",
438
+ badge: { text: "connected", kind: "verified" },
439
+ detail: `${installed.map((agent) => agent.label).join(" and ")} can search this vault through the local Sealkeep MCP. No command is left for you to run.`
440
+ }
441
+ : installed.length > 0
442
+ ? {
443
+ label: "Agent memory",
444
+ badge: { text: "partly connected", kind: "held" },
445
+ detail: `${installed.map((agent) => agent.label).join(" and ")} can search this vault. ${failed.map((agent) => `${agent.label}: ${agent.detail}`).join(" ")}`
446
+ }
447
+ : {
448
+ label: "Agent memory",
449
+ badge: { text: "not connected", kind: "held" },
450
+ detail: failed.length > 0
451
+ ? failed.map((agent) => `${agent.label}: ${agent.detail}`).join(" ")
452
+ : "No supported agent was found on this machine yet. Install Codex or Claude Code and reopen setup; Sealkeep will register its local memory connection automatically."
453
+ });
454
+ }
396
455
 
397
456
  return rows;
398
457
  }
package/web/setup.html CHANGED
@@ -57,8 +57,8 @@
57
57
  </div>
58
58
  <div class="trace-row sealed">
59
59
  <b>The copy is stored. The original stays yours.</b>
60
- <p>Copies go wherever you choose next — keeping them only on this disk is a real answer. The original file moves to your trash, never further, only after the copy is checked, and only once you allow it.</p>
61
- <span class="state">reversible</span>
60
+ <p>Copies go wherever you choose next — keeping them only on this disk is a real answer. Nothing removes an original by default. If you later enable reclaiming, Sealkeep first proves the remote copy, search entry, exact bytes, and idleness; then it permanently frees that local original. Codex keeps a tiny same-session resume pointer.</p>
61
+ <span class="state">off by default</span>
62
62
  </div>
63
63
  </div>
64
64
 
@@ -158,36 +158,83 @@
158
158
  <div class="panel pad" id="account-panel" hidden>
159
159
  <h3>Your Sealkeep account</h3>
160
160
  <p class="note">
161
- Managed storage runs on an account. Create one or sign in below, then press
162
- <b>Show a pairing code</b> on that page and bring the code back here. Nothing on this page is sent
163
- anywhere until you do.
161
+ Connect this machine in your browser. Create an account or sign in,
162
+ review the machine and detected agents, then approve once. Setup will
163
+ continue here automatically.
164
164
  </p>
165
- <div class="row">
166
- <div class="field">
167
- <label for="f-enroll-email">The email on your account</label>
168
- <input id="f-enroll-email" name="enrollEmail" type="email" autocomplete="email" spellcheck="false" placeholder="you@example.com">
169
- </div>
170
- <div class="field">
171
- <label for="f-enroll-code">Pairing code</label>
172
- <input id="f-enroll-code" name="enrollCode" type="text" autocomplete="off" spellcheck="false" placeholder="paste the code">
173
- </div>
165
+ <div class="field">
166
+ <label for="f-device-label">Name this machine</label>
167
+ <input id="f-device-label" name="deviceLabel" type="text" autocomplete="off" spellcheck="false" maxlength="80" value="My new machine">
168
+ <p class="note">Only this label, the OS family, detected agent names and an encryption public key are shown for approval. No hostname or local path is sent.</p>
174
169
  </div>
175
- <p class="note error" id="e-enroll-code" hidden></p>
176
- <!-- Both links name a tab, not just the section, so nobody lands on the
177
- page and has to work out which half they were sent for. -->
178
- <div class="actions">
179
- <a class="action primary" href="https://sealkeep.spala.ai/#signup" target="_blank" rel="noreferrer noopener">Create an account</a>
180
- <a class="action" href="https://sealkeep.spala.ai/#signin" target="_blank" rel="noreferrer noopener">I already have one — sign in</a>
170
+ <div class="actions" id="account-start-actions">
171
+ <button type="button" class="action primary" id="account-authorize">Continue securely in your browser</button>
172
+ <a class="action" id="account-approval-link" target="_blank" rel="noreferrer noopener" hidden>Open approval page</a>
181
173
  </div>
182
- <p class="note">The pairing code is single use, and it is not your recovery phrase.</p>
183
- <div class="actions">
184
- <button type="button" class="action" id="enroll-go">Add this machine</button>
174
+ <p class="note" id="account-authorization-status" aria-live="polite"></p>
175
+ <p class="note error" id="account-authorization-error" hidden></p>
176
+
177
+ <details class="panel pad callout" id="pairing-fallback">
178
+ <summary>Manual fallback — use an email and pairing code</summary>
179
+ <p class="note">Use this only when the browser approval page cannot return to this machine. The pairing code is single use, and it is not your recovery phrase. Sign in first, generate it, then paste it below.</p>
180
+ <div class="actions">
181
+ <a class="action" href="https://sealkeep.spala.ai/#signup" target="_blank" rel="noreferrer noopener">Create an account</a>
182
+ <a class="action" href="https://sealkeep.spala.ai/#signin" target="_blank" rel="noreferrer noopener">Sign in</a>
183
+ <button type="button" class="action" id="show-pairing-form">I have a pairing code</button>
184
+ </div>
185
+ <div id="account-pairing-form" hidden>
186
+ <div class="row">
187
+ <div class="field">
188
+ <label for="f-enroll-email">The email on your account</label>
189
+ <input id="f-enroll-email" name="enrollEmail" type="email" autocomplete="email" spellcheck="false" placeholder="you@example.com">
190
+ </div>
191
+ <div class="field">
192
+ <label for="f-enroll-code">Pairing code</label>
193
+ <input id="f-enroll-code" name="enrollCode" type="text" autocomplete="off" spellcheck="false" placeholder="paste the code">
194
+ </div>
195
+ </div>
196
+ <p class="note error" id="e-enroll-code" hidden></p>
197
+ <div class="actions">
198
+ <button type="button" class="action primary" id="enroll-go">Pair this machine</button>
199
+ </div>
200
+ <p class="note" id="enroll-result" aria-live="polite"></p>
201
+ <p class="note">Pairing links this machine to your plan and encrypted storage. The code cannot open an archive, and neither can Sealkeep.</p>
202
+ </div>
203
+ </details>
204
+
205
+ <div class="panel pad callout" id="device-enrollment" hidden>
206
+ <h3>Your account already has preserved memories</h3>
207
+ <p>Approve this machine from Sealkeep Settings on a device that already opens your vault. Compare the code on both screens before approving.</p>
208
+ <div id="device-enrollment-comparison" hidden>
209
+ <div class="boundary">
210
+ <span class="side">shown on both devices</span>
211
+ <b id="device-enrollment-code" aria-label="Six-digit comparison code"></b>
212
+ <span class="side sealed">expires shortly</span>
213
+ </div>
214
+ <p class="note" id="device-enrollment-expiry"></p>
215
+ </div>
216
+ <p class="note" id="device-enrollment-status" aria-live="polite"></p>
217
+ <p class="note error" id="device-enrollment-error" hidden></p>
218
+ <div class="actions">
219
+ <button type="button" class="action" id="device-enrollment-retry" hidden>Start a new request</button>
220
+ </div>
221
+ <details class="panel pad explain" id="backup-code-recovery">
222
+ <summary>I lost every other device — use a backup code</summary>
223
+ <p class="note">Enter one unused code from the Recovery Kit you printed or saved. It is opened on this machine; Gmail and SealKeep Cloud cannot read it.</p>
224
+ <div class="row">
225
+ <label for="f-recovery-code">Backup recovery code</label>
226
+ <input id="f-recovery-code" type="password" autocomplete="off" spellcheck="false" placeholder="SK-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XX">
227
+ <button type="button" class="action primary" id="recovery-code-go">Recover this machine</button>
228
+ </div>
229
+ <div class="field">
230
+ <label for="f-recovery-code-file">Or choose a saved Recovery Kit (.txt)</label>
231
+ <input id="f-recovery-code-file" type="file" accept=".txt,text/plain" autocomplete="off">
232
+ </div>
233
+ <p class="note error" id="recovery-code-error" hidden></p>
234
+ <p class="note" id="recovery-code-status" aria-live="polite"></p>
235
+ </details>
236
+ <p class="note">Only an encrypted package for this machine crosses the hosted service. The recovery phrase, enrollment private key, account token, and machine key never appear on this page.</p>
185
237
  </div>
186
- <p class="note" id="enroll-result"></p>
187
- <p class="note">
188
- This links the machine to your plan and storage. It is not your recovery phrase and it does not
189
- let anyone open a sealed archive — including us.
190
- </p>
191
238
  </div>
192
239
 
193
240
  <form class="panel pad" id="storage-form" hidden>
@@ -220,7 +267,7 @@
220
267
  <div class="actions"><button type="button" class="action" data-go="2">Back</button></div>
221
268
  <div class="actions">
222
269
  <p class="note error" id="storage-error" hidden></p>
223
- <button type="button" class="action primary" data-go="4">Continue to your key</button>
270
+ <button type="button" class="action primary" id="storage-continue" data-go="4">Continue to your key</button>
224
271
  </div>
225
272
  </div>
226
273
  </section>
@@ -232,6 +279,10 @@
232
279
  <p class="lede" id="phrase-lede">The part that decides who can ever open these archives.</p>
233
280
  </header>
234
281
 
282
+ <div class="panel pad callout">
283
+ <p><b>Finish makes the product automatic.</b> Sealkeep starts at login, watches supported agent sessions, and adds its local memory connection to detected Codex and Claude Code settings. You will not need to run a command afterwards.</p>
284
+ </div>
285
+
235
286
  <!-- The free paths: a password first, because it is the kind of key people
236
287
  already know how to keep. The phrase still exists — offered after the
237
288
  vault does, as the extra rather than the toll. -->
@@ -308,6 +359,17 @@
308
359
 
309
360
  <ol class="phrase" id="phrase-words" aria-label="Your 24-word recovery phrase"></ol>
310
361
 
362
+ <div class="panel pad callout" id="recovery-codes" hidden>
363
+ <h3>Your one-time backup codes</h3>
364
+ <p>Each code can restore this same vault after you sign in with Google, even if every trusted machine is gone. SealKeep stores only ciphertext; save these away from this machine.</p>
365
+ <ol class="plain" id="recovery-code-list"></ol>
366
+ <div class="actions">
367
+ <button type="button" class="action" id="copy-recovery-codes">Copy all codes</button>
368
+ <button type="button" class="action" id="download-recovery-codes">Download Recovery Kit</button>
369
+ </div>
370
+ <p class="note" id="recovery-codes-note">Shown once. Generating a replacement set later invalidates this set.</p>
371
+ </div>
372
+
311
373
  <div class="actions phrase-actions">
312
374
  <button type="button" class="action" id="copy-phrase">Copy the phrase</button>
313
375
  <button type="button" class="action" id="print-phrase">Print this page</button>
@@ -343,6 +405,22 @@
343
405
  <div class="actions"><button type="button" class="action primary" id="finish-existing">Finish setup</button></div>
344
406
  </div>
345
407
  </div>
408
+
409
+ <!-- A returning account with preserved memories receives its existing
410
+ vault identity through device-to-device E2EE. There is no new phrase
411
+ ceremony here because creating a temporary vault would replace the
412
+ identity the other device just approved. -->
413
+ <div id="phrase-enrolled" hidden>
414
+ <div class="panel pad callout">
415
+ <h3>Your existing vault is ready on this machine</h3>
416
+ <p>An approved device encrypted the vault identity directly for this machine. Sealkeep opened that package locally and kept the key in this machine's keychain.</p>
417
+ <p>No recovery phrase, private enrollment key, account token, or machine key was shown to this page or stored by the browser.</p>
418
+ </div>
419
+ <div class="wizfoot">
420
+ <p class="note">The same vault now continues here; setup did not create a temporary replacement.</p>
421
+ <div class="actions"><button type="button" class="action primary" id="finish-enrolled">Finish setup</button></div>
422
+ </div>
423
+ </div>
346
424
  </section>
347
425
 
348
426
  <!-- 5 ─ Done ──────────────────────────────────────────────────── -->
@@ -358,7 +436,7 @@
358
436
  <h3>What happens next</h3>
359
437
  <p id="next-detail">Nothing else is needed. The next time one of your agents finishes a session, the transcript is queued here and archived. It shows up on the dashboard within a few minutes.</p>
360
438
  <h3>Two things worth doing today</h3>
361
- <p><span id="today-key">Check that your recovery phrase is somewhere you will still have it in a year — a drawer beats a laptop.</span> Then open <a href="./index.html#rules">Retention</a> and decide when Sealkeep may move originals to your trash. Until you say so, it never does.</p>
439
+ <p><span id="today-key">Check that your recovery phrase is somewhere you will still have it in a year — a drawer beats a laptop.</span> Then open <a href="./index.html#rules">Retention</a> and decide whether Sealkeep may permanently free safely preserved originals. Until you say so, it never does.</p>
362
440
  </div>
363
441
 
364
442
  <div class="wizfoot">
@@ -371,27 +449,18 @@
371
449
  </section>
372
450
 
373
451
  <!-- Shown instead of a blank page when the machine cannot answer. -->
374
- <!-- The token lives in sessionStorage, which belongs to one browser. Opening
375
- this address in a second one arrives with nothing, and telling that person
376
- to "run sealkeep ui" is both a terminal instruction and the wrong one:
377
- it would open their default browser, not the one they are looking at. So
378
- the page offers the way in that works from here. -->
452
+ <!-- The redeemed bearer lives in sessionStorage, which belongs to one tab.
453
+ A direct local visit gives each new browser its own one-use ticket. -->
379
454
  <section class="screen" id="screen-gate" hidden>
380
455
  <header>
381
- <h1>Sign this browser in</h1>
382
- <p class="lede">Sealkeep is running on this machine — this browser just has not been given its key yet. That happens automatically in whichever browser <code>sealkeep ui</code> opened.</p>
456
+ <h1>Open Sealkeep securely</h1>
457
+ <p class="lede">Sealkeep normally signs in a direct local-browser visit automatically. If this browser does not provide that signal, request approval from any Sealkeep tab already open on this machine.</p>
383
458
  </header>
384
459
  <div class="panel pad">
385
- <p class="note">Paste the token from <code id="gate-token-path">local-api-token</code> in your Sealkeep data folder (<code>~/.sealkeep</code>, or <code>~/.vaultline</code> for older vaults). It is a local file; nothing here is sent anywhere.</p>
386
460
  <form id="gate-form">
387
- <div class="field">
388
- <label for="gate-token">Local API token</label>
389
- <input id="gate-token" type="password" autocomplete="off" spellcheck="false" placeholder="paste the token">
390
- </div>
391
- <div class="actions"><button type="submit" class="action primary">Connect this browser</button></div>
461
+ <div class="actions"><button id="gate-retry" type="submit" class="action primary">Request access in this browser</button></div>
392
462
  </form>
393
463
  <p class="note error" id="gate-error" hidden></p>
394
- <p class="note">On macOS or Linux: <code>cat ~/.sealkeep/local-api-token</code> (older vaults: <code>~/.vaultline</code>)</p>
395
464
  </div>
396
465
  </section>
397
466