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.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +223 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/site/index.html
CHANGED
|
@@ -1,1905 +1,1809 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<meta charset="utf-8">
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
}
|
|
1810
|
-
const name = document.createElement("b"); name.textContent = pack.name;
|
|
1811
|
-
const price = document.createElement("span"); price.textContent = `$${pack.monthly}/mo`;
|
|
1812
|
-
chip.append(name, price);
|
|
1813
|
-
if (pack.id === current) {
|
|
1814
|
-
const tag = document.createElement("i"); tag.textContent = "your package"; chip.append(tag);
|
|
1815
|
-
}
|
|
1816
|
-
host.append(chip);
|
|
1817
|
-
}
|
|
1818
|
-
if (current === "custom") {
|
|
1819
|
-
const gb = Math.round((Number(user.quota_bytes) || 0) / 1073741824);
|
|
1820
|
-
$("a-pack-note").textContent = `You are on a custom allocation of ${gb} GB, not one of the standard packages.`;
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
|
|
1824
|
-
const rulesFrom = (user) => {
|
|
1825
|
-
const mode = user.sync_mode || "all";
|
|
1826
|
-
const radio = document.querySelector(`input[name=sync][value="${mode}"]`) || choices[0].querySelector("input");
|
|
1827
|
-
radio.checked = true;
|
|
1828
|
-
if (user.sync_keep_last) $("r-last").value = user.sync_keep_last;
|
|
1829
|
-
if (user.sync_min_age_days) $("r-age").value = user.sync_min_age_days;
|
|
1830
|
-
paintChoices();
|
|
1831
|
-
};
|
|
1832
|
-
|
|
1833
|
-
$("r-save").addEventListener("click", async () => {
|
|
1834
|
-
const token = sessionStorage.getItem(TOKEN);
|
|
1835
|
-
const mode = document.querySelector("input[name=sync]:checked").value;
|
|
1836
|
-
$("r-save").disabled = true;
|
|
1837
|
-
$("r-msg").textContent = "Saving";
|
|
1838
|
-
try {
|
|
1839
|
-
await api("/v1/cloud/sync-rules", {
|
|
1840
|
-
method: "POST", token,
|
|
1841
|
-
body: {
|
|
1842
|
-
sync_mode: mode,
|
|
1843
|
-
sync_keep_last: Number($("r-last").value) || 20,
|
|
1844
|
-
sync_min_age_days: Number($("r-age").value) || 7
|
|
1845
|
-
}
|
|
1846
|
-
});
|
|
1847
|
-
$("r-msg").textContent = "Saved to your account.";
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
$("r-msg").textContent = /404|not found/i.test(error.message)
|
|
1850
|
-
? "Rules are not switched on yet — nothing was changed."
|
|
1851
|
-
: errText(error);
|
|
1852
|
-
} finally {
|
|
1853
|
-
$("r-save").disabled = false;
|
|
1854
|
-
setTimeout(() => { $("r-msg").textContent = ""; }, 6000);
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
|
|
1858
|
-
/* ------------------------------------------------------ email confirmation */
|
|
1859
|
-
async function finishConfirm(accountId, token) {
|
|
1860
|
-
history.replaceState(null, "", location.pathname + "#account");
|
|
1861
|
-
const signedIn = sessionStorage.getItem(TOKEN);
|
|
1862
|
-
try {
|
|
1863
|
-
await api("/confirm-email", { method: "POST", body: { account_id: accountId, token } });
|
|
1864
|
-
if (signedIn && await loadAccount(signedIn)) {
|
|
1865
|
-
await bootDone(returning);
|
|
1866
|
-
$("a-verify-msg").textContent = "Email confirmed.";
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
await backToSignIn("Email confirmed. Sign in to continue.");
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
if (signedIn && await loadAccount(signedIn)) {
|
|
1872
|
-
await bootDone(returning);
|
|
1873
|
-
$("a-verify-msg").textContent = errText(error);
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
await backToSignIn(errText(error), true);
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
/* ------------------------------------------------------------------ start
|
|
1881
|
-
One entry point, and the hash is routed only once the outcome is settled.
|
|
1882
|
-
Everything that follows a return — where the reader lands, what they are
|
|
1883
|
-
told, and whether the marketing page is even in the way — is decided here
|
|
1884
|
-
rather than left to whichever of three scroll mechanisms fired last. */
|
|
1885
|
-
(async function start() {
|
|
1886
|
-
if (authCode) return void finishGoogle(authCode);
|
|
1887
|
-
if (confirmToken && confirmAccount) return void finishConfirm(confirmAccount, confirmToken);
|
|
1888
|
-
if (savedToken) {
|
|
1889
|
-
const opened = await loadAccount(savedToken);
|
|
1890
|
-
// A resumed session is not an event, so it exits without the beat.
|
|
1891
|
-
// Coming back from a provider is, so it keeps it.
|
|
1892
|
-
await bootDone(returning && opened);
|
|
1893
|
-
sessionStorage.removeItem("vaultline.returning");
|
|
1894
|
-
if (!opened) return void await backToSignIn("That session has expired. Sign in again.");
|
|
1895
|
-
if (location.hash === "#account") requestAnimationFrame(goAccount);
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
await bootDone(false);
|
|
1899
|
-
if (location.hash === "#account") requestAnimationFrame(goAccount);
|
|
1900
|
-
// A link that names a tab opens that tab, on arrival as well as on a later
|
|
1901
|
-
// hash change. Someone sent here mid-install should land where they were
|
|
1902
|
-
// sent, not on the section and a guess.
|
|
1903
|
-
requestAnimationFrame(openAuthFromHash);
|
|
1904
|
-
})();
|
|
1
|
+
<!doctype html><meta charset="utf-8"><title>Sealkeep — your agent history, sealed</title><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Seal. Keep. Share. Your agent’s memory — sealed on your machine, kept in storage you choose, shared on your terms. Encrypted archive for Claude Code and Codex sessions."><link rel="alternate" type="text/plain" href="/llms.txt" title="Sealkeep product context for AI systems"><style>
|
|
2
|
+
:root{--paper:#161311;--card:#1e1a17;--ink:#f0ede6;--soft:#9a948a;--rule:#2e2a24;--seal:#100e0b;--seal-ink:#e8c793;--live:#e3a455;--warn:#e06c4f;--warn-bg:#33201a;--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,"Helvetica Neue",sans-serif;--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,"DejaVu Sans Mono",Consolas,monospace;color-scheme:dark;--nav-h:58px;--ok:#4cbf7a}@font-face{font-family:'Archivo Black';font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format('woff2');unicode-range:U+0000-00FF,U+2000-206F}body:not(.panel){--paper:#000000;--card:#0d0d0d;--ink:#ffffff;--soft:#9c9c9c;--rule:#242424;--seal:#0a0a0a;--seal-ink:#c2e9f2;--live:#4fc9e8;--warn:#e06c4f;--warn-bg:#2a1712;background:var(--paper)}body:not(.panel) .primary{background:var(--ink);color:var(--paper);border-radius:6px;font-family:var(--mono);font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;padding:.9rem 1.35rem}body:not(.panel) .ghost{border-radius:6px;font-family:var(--mono);font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;padding:.9rem 1.35rem;border-color:color-mix(in srgb,var(--ink) 30%,var(--paper))}body:not(.panel) .logo{font-family:var(--mono);font-weight:700;letter-spacing:.16em;text-transform:uppercase;font-size:.88rem}body:not(.panel) .pills{background:none;border:0;padding:0;gap:1.3rem}body:not(.panel) .pills a{font-family:var(--mono);font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;padding:.3rem 0;border-radius:0}body:not(.panel) .pills a.on{background:none;color:var(--ink);font-weight:700;box-shadow:inset 0 -2px 0 var(--live)}.navlink{font-family:var(--mono);font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;color:var(--soft);text-decoration:none;padding:.3rem 0}.navlink:hover{color:var(--ink)}@media(max-width:56rem){.navlink{display:none}}.npmchip .ver{font-family:var(--mono);font-size:.66rem;letter-spacing:.1em;color:var(--ok);white-space:nowrap;font-weight:700;text-transform:uppercase}body:not(.panel) .hero{text-align:left;padding:5.6rem 0 4.6rem}.promise-eyebrow{display:flex;align-items:center;gap:1rem;font-family:var(--mono);font-size:.7rem;letter-spacing:.26em;color:var(--ink);text-transform:uppercase;margin:0}.promise-eyebrow i{width:2.4rem;height:2px;background:var(--ink)}.mega{font-family:'Archivo Black',var(--sans);font-weight:400;font-size:clamp(3.8rem,10.5vw,7rem);line-height:.9;letter-spacing:.01em;margin:1.7rem 0 1.9rem}.mega .dot{color:transparent;position:relative}.mega .dot::after{content:"";position:absolute;left:.07em;bottom:.06em;width:.15em;height:.15em;border-radius:50%;background:var(--live)}body:not(.panel) .logo::after{content:" .";color:var(--live);font-weight:900}body:not(.panel) .hero .lede{margin:0 0 2.2rem;max-width:36ch;font-size:1.18rem}.hero .lede u{color:var(--ink);text-decoration-color:var(--live);text-decoration-thickness:2px;text-underline-offset:5px}body:not(.panel) .hero .cta-row{justify-content:flex-start}.wrap.hero-wrap{max-width:100rem}body:not(.panel) .hero{display:grid;grid-template-columns:minmax(360px,430px) minmax(0,1fr);align-items:center;gap:clamp(2rem,5vw,6rem);min-height:calc(100vh - var(--nav-h))}.hero-copy{min-width:0}.hero-demo{min-width:0;display:grid;grid-template-rows:auto auto auto;gap:.8rem}.hero-demo-head{display:flex;align-items:end;justify-content:space-between;gap:1.5rem;min-height:2.5rem}.hero-demo-head h2{margin:0;color:var(--ink);font:700 clamp(1.25rem,2vw,1.8rem)/1.1 var(--mono);letter-spacing:-.035em}.hero-demo-head span{flex:none;color:var(--live);font:.67rem var(--mono);letter-spacing:.14em}.hero-demo-frame{position:relative;height:560px;min-height:0;overflow:hidden;border:1px solid var(--rule);background:#161311}.hero-demo-frame::after{content:"";position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 0 1px rgb(255 255 255/2%)}.hero-demo-frame iframe{display:block;width:1100px;height:560px;border:0;background:#161311;transform-origin:top left}.hero-demo-progress{display:grid;grid-template-columns:repeat(3,1fr);gap:.45rem}.hero-demo-progress button{position:relative;height:20px;padding:0;border:0;background:transparent;cursor:pointer}.hero-demo-progress button::before{content:"";position:absolute;inset:8px 0 9px;background:#242424}.hero-demo-progress button[aria-current="true"]::before{background:var(--live)}@media(max-width:72rem){body:not(.panel) .hero{grid-template-columns:minmax(320px,390px) minmax(0,1fr);gap:2.2rem}}@media(max-width:64rem){body:not(.panel) .hero{grid-template-columns:1fr;min-height:0}.hero-demo{margin-top:1rem}.hero-demo-head h2{font-size:1.25rem}}@media(max-width:47.5rem){.hero-demo-progress{grid-row:2;position:sticky;top:var(--nav-h);z-index:4;height:36px;background:var(--paper)}.hero-demo-progress button{height:36px}.hero-demo-progress button::before{inset:17px 0 18px}.hero-demo-frame{grid-row:3}}.custody{border-block:1px solid var(--rule);background:var(--paper)}.custody .cols{display:grid;grid-template-columns:repeat(3,1fr);max-width:96rem;margin:0 auto}.custody .col{padding:2.4rem 2rem 2.6rem;border-left:1px solid var(--rule);display:flex;flex-direction:column;min-height:14.5rem}.custody .col:first-child{border-left:0}.custody .idx{font-family:var(--mono);font-size:.64rem;letter-spacing:.18em;color:var(--live);text-transform:uppercase;margin-bottom:1.5rem}.custody h3{font-size:1.02rem;letter-spacing:.035em;text-transform:uppercase;font-weight:800;margin:0 0 .55rem}.custody p{color:var(--soft);font-size:.9rem;max-width:40ch;margin:0}.custody .tail{margin-top:auto;padding-top:2.4rem;font-family:var(--mono);font-size:.6rem;letter-spacing:.2em;color:color-mix(in srgb,var(--soft) 55%,var(--paper));text-transform:uppercase}@media(max-width:47rem){.custody .cols{grid-template-columns:1fr}.custody .col{border-left:0;border-top:1px solid var(--rule);min-height:0}.custody .col:first-child{border-top:0}}*{box-sizing:border-box}html{scroll-behavior:smooth}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{animation:none!important;transition:none!important}}body{margin:0;background:radial-gradient(1100px 520px at 50% -140px,color-mix(in srgb,var(--live) 8%,var(--paper)) 0%,var(--paper) 60%) no-repeat,var(--paper);color:var(--ink);font:400 1rem/1.62 var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.wrap{max-width:64rem;margin:0 auto;padding:0 1.5rem}a{color:var(--live)}:focus-visible{outline:2px solid var(--live);outline-offset:3px}code,pre,.mono{font-family:var(--mono);font-variant-ligatures:none}h1{font-size:clamp(2.6rem,7vw,4.2rem);font-weight:800;line-height:1.04;letter-spacing:-.04em;margin:0 0 1.1rem}h2{font-size:clamp(1.7rem,3.6vw,2.4rem);font-weight:750;letter-spacing:-.032em;margin:0 0 .6rem}h3{font-size:1.16rem;font-weight:700;letter-spacing:-.018em;margin:0 0 .4rem}p{margin:0 0 1rem}.sub{color:var(--soft);font-size:1.05rem;max-width:52ch;margin:0 0 2rem}.primary{background:var(--live);color:var(--paper);padding:.74rem 1.35rem;font-size:.94rem;font-weight:650;letter-spacing:-.01em;border:1px solid transparent;border-radius:12px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center}.primary:hover{filter:brightness(1.08)}.primary:disabled{opacity:.45;cursor:default}.ghost{background:none;border:1px solid var(--rule);color:var(--ink);padding:.68rem 1.15rem;font-size:.92rem;border-radius:12px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center}.ghost:hover{border-color:var(--ink)}button{font:inherit}.kicker{display:block;font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--live);text-align:center;margin:0 0 .9rem}.eyebrow,.ck{display:block;font-family:var(--mono);font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);margin:0 0 .8rem}.navbar{position:sticky;top:0;z-index:40;background:color-mix(in srgb,var(--paper) 86%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--rule)}.navbar .row{display:flex;align-items:center;gap:1rem;min-height:var(--nav-h)}.logo{font-weight:750;letter-spacing:-.03em;font-size:1.02rem;color:var(--ink);text-decoration:none}.navbar #nav-logo{display:inline-flex;align-items:center;min-height:24px}.pills{display:flex;gap:.2rem;background:var(--card);border:1px solid var(--rule);border-radius:999px;padding:.22rem;margin:0 auto}.pills a{font-size:.86rem;color:var(--soft);text-decoration:none;padding:.34rem .95rem;border-radius:999px;min-height:26px;display:inline-flex;align-items:center}.pills a:hover{color:var(--ink)}.pills a.on{background:var(--live);color:var(--paper);font-weight:650}.npmchip{display:flex;align-items:center;gap:.45rem;border:1px solid var(--rule);border-radius:999px;padding:.3rem .35rem .3rem .85rem}.npmchip code{font-size:.78rem;color:var(--soft);white-space:nowrap}.npmchip button{background:none;border:0;color:var(--live);font-family:var(--mono);font-size:.72rem;cursor:pointer;padding:.2rem .5rem;border-radius:999px}.npmchip button:hover{background:color-mix(in srgb,var(--live) 14%,transparent)}@media(max-width:47rem){.npmchip{display:none}.pills{margin:0 0 0 auto}}.hero{padding:4.6rem 0 2.6rem;text-align:center}.hero .lede{font-size:1.12rem;color:var(--soft);max-width:44ch;margin:0 auto 1.9rem}.cta-row{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap}.hero .meta{font-family:var(--mono);font-size:.76rem;color:var(--soft);margin:1.5rem 0 0;letter-spacing:.02em}.term{background:var(--seal);border:1px solid color-mix(in srgb,var(--seal-ink) 14%,var(--seal));border-radius:14px;margin:2.6rem auto 0;max-width:54rem;text-align:left;overflow:hidden}.term header{display:flex;align-items:center;gap:.8rem;padding:.7rem 1rem;border-bottom:1px solid color-mix(in srgb,var(--seal-ink) 10%,var(--seal))}.term .dots{font-size:.5rem;letter-spacing:.28em;color:color-mix(in srgb,var(--seal-ink) 30%,var(--seal))}.term .title{font-family:var(--mono);font-size:.74rem;color:var(--soft);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.term .run{font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;color:var(--ok)}.term pre{margin:0;background:none;border:0;padding:1.1rem 1.2rem 1.3rem;font-size:.82rem;line-height:1.85;color:var(--seal-ink);overflow-x:auto}.term pre b{font-weight:400;color:var(--live)}.term pre i{font-style:normal;color:var(--ok)}.term pre .dim{color:color-mix(in srgb,var(--seal-ink) 55%,var(--seal))}.belt{display:grid;grid-template-columns:repeat(4,1fr);border-block:1px solid var(--rule);margin:3.2rem 0 0}.belt>div{padding:1.5rem 1.3rem;border-left:1px solid var(--rule)}.belt>div:first-child{border-left:0}.belt .b-v{font-size:1.28rem;font-weight:750;letter-spacing:-.02em;line-height:1.25;margin-top:.4rem}.belt .amber{color:var(--live)}@media(max-width:47rem){.belt{grid-template-columns:1fr 1fr}.belt>div:nth-child(odd){border-left:0}.belt>div{border-top:1px solid var(--rule)}.belt>div:nth-child(-n+2){border-top:0}}section{padding:4.2rem 0}.center{text-align:center}.center .sub{margin-inline:auto}.how-grid{display:grid;gap:1rem;grid-template-columns:1fr 1fr;margin-top:2.2rem}.step-card{background:var(--card);border:1px solid var(--rule);border-radius:14px;padding:1.5rem;text-align:left}.step-card .num{display:inline-flex;align-items:center;justify-content:center;width:1.9rem;height:1.9rem;border-radius:999px;background:color-mix(in srgb,var(--ink) 10%,var(--card));font-family:var(--mono);font-size:.8rem;margin-right:.6rem;vertical-align:.2rem}.step-card h3{display:inline}.step-card p{color:var(--soft);font-size:.95rem;margin:.8rem 0 0}.step-card .foot{font-family:var(--mono);font-size:.74rem;color:var(--soft);border-top:1px solid var(--rule);margin-top:1rem;padding-top:.8rem}.step-card.hot{background:color-mix(in srgb,var(--live) 9%,var(--card));border-color:color-mix(in srgb,var(--live) 35%,var(--rule))}.step-card.hot .foot{color:var(--live);border-top-color:color-mix(in srgb,var(--live) 30%,var(--rule))}.how-note{color:var(--soft);text-align:center;max-width:56ch;margin:1.8rem auto 0;font-size:.97rem}@media(max-width:47rem){.how-grid{grid-template-columns:1fr}}.bento{display:grid;gap:1rem;grid-template-columns:1fr 1fr;margin-top:2.2rem}.b-card{background:var(--card);border:1px solid var(--rule);border-radius:14px;padding:1.6rem;text-align:left}.b-card.span2{grid-column:1/-1}.b-card p{color:var(--soft);font-size:.96rem}.b-card .foot{font-family:var(--mono);font-size:.74rem;color:var(--soft);line-height:1.8;margin:.9rem 0 0}.b-card .green{color:var(--ok);font-size:.9rem;margin:.8rem 0 0}.b-card.glow{background:color-mix(in srgb,var(--live) 8%,var(--card));border-color:color-mix(in srgb,var(--live) 30%,var(--rule))}.b-term{background:var(--seal);border-radius:10px;padding:.9rem 1rem;font-family:var(--mono);font-size:.78rem;line-height:1.85;color:var(--seal-ink);overflow-x:auto;margin:.9rem 0 0;white-space:pre-wrap;word-break:break-word}.b-term b{font-weight:400;color:var(--live)}.b-term i{font-style:normal;color:var(--ok)}.b-term .dim{color:color-mix(in srgb,var(--seal-ink) 55%,var(--seal))}.b-term .row{display:flex;gap:.9rem;justify-content:space-between;flex-wrap:wrap}.cipherbox{font-family:var(--mono);font-size:.76rem;line-height:1.8;background:var(--seal);color:var(--seal-ink);word-break:break-all;padding:.9rem 1rem;border-radius:10px;margin:.9rem 0 0}.b-split{display:grid;gap:1.4rem;grid-template-columns:1.1fr 1fr;align-items:center}@media(max-width:47rem){.bento{grid-template-columns:1fr}.b-split{grid-template-columns:1fr}}.zk{background:var(--seal);border-block:1px solid color-mix(in srgb,var(--seal-ink) 12%,var(--seal));margin:2rem calc(50% - 50vw) 0;padding:4.2rem 0}.zk .cols{display:grid;gap:2.6rem;grid-template-columns:1fr 1.1fr;align-items:start}.zk h2{margin-top:.2rem}.zk p{color:var(--soft)}.zk blockquote{margin:1.6rem 0 0;padding:.2rem 0 .2rem 1.1rem;border-left:3px solid var(--live);font-size:.98rem;color:var(--soft)}.zk blockquote strong{color:var(--ink)}.holds{display:grid;grid-template-columns:1fr 1fr;border:1px solid color-mix(in srgb,var(--seal-ink) 14%,var(--seal));border-radius:14px;overflow:hidden}.holds .h{font-family:var(--mono);font-size:.68rem;letter-spacing:.12em;text-transform:uppercase;color:var(--live);padding:1rem 1.2rem .4rem}.holds .h+.h{border-left:1px solid color-mix(in srgb,var(--seal-ink) 12%,var(--seal))}.holds div{padding:.85rem 1.2rem;font-size:.92rem;color:var(--ink);border-top:1px solid color-mix(in srgb,var(--seal-ink) 10%,var(--seal))}.holds div:nth-child(even){border-left:1px solid color-mix(in srgb,var(--seal-ink) 12%,var(--seal))}@media(max-width:47rem){.zk .cols{grid-template-columns:1fr}}.kit{background:var(--card);border:1px solid var(--rule);border-radius:16px;padding:1.6rem;margin-top:2.2rem;text-align:left}.kit .head{display:flex;gap:1rem;justify-content:space-between;flex-wrap:wrap;font-family:var(--mono);font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;color:var(--soft);margin-bottom:1.2rem}.kit .head b{color:var(--live);font-weight:500;text-transform:none;letter-spacing:.02em}.words{display:grid;gap:.55rem;grid-template-columns:repeat(6,1fr)}.words span{display:flex;gap:.5rem;align-items:baseline;border:1px solid var(--rule);border-radius:10px;padding:.5rem .7rem;font-family:var(--mono);font-size:.84rem}.words i{font-style:normal;font-size:.66rem;color:var(--soft)}.kit .verify{color:var(--soft);font-size:.94rem;margin:1.3rem 0 0}.kit .verify em{color:var(--soft)}@media(max-width:56rem){.words{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(max-width:36rem){.kit{padding:1rem}.words{grid-template-columns:repeat(2,minmax(0,1fr))}.words span{min-width:0;padding:.5rem;gap:.35rem;overflow:hidden}}.pricing{display:grid;gap:1rem;grid-template-columns:1fr 1fr;margin-top:2.2rem;align-items:stretch}.price-card{border-radius:16px;padding:1.8rem;text-align:left;border:1px solid var(--rule);background:var(--card)}.price-card .p-name{font-size:1.2rem;font-weight:750;letter-spacing:-.02em}.price-card .p-price{font-size:2.3rem;font-weight:800;letter-spacing:-.04em;margin:.7rem 0 0}.price-card .p-price small{font-size:.9rem;font-weight:400;letter-spacing:0;color:var(--soft)}.price-card .pick{margin:1.1rem 0 0;font-size:.95rem}.price-card ul{list-style:none;margin:1.1rem 0 0;padding:0;font-size:.93rem}.price-card li{padding:.5rem 0 .5rem 1.5rem;position:relative;border-top:1px solid var(--rule)}.price-card li::before{content:"\2713";position:absolute;left:0;font-family:var(--mono);color:var(--live)}.price-card li.dash::before{content:"\2014";color:var(--soft)}.price-card.slim .p-price{font-size:1.7rem}.acct-why{display:grid;gap:1rem;grid-template-columns:1fr 1fr;margin:2rem 0 1.4rem;text-align:left}.why-card{background:var(--card);border:1px solid var(--rule);border-radius:14px;padding:1.2rem 1.3rem;display:flex;flex-direction:column}.why-card h3{margin:.2rem 0 .35rem}.why-card p{font-size:.92rem;color:var(--soft);margin:0}.why-card .ck{color:var(--live)}.why-card .tailln{margin-top:.9rem;font-family:var(--mono);font-size:.66rem;letter-spacing:.12em;text-transform:uppercase;color:color-mix(in srgb,var(--soft) 60%,var(--paper))}.acct-loop{font-size:.95rem;color:var(--soft);max-width:56ch;margin:0 auto 2.4rem;text-align:center}@media(max-width:47rem){.acct-why{grid-template-columns:1fr}}.price-card.light{background:var(--ink);color:var(--paper);border-color:var(--ink)}.price-card.light .pick,.price-card.light li{color:color-mix(in srgb,var(--paper) 78%,var(--ink))}.price-card.light li{border-top-color:color-mix(in srgb,var(--paper) 14%,var(--ink))}.price-card.light .p-price small{color:color-mix(in srgb,var(--paper) 60%,var(--ink))}.price-card.light li::before{color:color-mix(in srgb,var(--live) 55%,var(--paper))}.price-card.light .ghost{border-color:color-mix(in srgb,var(--paper) 30%,var(--ink));color:var(--paper)}.managed-chip{font-family:var(--mono);font-size:.64rem;letter-spacing:.12em;background:var(--live);color:var(--paper);border-radius:999px;padding:.24rem .6rem;vertical-align:.24rem;margin-left:.5rem}.packs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1.2rem 0 0}.packs span{font-family:var(--mono);font-size:.8rem;border:1px solid var(--rule);border-radius:10px;padding:.5rem .8rem}.packs span.now{border-color:var(--live)}.price-note{color:var(--soft);text-align:center;max-width:60ch;margin:1.8rem auto 0;font-size:.95rem}.price-card .cta{margin-top:1.4rem}@media(max-width:47rem){.pricing{grid-template-columns:1fr}}.enterprise{margin:2rem calc(50% - 50vw) 0;padding:5.2rem 0 0;background:#f1f2ee;color:#090909;border-block:1px solid #d2d4cf;scroll-margin-top:var(--nav-h)}.enterprise .enterprise-wrap{max-width:80rem}.enterprise-head{display:grid;grid-template-columns:minmax(18rem,.82fr) minmax(32rem,1.18fr);gap:clamp(3rem,7vw,7rem);align-items:center}.enterprise-copy .kicker{color:#087d99;text-align:left;margin-bottom:1.2rem}.enterprise h2{font-family:'Archivo Black',var(--sans);font-size:clamp(2.75rem,5.7vw,5.1rem);line-height:.94;letter-spacing:-.025em;margin:0 0 1.5rem;max-width:9ch}.enterprise-copy>p{color:#4f524e;font-size:1.06rem;max-width:42ch;margin-bottom:1.8rem}.enterprise .cta-row{justify-content:flex-start}body:not(.panel) .enterprise .primary{background:#090909;color:#fff;border-color:#090909}body:not(.panel) .enterprise .ghost{color:#090909;border-color:#9c9f99}body:not(.panel) .enterprise .ghost:hover{border-color:#090909}.enterprise-boundary{background:#fff;border:1px solid #cfd1cc;box-shadow:0 22px 70px rgb(0 0 0/8%)}.enterprise-boundary-head{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:1rem 1.25rem;border-bottom:1px solid #d9dbd6;font-family:var(--mono);font-size:.67rem;letter-spacing:.13em;text-transform:uppercase;color:#5f625e}.enterprise-boundary-head strong{color:#087d99;font-weight:700}.enterprise-stack{padding:0 1.25rem}.enterprise-layer{display:grid;grid-template-columns:2.25rem minmax(0,1fr) auto;gap:1rem;align-items:center;padding:1.05rem 0;border-top:1px solid #e2e3df}.enterprise-layer:first-child{border-top:0}.enterprise-layer .num{font:700 .64rem var(--mono);color:#087d99}.enterprise-layer b{display:block;font-size:.92rem;line-height:1.25}.enterprise-layer small{display:block;color:#696c68;font-size:.77rem;margin-top:.2rem;line-height:1.35}.enterprise-layer .owner{font:700 .62rem var(--mono);letter-spacing:.1em;text-transform:uppercase;color:#393b38;white-space:nowrap}.enterprise-boundary-foot{display:grid;grid-template-columns:1fr auto;gap:1rem;align-items:center;padding:1rem 1.25rem;background:#090909;color:#fff;font:700 .68rem var(--mono);letter-spacing:.12em;text-transform:uppercase}.enterprise-boundary-foot strong{color:#52cee9;font-weight:700}.enterprise-proof{display:grid;grid-template-columns:repeat(4,1fr);margin-top:4.5rem;border-top:1px solid #cfd1cc}.enterprise-proof>div{padding:1.55rem 1.5rem 1.8rem;border-left:1px solid #cfd1cc}.enterprise-proof>div:first-child{border-left:0}.enterprise-proof span{display:block;color:#087d99;font:700 .63rem var(--mono);letter-spacing:.13em;text-transform:uppercase;margin-bottom:.75rem}.enterprise-proof strong{display:block;font-size:.96rem;line-height:1.35}.enterprise-proof p{color:#5d605c;font-size:.83rem;line-height:1.5;margin:.5rem 0 0}.enterprise-modes{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid #cfd1cc}.enterprise-mode{padding:2.5rem 1.5rem 3rem}.enterprise-mode+.enterprise-mode{border-left:1px solid #cfd1cc}.enterprise-mode .mode-name{display:flex;align-items:center;gap:.65rem;margin-bottom:.65rem}.enterprise-mode .mode-name i{width:.55rem;height:.55rem;border-radius:50%;background:#9a9d97;flex:none}.enterprise-mode.isolated .mode-name i{background:#087d99;box-shadow:0 0 0 4px rgb(8 125 153/12%)}.enterprise-mode h3{font-size:1.08rem;margin:0}.enterprise-mode p{color:#5d605c;font-size:.91rem;max-width:53ch;margin:0}.enterprise-docs{display:flex;align-items:center;gap:2rem;justify-content:space-between;background:#090909;color:#fff;padding:1.5rem max(1.5rem,calc((100vw - 80rem)/2));margin-top:0}.enterprise-docs p{margin:0;color:#bec1bc;font-size:.9rem}.enterprise-docs strong{color:#fff}.enterprise-docs a{display:inline-flex;align-items:center;min-height:2.75rem;color:#52cee9;font:700 .7rem var(--mono);letter-spacing:.12em;text-transform:uppercase;text-decoration:none;white-space:nowrap}.enterprise-docs a:hover{text-decoration:underline;text-underline-offset:4px}@media(max-width:64rem){.enterprise-head{grid-template-columns:1fr;gap:2.5rem}.enterprise h2{max-width:12ch}.enterprise-proof{grid-template-columns:1fr 1fr}.enterprise-proof>div:nth-child(3){border-left:0}.enterprise-proof>div:nth-child(n+3){border-top:1px solid #cfd1cc}}@media(max-width:47rem){.enterprise{padding-top:4rem}.enterprise h2{font-size:clamp(2.65rem,14vw,4rem)}.enterprise-layer{grid-template-columns:1.8rem minmax(0,1fr)}.enterprise-layer .owner{grid-column:2;white-space:normal}.enterprise-proof,.enterprise-modes{grid-template-columns:1fr}.enterprise-proof>div{border-left:0;border-top:1px solid #cfd1cc}.enterprise-proof>div:first-child{border-top:0}.enterprise-mode+.enterprise-mode{border-left:0;border-top:1px solid #cfd1cc}.enterprise-docs{display:block}.enterprise-docs a{display:inline-block;margin-top:1rem;white-space:normal}}body.enterprise-page{background:#f1f2ee}body.enterprise-page .enterprise{display:block;margin:0;padding:0;background:#f1f2ee}body.enterprise-page .enterprise-wrap{max-width:90rem}.enterprise-page-hero{position:relative;display:grid;grid-template-columns:1fr;gap:2rem;min-height:calc(100vh - var(--nav-h));padding:3.2rem 0 4.5rem;color:#f7f1e6;background:radial-gradient(circle at 48% 55%,rgb(224 165 44/11%),transparent 24%),linear-gradient(rgb(119 151 101/6%) 1px,transparent 1px),linear-gradient(90deg,rgb(119 151 101/6%) 1px,transparent 1px),#0a100c;background-size:auto,34px 34px,34px 34px,auto;box-shadow:0 0 0 100vmax #0a100c;clip-path:inset(0 -100vmax)}.enterprise-copy{display:grid;grid-template-columns:minmax(0,1fr) minmax(19rem,24rem);grid-template-rows:auto auto;column-gap:clamp(3rem,6vw,6rem);align-items:start}.enterprise-copy h1{grid-column:1;grid-row:1;font-family:'Archivo Black',var(--sans);font-size:clamp(3.1rem,3.7vw,3.65rem);font-weight:400;line-height:.96;letter-spacing:-.045em;margin:0 0 1.25rem;max-width:none;color:#f8f2e8}.enterprise-copy>p{grid-column:1;grid-row:2;color:#b0b8af;font-size:1.04rem;line-height:1.6;max-width:58rem;margin:0}.enterprise-copy .cta-row{grid-column:2;grid-row:1/3;display:grid;gap:1rem;align-content:center;margin:0}body:not(.panel) .enterprise-copy .primary{background:#e0a52c;color:#10150f;border-color:#e0a52c;box-shadow:0 12px 34px rgb(224 165 44/16%)}body:not(.panel) .enterprise-copy .primary:hover{background:#efb744;border-color:#efb744}body:not(.panel) .enterprise-copy .ghost{color:#c9d889;border-color:#7d8d47;background:rgb(18 29 19/62%)}body:not(.panel) .enterprise-copy .ghost:hover{color:#fff;border-color:#b6cb75}.enterprise-story{min-width:0;overflow:hidden;border:1px solid #405039;border-radius:13px;background:#0a100c;box-shadow:0 30px 90px rgb(0 0 0/46%),0 0 0 1px rgb(224 165 44/6%)}.enterprise-story-frame{position:relative;height:620px;overflow:hidden}.enterprise-story-frame iframe{position:relative;z-index:2;display:block;width:1360px;height:620px;border:0;background:#0a100c;opacity:0;transform-origin:top left;transition:opacity .18s ease}.enterprise-story-frame.is-ready iframe{opacity:1}.enterprise-story-preview{position:absolute;z-index:1;inset:0;display:grid;grid-template-rows:auto auto 1fr;gap:1.15rem;padding:1.35rem;background:radial-gradient(circle at 50% 55%,rgb(224 165 44/12%),transparent 30%),#0a100c;color:#f4eee3;transition:opacity .16s ease,visibility .16s ease}.enterprise-story-frame.is-ready .enterprise-story-preview{visibility:hidden;opacity:0}.enterprise-preview-tabs{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #35432f;border-radius:999px;overflow:hidden;color:#9da99d;font:700 .58rem var(--mono);letter-spacing:.055em;text-transform:uppercase}.enterprise-preview-tabs span{padding:.55rem .7rem;border-left:1px solid #35432f;text-align:center}.enterprise-preview-tabs span:first-child{border-left:0}.enterprise-preview-tabs span:first-child{background:#183321;color:#e0a52c}.enterprise-preview-title{display:flex;align-items:end;justify-content:space-between;gap:1rem;padding:0 .2rem}.enterprise-preview-title strong{max-width:21ch;font-size:clamp(1.3rem,2.3vw,2rem);line-height:1.02;letter-spacing:-.035em}.enterprise-preview-title small{color:#a9c779;font:700 .6rem var(--mono);letter-spacing:.08em;text-transform:uppercase}.enterprise-preview-flow{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;min-height:0}.enterprise-preview-flow article{display:flex;flex-direction:column;gap:.75rem;padding:1.2rem;border:1px solid #35432f;background:#101a12}.enterprise-preview-flow article:nth-child(2){border-color:#916d28;background:linear-gradient(145deg,rgb(224 165 44/11%),transparent),#101a12}.enterprise-preview-flow b{font-size:.85rem}.enterprise-preview-flow i{height:1px;background:#35432f}.enterprise-preview-flow span{color:#aab3aa;font:.62rem/1.6 var(--mono)}body.enterprise-page .enterprise-page-hero~*{content-visibility:auto;contain-intrinsic-size:auto 620px}.enterprise-section-head{max-width:58rem;margin:7.5rem auto 2.5rem;text-align:center}.enterprise-section-head.compact{margin-top:6.5rem}.enterprise-section-head .kicker{text-align:center;margin-bottom:1rem}.enterprise-section-head h2{font-size:clamp(2.35rem,5vw,4.4rem);line-height:.98;letter-spacing:-.045em;margin:0 auto 1.3rem;max-width:13ch}.enterprise-section-head p{color:#555954;font-size:1.02rem;line-height:1.7;max-width:68ch;margin:0 auto}.enterprise-needs{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid #cfd1cc;border-bottom:1px solid #cfd1cc}.enterprise-needs article{min-height:15.5rem;padding:2.5rem;border-left:1px solid #cfd1cc}.enterprise-needs article:nth-child(odd){border-left:0}.enterprise-needs article:nth-child(n+3){border-top:1px solid #cfd1cc}.enterprise-needs span,.enterprise-responsibility-grid>div>span,.enterprise-review-grid span{display:block;color:#087d99;font:700 .63rem var(--mono);letter-spacing:.13em;text-transform:uppercase;margin-bottom:1.1rem}.enterprise-needs h3{font-size:1.45rem;line-height:1.15;letter-spacing:-.025em;max-width:22ch;margin:0 0 .9rem}.enterprise-needs p{color:#5d605c;font-size:.92rem;line-height:1.65;max-width:48ch;margin:0}.enterprise-value-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));border:1px solid #cfd1cc;background:#fff}.enterprise-value{min-height:19rem;padding:2.65rem;border-left:1px solid #cfd1cc;border-top:1px solid #cfd1cc}.enterprise-value:nth-child(odd){border-left:0}.enterprise-value:nth-child(-n+2){border-top:0}.enterprise-value .value-num{display:block;color:#087d99;font:700 .63rem var(--mono);letter-spacing:.13em;text-transform:uppercase;margin-bottom:2.4rem}.enterprise-value h3{font-size:clamp(1.55rem,2.5vw,2.15rem);line-height:1.04;letter-spacing:-.035em;max-width:18ch;margin:0 0 1rem}.enterprise-value p{color:#5d605c;font-size:.94rem;line-height:1.68;max-width:52ch;margin:0}.enterprise-value .value-proof{display:block;margin-top:1.4rem;padding-top:1rem;border-top:1px solid #e0e2dd;color:#343734;font:600 .68rem/1.55 var(--mono);letter-spacing:.035em;text-transform:uppercase}.enterprise-operations{display:grid;grid-template-columns:minmax(16rem,.72fr) minmax(28rem,1.28fr);gap:3.5rem;margin-top:2rem;padding:3rem;background:#0f1211;color:#fff}.enterprise-operations .kicker{color:#52cee9;text-align:left;margin-bottom:1rem}.enterprise-operations h3{font-family:'Archivo Black',var(--sans);font-size:clamp(2rem,3.3vw,3.25rem);font-weight:400;line-height:.98;letter-spacing:-.04em;max-width:10ch;margin:0 0 1.2rem}.enterprise-operations-copy p{color:#aeb5b0;font-size:.91rem;line-height:1.65;max-width:43ch;margin:0}.enterprise-interfaces{border-top:1px solid #303733}.enterprise-interface{display:grid;grid-template-columns:minmax(11rem,.8fr) 1.2fr;gap:1.5rem;padding:1.3rem 0;border-bottom:1px solid #303733}.enterprise-interface code{color:#78d9ed;font:600 .75rem/1.45 var(--mono);word-break:break-word}.enterprise-interface p{color:#c3c9c5;font-size:.82rem;line-height:1.55;margin:0}.enterprise-interface b{color:#fff;font-weight:700}.enterprise-interface-note{color:#838c86;font-size:.74rem;line-height:1.55;margin:1.25rem 0 0}.enterprise-mode ul,.enterprise-responsibility-grid ul{list-style:none;margin:1.4rem 0 0;padding:0;border-top:1px solid #d9dbd6}.enterprise-mode li,.enterprise-responsibility-grid li{position:relative;padding:.75rem 0 .75rem 1.25rem;border-bottom:1px solid #d9dbd6;color:#555954;font-size:.84rem;line-height:1.45}.enterprise-mode li::before,.enterprise-responsibility-grid li::before{content:"✓";position:absolute;left:0;color:#087d99;font:700 .74rem var(--mono)}.enterprise-responsibility{margin-top:1rem}.enterprise-responsibility-grid{display:grid;grid-template-columns:1fr 1fr;background:#fff;border:1px solid #cfd1cc}.enterprise-responsibility-grid>div{padding:2.5rem}.enterprise-responsibility-grid>div+div{border-left:1px solid #cfd1cc}.enterprise-review{padding-bottom:7rem}.enterprise-review-grid{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid #cfd1cc;border-bottom:1px solid #cfd1cc}.enterprise-review-grid>div{min-height:12.5rem;padding:2rem;border-left:1px solid #cfd1cc}.enterprise-review-grid>div:nth-child(3n+1){border-left:0}.enterprise-review-grid>div:nth-child(n+4){border-top:1px solid #cfd1cc}.enterprise-review-grid b{display:block;font-size:1rem;line-height:1.3;margin-bottom:.65rem}.enterprise-review-grid small{display:block;color:#5d605c;font-size:.79rem;line-height:1.55}.enterprise-review-grid .artifact-link{display:inline-block;margin-top:1.1rem;color:#087d99;font:700 .66rem var(--mono);letter-spacing:.08em;text-transform:uppercase;text-decoration:none}.enterprise-review-grid .artifact-link:hover{text-decoration:underline;text-underline-offset:4px}.enterprise-assurance{display:grid;grid-template-columns:minmax(12rem,.35fr) 1fr;gap:2rem;margin-top:2.5rem;padding:1.5rem 0;border-top:1px solid #cfd1cc;border-bottom:1px solid #cfd1cc}.enterprise-assurance strong{color:#8a4e41;font:700 .72rem var(--mono);letter-spacing:.08em;text-transform:uppercase}.enterprise-assurance p{color:#5d605c;font-size:.84rem;line-height:1.65;margin:0}.enterprise-docs>div{display:flex;gap:1.5rem;align-items:center;flex:none}.enterprise-docs>div a+a{color:#fff}body.enterprise-page footer{background:#090909;color:#8f938d;border-top:1px solid #222}body.enterprise-page footer .logo{color:#fff}@media(max-width:72rem){.enterprise-page-hero{min-height:0}.enterprise-copy{grid-template-columns:1fr}.enterprise-copy h1,.enterprise-copy>p,.enterprise-copy .cta-row{grid-column:1}.enterprise-copy h1{grid-row:1;max-width:14ch}.enterprise-copy>p{grid-row:2}.enterprise-copy .cta-row{grid-row:3;grid-template-columns:auto auto;justify-content:start;margin-top:1.6rem}.enterprise-story{margin-top:.5rem}.enterprise-operations{grid-template-columns:1fr;gap:2rem}.enterprise-review-grid{grid-template-columns:1fr 1fr}.enterprise-review-grid>div:nth-child(3n+1){border-left:1px solid #cfd1cc}.enterprise-review-grid>div:nth-child(odd){border-left:0}.enterprise-review-grid>div:nth-child(n+3){border-top:1px solid #cfd1cc}}@media(max-width:47.5rem){.navbar .row{gap:.55rem}body:not(.panel) #nav-logo{display:inline-flex;align-items:center;width:1.75rem;min-height:1.75rem;overflow:hidden;white-space:nowrap;font-size:0}body:not(.panel) #nav-logo::before{content:"⌑";font:700 1rem var(--mono)}body:not(.panel) #nav-logo::after{content:""}body:not(.panel) .pills{gap:.65rem}body:not(.panel) .pills a{font-size:.58rem;letter-spacing:.08em}.enterprise-page-hero{padding:2.8rem 0 3.5rem;gap:1.4rem}.enterprise-copy h1{font-size:clamp(2.75rem,13vw,4rem)}.enterprise-copy>p{font-size:.97rem;line-height:1.55}.enterprise-copy .cta-row{grid-template-columns:1fr;width:100%}.enterprise-copy .cta-row a{text-align:center}.enterprise-story{margin-inline:-.45rem;border-radius:10px}.enterprise-story-frame{height:2100px}.enterprise-story-frame iframe{width:100%;height:2100px;transform:none}.enterprise-story-preview{align-content:start;padding:1rem}.enterprise-preview-tabs{grid-template-columns:1fr 1fr;border-radius:12px}.enterprise-preview-tabs span:nth-child(3){border-left:0}.enterprise-preview-tabs span:nth-child(n+3){border-top:1px solid #35432f}.enterprise-preview-title{display:grid;padding-top:1rem}.enterprise-preview-flow{grid-template-columns:1fr}.enterprise-preview-flow article{min-height:13rem}.enterprise-section-head,.enterprise-section-head.compact{margin-top:5rem;text-align:left}.enterprise-section-head .kicker{text-align:left}.enterprise-section-head h2{margin-left:0}.enterprise-needs,.enterprise-value-grid,.enterprise-responsibility-grid,.enterprise-review-grid{grid-template-columns:1fr}.enterprise-needs article,.enterprise-needs article:nth-child(n),.enterprise-value:nth-child(n),.enterprise-responsibility-grid>div+div,.enterprise-review-grid>div:nth-child(n){border-left:0}.enterprise-needs article+article,.enterprise-value+.enterprise-value,.enterprise-responsibility-grid>div+div,.enterprise-review-grid>div+div{border-top:1px solid #cfd1cc}.enterprise-needs article{min-height:0;padding:2rem 0}.enterprise-responsibility-grid>div{padding:2rem 1.25rem}.enterprise-review-grid>div{min-height:0;padding:1.6rem 0}.enterprise-value{min-height:0;padding:2rem 1.25rem}.enterprise-value .value-num{margin-bottom:1.5rem}.enterprise-operations{padding:2rem 1.25rem;margin-inline:-1.25rem}.enterprise-interface{grid-template-columns:1fr;gap:.55rem}.enterprise-assurance{grid-template-columns:1fr;gap:.8rem}.enterprise-docs>div{display:block}.enterprise-docs>div a{display:block;margin-top:1rem}}body.enterprise-page{--ent-bg:#08100b;--ent-band:#0d1710;--ent-surface:#111d14;--ent-rule:#35432f;--ent-rule-soft:#223025;--ent-ink:#f4eee3;--ent-soft:#aab3aa;--ent-sage:#a9c779;--ent-amber:#e0a52c;background:var(--ent-bg);color:var(--ent-ink)}body.enterprise-page .navbar{background:rgb(8 16 11/91%);border-color:var(--ent-rule-soft)}body.enterprise-page .navbar .logo{color:var(--ent-ink)}body.enterprise-page .pills a{color:#aab3aa}body.enterprise-page .pills a:hover,body.enterprise-page .pills a.on{color:var(--ent-ink)}body.enterprise-page .pills a.on{box-shadow:inset 0 -2px 0 var(--ent-amber)}body.enterprise-page .npmchip{border-color:var(--ent-rule);color:var(--ent-soft)}body.enterprise-page .npmchip code{color:var(--ent-soft)}body.enterprise-page .enterprise{background:var(--ent-bg);color:var(--ent-ink);overflow:hidden}body.enterprise-page .enterprise .kicker{color:var(--ent-sage);text-align:left}body.enterprise-page .enterprise-page-hero{border-bottom:1px solid var(--ent-rule-soft)}.enterprise-loss-story{display:grid;grid-template-columns:minmax(0,.9fr) minmax(31rem,1.1fr);gap:clamp(3rem,6vw,7rem);padding:7.5rem 0 6rem;border-bottom:1px solid var(--ent-rule);position:relative}.enterprise-loss-copy h2{font-family:'Archivo Black',var(--sans);font-size:clamp(2.45rem,4.5vw,4.75rem);font-weight:400;line-height:.96;letter-spacing:-.045em;max-width:10.5ch;margin:0 0 2rem;color:var(--ent-ink)}.enterprise-loss-copy>p{color:var(--ent-sage);font:700 .78rem/1.5 var(--mono);letter-spacing:.04em;margin:0 0 1.25rem}.enterprise-loss-copy ol{list-style:none;margin:0;padding:0;border-top:1px solid var(--ent-rule-soft)}.enterprise-loss-copy li{display:grid;grid-template-columns:2.4rem 1fr;gap:.8rem;align-items:center;padding:.9rem 0;border-bottom:1px solid var(--ent-rule-soft);color:#dce1d9;font-size:.91rem}.enterprise-loss-copy li span{color:var(--ent-sage);font:700 .68rem var(--mono)}.enterprise-loss-ledger{display:grid;grid-template-columns:minmax(11rem,.62fr) 2rem minmax(16rem,1fr);align-items:center;gap:1rem}.enterprise-loss-ledger>div:not(.loss-arrow){min-height:24rem;border:1px solid var(--ent-rule);background:linear-gradient(145deg,rgb(169 199 121/5%),transparent 62%),var(--ent-band);padding:2rem;display:flex;flex-direction:column}.enterprise-loss-ledger span,.enterprise-ownership span{color:var(--ent-sage);font:700 .68rem var(--mono);letter-spacing:.08em;text-transform:uppercase}.enterprise-loss-ledger strong{font-size:1.45rem;letter-spacing:-.025em;margin:.65rem 0 1.6rem;color:var(--ent-ink)}.enterprise-loss-ledger code{display:block;border:1px solid var(--ent-rule-soft);background:#09100b;padding:1rem;color:#c7d5bd;font:.73rem/1.8 var(--mono)}.enterprise-loss-ledger small{margin-top:auto;color:#7f897f;font:600 .65rem var(--mono);text-transform:uppercase;letter-spacing:.06em}.enterprise-loss-ledger .loss-sealkeep{border-color:#a87920!important;box-shadow:0 0 44px rgb(224 165 44/8%)}.loss-sealkeep span,.enterprise-aha,.sealkeep-owner strong,.sealkeep-owner span{color:var(--ent-amber)!important}.loss-sealkeep ul{list-style:none;margin:0;padding:0}.loss-sealkeep li{position:relative;padding:.72rem 0 .72rem 1.45rem;border-top:1px solid var(--ent-rule-soft);color:#d7ded4;font-size:.83rem}.loss-sealkeep li::before{content:'✓';position:absolute;left:0;color:var(--ent-amber);font:700 .72rem var(--mono)}.loss-arrow{color:var(--ent-amber);font-size:2rem;text-align:center}.enterprise-aha{grid-column:1/-1;text-align:center;font-size:clamp(1.25rem,2.3vw,2rem);font-weight:750;letter-spacing:-.025em;margin:0}.enterprise-ownership{display:grid;grid-template-columns:repeat(4,1fr);margin:0 0 1px;border-bottom:1px solid var(--ent-rule);background:var(--ent-band)}.enterprise-ownership>div{min-height:14rem;padding:2rem;border-left:1px solid var(--ent-rule-soft)}.enterprise-ownership>div:first-child{border-left:0}.enterprise-ownership strong{display:block;font:700 1.03rem var(--mono);margin-bottom:1rem}.enterprise-ownership p{color:var(--ent-soft);font-size:.84rem;line-height:1.55;margin:2rem 0 0}.enterprise-ownership .sealkeep-owner{background:linear-gradient(150deg,rgb(224 165 44/12%),transparent 75%);box-shadow:inset 0 0 0 1px #8f6a22}body.enterprise-page .enterprise-proof{margin:0;background:#0a120d;border:0;border-bottom:1px solid var(--ent-rule)}body.enterprise-page .enterprise-proof>div{padding:2rem 1.6rem;border-color:var(--ent-rule-soft)}body.enterprise-page .enterprise-proof span{color:var(--ent-amber)}body.enterprise-page .enterprise-proof strong{color:var(--ent-ink)}body.enterprise-page .enterprise-proof p{color:var(--ent-soft)}body.enterprise-page .enterprise-section-head{margin:8rem 0 3rem;text-align:left;max-width:64rem}body.enterprise-page .enterprise-section-head .kicker{text-align:left}body.enterprise-page .enterprise-section-head h2{color:var(--ent-ink);font-size:clamp(2.55rem,5vw,5rem);max-width:12ch;margin:0 0 1.35rem}body.enterprise-page .enterprise-section-head p{color:var(--ent-soft);margin:0;max-width:62ch}body.enterprise-page .enterprise-value-grid{display:block;border:1px solid var(--ent-rule);background:var(--ent-band)}body.enterprise-page .enterprise-value{display:grid;grid-template-columns:9rem minmax(13rem,.75fr) minmax(18rem,1fr);column-gap:clamp(2rem,4vw,4.5rem);min-height:0;padding:2.6rem;border:0;border-top:1px solid var(--ent-rule);align-items:start}body.enterprise-page .enterprise-value:first-child{border-top:0}body.enterprise-page .enterprise-value:nth-child(even){background:rgb(169 199 121/4%)}body.enterprise-page .enterprise-value.featured{padding-block:3.4rem;background:linear-gradient(90deg,rgb(224 165 44/5%),transparent 45%)}body.enterprise-page .enterprise-value .value-num{color:var(--ent-sage);margin:0}body.enterprise-page .enterprise-value h3{color:var(--ent-ink);font-size:clamp(1.65rem,2.6vw,2.65rem);max-width:17ch;margin:0}body.enterprise-page .enterprise-value p{color:var(--ent-soft);margin:0;max-width:60ch}body.enterprise-page .enterprise-value p b{color:#ede5d8}body.enterprise-page .enterprise-value .value-proof{grid-column:3;margin-top:1.25rem;padding-top:1rem;border-color:var(--ent-rule-soft);color:#c1cfb7}body.enterprise-page .value-flow{grid-column:2/4;display:grid;grid-template-columns:1fr auto 1fr;gap:1rem;align-items:center;max-width:34rem;margin-top:1.6rem;padding-top:1.3rem;border-top:1px solid var(--ent-rule-soft)}body.enterprise-page .value-flow i{display:grid;place-items:center;min-height:4.2rem;padding:.7rem;border:1px dashed #64764d;color:#c7d5bd;background:#0a120d;font:700 .65rem/1.4 var(--mono);font-style:normal;letter-spacing:.06em;text-align:center;text-transform:uppercase}body.enterprise-page .value-flow em{color:var(--ent-amber);font:400 1.4rem var(--sans);font-style:normal}body.enterprise-page .enterprise-operations{margin:1px 0 0;padding:5rem clamp(2rem,5vw,5rem);background:radial-gradient(circle at 10% 20%,rgb(224 165 44/7%),transparent 30%),#090f0b;border:1px solid var(--ent-rule);color:var(--ent-ink)}body.enterprise-page .enterprise-operations .kicker{color:var(--ent-amber)}body.enterprise-page .enterprise-operations h3{font-size:clamp(2.2rem,3.6vw,3.75rem);max-width:13ch}body.enterprise-page .enterprise-operations-copy p{color:var(--ent-soft)}body.enterprise-page .enterprise-interfaces{border-color:var(--ent-rule)}body.enterprise-page .enterprise-interface{border-color:var(--ent-rule)}body.enterprise-page .enterprise-interface code{color:var(--ent-sage);font-size:.8rem}body.enterprise-page .enterprise-interface p{color:#c3cbc1}body.enterprise-page .enterprise-interface-note{color:#7f897f}body.enterprise-page .enterprise-needs{border-color:var(--ent-rule);background:var(--ent-band)}body.enterprise-page .enterprise-needs article{border-color:var(--ent-rule);background:transparent}body.enterprise-page .enterprise-needs span,body.enterprise-page .enterprise-responsibility-grid>div>span,body.enterprise-page .enterprise-review-grid span{color:var(--ent-amber)}body.enterprise-page .enterprise-needs h3{color:var(--ent-ink)}body.enterprise-page .enterprise-needs p{color:var(--ent-soft)}body.enterprise-page .enterprise-modes{border:1px solid var(--ent-rule);background:var(--ent-band)}body.enterprise-page .enterprise-mode{padding:3rem;border-color:var(--ent-rule)}body.enterprise-page .enterprise-mode.isolated{background:linear-gradient(145deg,rgb(224 165 44/7%),transparent 65%)}body.enterprise-page .enterprise-mode h3{color:var(--ent-ink);font-size:1.3rem}body.enterprise-page .enterprise-mode p{color:var(--ent-soft)}body.enterprise-page .enterprise-mode ul,body.enterprise-page .enterprise-responsibility-grid ul{border-color:var(--ent-rule)}body.enterprise-page .enterprise-mode li,body.enterprise-page .enterprise-responsibility-grid li{border-color:var(--ent-rule-soft);color:var(--ent-soft)}body.enterprise-page .enterprise-mode li::before,body.enterprise-page .enterprise-responsibility-grid li::before{color:var(--ent-sage)}body.enterprise-page .enterprise-mode.isolated .mode-name i{background:var(--ent-amber);box-shadow:0 0 0 4px rgb(224 165 44/12%)}body.enterprise-page .enterprise-responsibility-grid{background:var(--ent-band);border-color:var(--ent-rule)}body.enterprise-page .enterprise-responsibility-grid>div+div{border-color:var(--ent-rule)}body.enterprise-page .enterprise-review{padding-bottom:7rem}body.enterprise-page .enterprise-review-grid{border-color:var(--ent-rule);background:var(--ent-band)}body.enterprise-page .enterprise-review-grid>div{border-color:var(--ent-rule);background:transparent}body.enterprise-page .enterprise-review-grid b{color:var(--ent-ink)}body.enterprise-page .enterprise-review-grid small{color:var(--ent-soft)}body.enterprise-page .enterprise-assurance{border-color:#705722;background:rgb(224 165 44/6%);padding:1.5rem}body.enterprise-page .enterprise-assurance strong{color:var(--ent-amber)}body.enterprise-page .enterprise-assurance p{color:#c4cbc1}body.enterprise-page .enterprise-docs{background:#101b12;border-top:1px solid var(--ent-rule);padding-block:3.2rem;color:var(--ent-ink)}body.enterprise-page .enterprise-docs p{font-size:clamp(1.4rem,2.6vw,2.15rem);line-height:1.12;color:var(--ent-ink);max-width:28ch}body.enterprise-page .enterprise-docs p span{display:block;color:var(--ent-soft);font:400 .85rem/1.55 var(--sans);margin-top:.8rem;max-width:62ch}body.enterprise-page .enterprise-docs a{border:1px solid #788a45;color:var(--ent-sage);padding:.75rem 1rem;text-decoration:none}body.enterprise-page .enterprise-docs a.primary{background:var(--ent-amber);border-color:var(--ent-amber);color:#10150f}body.enterprise-page footer{background:#060b08;color:#778077;border-color:var(--ent-rule-soft)}@media(max-width:72rem){.enterprise-loss-story{grid-template-columns:1fr;gap:3rem}.enterprise-loss-copy h2{max-width:13ch}.enterprise-loss-copy ol{max-width:42rem}.enterprise-ownership{grid-template-columns:1fr 1fr}.enterprise-ownership>div:nth-child(odd){border-left:0}.enterprise-ownership>div:nth-child(n+3){border-top:1px solid var(--ent-rule-soft)}body.enterprise-page .enterprise-value{grid-template-columns:10rem minmax(13rem,.8fr) minmax(15rem,1fr);gap:2rem}}@media(max-width:47.5rem){.enterprise-loss-story{padding:5rem 0 4rem}.enterprise-loss-copy h2{font-size:clamp(2.3rem,11.5vw,3.4rem)}.enterprise-loss-ledger{grid-template-columns:1fr}.enterprise-loss-ledger>div:not(.loss-arrow){min-height:0}.loss-arrow{transform:rotate(90deg)}.enterprise-aha{text-align:left}.enterprise-ownership{grid-template-columns:1fr}.enterprise-ownership>div:nth-child(n){min-height:0;border-left:0;border-top:1px solid var(--ent-rule-soft)}.enterprise-ownership>div:first-child{border-top:0}body.enterprise-page .enterprise-proof{grid-template-columns:1fr}.enterprise-proof>div:nth-child(n){border-left:0;border-top:1px solid var(--ent-rule-soft)}.enterprise-proof>div:first-child{border-top:0}body.enterprise-page .enterprise-section-head{margin-top:5.5rem}body.enterprise-page .enterprise-value{grid-template-columns:1fr;gap:1rem;padding:2rem 1.3rem}.enterprise-value h3{margin-bottom:.4rem!important}.enterprise-value .value-proof,.enterprise-value .value-flow{grid-column:1!important}.enterprise-value .value-flow{width:100%}body.enterprise-page .enterprise-operations{padding:2.5rem 1.25rem;margin-inline:-.45rem}body.enterprise-page .enterprise-needs article{padding:2rem 1.25rem}body.enterprise-page .enterprise-mode{padding:2rem 1.25rem}body.enterprise-page .enterprise-docs{padding-inline:1.25rem}.enterprise-docs>div{width:100%}.enterprise-docs a{text-align:center}}.ship{border:1px solid var(--rule);border-radius:16px;margin-top:2.2rem;overflow:hidden;text-align:left}.ship>div{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:2rem;align-items:center;min-height:88px;padding:1.25rem 1.5rem;border-top:1px solid var(--rule)}.ship>div:first-child{border-top:0}.ship .name{min-width:0;font-weight:700;letter-spacing:-.01em}.ship .d{display:block;max-width:82ch;color:var(--soft);font-size:.9rem;line-height:1.55;margin-top:.25rem;font-weight:400}.ship .tag{justify-self:end;font-family:var(--mono);font-size:.66rem;letter-spacing:.1em;border:1px solid currentColor;border-radius:999px;padding:.32rem .75rem;white-space:nowrap}.ship .tag.ok{color:var(--ok)}.ship .tag.warn{color:var(--live)}.ship-note{color:var(--soft);text-align:center;max-width:62ch;margin:1.8rem auto 0;font-size:.95rem}.ship-note strong{color:var(--ink)}@media(max-width:42rem){.ship>div{grid-template-columns:1fr;row-gap:.9rem;min-height:0;padding:1.25rem 1.15rem}.ship .tag{justify-self:start}}.faq{background:var(--card);border:1px solid var(--rule);border-radius:14px;margin-top:1rem;text-align:left}.faq summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:.45rem;padding:1.15rem 1.4rem;font-size:1.04rem;font-weight:700;letter-spacing:-.014em}.faq summary::-webkit-details-marker{display:none}.faq summary::after{content:"+";font-family:var(--mono);color:var(--soft);font-weight:400;margin-left:auto}.faq[open] summary::after{content:"\2212"}.faq div{padding:0 1.4rem 1.2rem;color:var(--soft);font-size:.97rem;max-width:72ch}.cta-final{text-align:center;padding:5rem 0 5.4rem}.cta-final h2{font-size:clamp(2.1rem,5.4vw,3.2rem)}.cta-final .sub{margin-inline:auto}.cta-final .meta{font-family:var(--mono);font-size:.76rem;color:var(--soft);margin-top:1.4rem}#enterprise{display:none}body.enterprise-page #enterprise{display:block}#docs{display:none}body.docs #docs{display:block}body.docs main#home{display:none}body.docs #account{display:none}body.enterprise-page main#home,body.enterprise-page #docs,body.enterprise-page #account{display:none}body.account-page main#home,body.account-page #enterprise,body.account-page #docs{display:none}body.account-page #account{border-top:0;min-height:calc(100vh - var(--nav-h));padding-top:3.4rem}body.account-page .acct-why,body.account-page .acct-loop{display:none}.cmdtable{border:1px solid var(--rule);border-radius:16px;overflow:hidden;margin-top:2rem}.cmdtable>div{display:grid;gap:.3rem 1.6rem;grid-template-columns:minmax(15rem,2fr) 3fr;padding:1rem 1.4rem;border-top:1px solid var(--rule);align-items:baseline}.cmdtable>div:first-child{border-top:0}.cmdtable code{color:var(--live);font-size:.86rem}.cmdtable p{margin:0;color:var(--soft);font-size:.94rem}@media(max-width:47rem){.cmdtable>div{grid-template-columns:1fr}}.arch{display:grid;gap:1rem;grid-template-columns:1fr 1fr;margin-top:2.4rem}.arch .b-card p strong{color:var(--ink)}.arch .caution{color:var(--warn);font-size:.92rem;margin:.9rem 0 0}@media(max-width:47rem){.arch{grid-template-columns:1fr}}@media(min-width:58rem){#p-account .cardgrid{display:grid;gap:1rem;grid-template-columns:1fr 1fr;align-items:start}#p-account .cardgrid>.card{margin-bottom:0}#p-account .cardgrid>.wide{grid-column:1/-1}}.tabs{display:flex;margin-bottom:1.6rem;border-bottom:1px solid var(--rule)}.tabs button{background:none;color:var(--soft);border:0;border-radius:0;border-bottom:2px solid transparent;padding:.6rem 1rem;font-size:.88rem}.tabs button[aria-selected=true]{color:var(--ink);font-weight:650;border-bottom-color:var(--live)}.pane{display:none}.pane.on{display:block}ol.steps{counter-reset:s;list-style:none;padding:0;margin:0}ol.steps li{counter-increment:s;position:relative;padding:0 0 1.6rem 2.7rem}ol.steps li::before{content:counter(s,decimal-leading-zero);position:absolute;left:0;top:.16rem;font-family:var(--mono);font-size:.76rem;color:var(--live);font-weight:600}ol.steps p{margin:.2rem 0 .55rem;color:var(--soft);font-size:.95rem}pre{background:var(--card);border:1px solid var(--rule);border-radius:10px;padding:.85rem .9rem;overflow-x:auto;font-size:.82rem;line-height:1.7;margin:.5rem 0 0}.phrase{background:var(--seal);color:var(--ink);border-radius:12px;padding:1.2rem 1.3rem;margin:2rem 0 0}.phrase strong{font-weight:700;font-size:1.04rem;letter-spacing:-.018em}.phrase p{margin:.4rem 0 0;font-size:.94rem;color:var(--seal-ink)}label{display:block;font-family:var(--mono);font-size:.72rem;color:var(--soft);margin:1rem 0 .35rem}input{width:100%;padding:.66rem .75rem;border:1px solid var(--rule);border-radius:10px;background:var(--card);color:var(--ink);font:inherit;font-size:.95rem}input:focus-visible{border-color:var(--live)}.row2{display:grid;gap:1rem;grid-template-columns:1fr}@media(min-width:34rem){.row2{grid-template-columns:1fr 1fr}}form button{margin-top:1.3rem}.msg{font-family:var(--mono);font-size:.78rem;margin:.8rem 0 0;min-height:1.2em;color:var(--soft)}.msg.err{color:var(--warn)}.saved{font-family:var(--mono);font-size:.76rem;color:var(--live)}.google{display:flex;align-items:center;justify-content:center;gap:.6rem;width:100%;background:var(--card);border:1px solid var(--rule);color:var(--ink);padding:.7rem 1rem;font-size:.92rem;font-weight:650}.google:hover{border-color:var(--ink)}.google svg{width:17px;height:17px;flex:none}.or{display:flex;align-items:center;gap:.85rem;margin:1.3rem 0;color:var(--soft);font-family:var(--mono);font-size:.72rem}.or::before,.or::after{content:"";height:1px;background:var(--rule);flex:1}body.panel main#home,body.panel #enterprise,body.panel #docs,body.panel .acct-intro,body.panel .guest{display:none}body.panel #account{border-top:0;padding-top:1.6rem;scroll-margin-top:calc(var(--nav-h) + 1px)}.ph{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .9rem;background:var(--card);border:1px solid var(--rule);border-radius:12px;padding:1.2rem 1.3rem;margin-bottom:1.3rem}.ph .name{font-size:1.15rem;font-weight:650;letter-spacing:-.02em}.ph .who{font-family:var(--mono);font-size:.75rem;color:var(--soft);flex:1 1 100%}.ph:focus,.ph:focus-visible{outline:none}.chip{font-family:var(--mono);font-size:.7rem;min-height:26px;display:inline-flex;align-items:center;border:1px solid var(--rule);border-radius:999px;color:var(--soft);padding:.2rem .55rem}.chip.pro{border-color:var(--live);color:var(--live);font-weight:600}.card{background:var(--card);border:1px solid var(--rule);border-radius:12px;padding:1.35rem;margin-bottom:.9rem}.card>p{color:var(--soft);font-size:.94rem;margin:0 0 .3rem;max-width:60ch}.empty{color:var(--soft);font-size:.9rem;margin:.5rem 0 0;max-width:60ch}.notice{background:var(--warn-bg);border:1px solid var(--warn);border-radius:12px;padding:1rem 1.2rem;margin-bottom:.9rem;display:flex;flex-wrap:wrap;gap:.75rem;align-items:center}.notice.soft{background:var(--card);border-color:var(--rule)}.notice p{margin:0;flex:1 1 17rem;font-size:.94rem}.notice strong{font-weight:700}.warncard{border-color:var(--warn)}.warncard .eyebrow{color:var(--warn)}.card-remedy{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-top:1rem;padding-top:1rem;border-top:1px solid var(--rule)}.card-remedy[hidden]{display:none}.card-remedy [hidden]{display:none!important}.card-remedy .msg{margin:0;flex:1 1 13rem}.card-remedy .primary,.card-remedy .ghost{justify-content:center}@media(max-width:32rem){.card-remedy .primary,.card-remedy .ghost{width:100%}}.kv{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr))}.kv>div{border:1px solid var(--rule);border-radius:12px;padding:.9rem 1rem}.kv .cap{display:block;font-size:.78rem;color:var(--soft);margin-top:.25rem}.kv .k{display:block;font-family:var(--mono);font-size:.72rem;color:var(--soft);margin-bottom:.2rem}.kv b{font-family:var(--mono);font-size:1rem;font-weight:500;color:var(--soft);letter-spacing:-.02em}.kv .lead b{font-size:clamp(1.55rem,2.1vw,2.2rem);font-weight:700;color:var(--ink);letter-spacing:-.04em;line-height:1.1;white-space:nowrap}.bar{height:8px;background:var(--rule);border-radius:999px;margin:1.05rem 0 .4rem;overflow:hidden}.bar i{display:block;height:100%;width:0;min-width:6px;background:var(--live);transition:width .5s ease}table{width:100%;border-collapse:collapse;font-size:.9rem;min-width:31rem}th{text-align:left;font-family:var(--mono);font-size:.72rem;color:var(--soft);font-weight:400;padding:.65rem;border-bottom:1px solid var(--rule)}td{padding:.68rem .65rem;border-bottom:1px solid var(--rule)}tr:last-child td{border-bottom:0}td code{font-size:.86em;color:var(--soft)}.state{font-family:var(--mono);font-size:.72rem;border:1px solid currentColor;border-radius:999px;padding:.18rem .55rem;white-space:nowrap}.choice .t,.choice .d{font-family:var(--sans)}.cellsub{display:block;font-family:var(--sans);font-size:.75rem;color:var(--soft);margin-top:.15rem}.cellsub.connection-disconnected{color:var(--warn);font-weight:650}.cellsub.connection-partial,.cellsub.connection-unknown{color:var(--live)}.cellsub.connection-connected{color:var(--ok)}.target-reconnect{display:inline;padding:0;border:0;background:none;color:inherit;font:inherit;font-weight:700;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.target-place-editor{display:grid;grid-template-columns:repeat(2,minmax(8rem,1fr));gap:.45rem;min-width:22rem}.target-place-editor label{display:grid;gap:.2rem;margin:0;color:var(--soft);font:600 .62rem var(--mono);letter-spacing:.05em;text-transform:uppercase}.target-place-editor label:last-child:nth-child(odd){grid-column:1/-1}.target-place-editor input{width:100%;min-width:0}@media(max-width:47rem){.target-place-editor{grid-template-columns:1fr;min-width:14rem}.target-place-editor label{grid-column:1!important}}.acct-nav{display:flex;gap:.35rem;flex-wrap:wrap;position:sticky;top:58px;z-index:5;background:var(--paper);padding:.65rem 0;margin:.4rem 0 .2rem;border-bottom:1px solid var(--rule)}.acct-nav a{font-family:var(--mono);font-size:.74rem;color:var(--soft);text-decoration:none;border:1px solid var(--rule);border-radius:999px;padding:.4rem .85rem;min-height:26px;display:inline-flex;align-items:center}.acct-nav a:hover{color:var(--ink);border-color:var(--ink)}.acct-nav a[aria-current="page"]{color:var(--ink);border-color:var(--live);background:color-mix(in srgb,var(--live) 12%,var(--card))}[data-account-section][hidden]{display:none!important}#d-retired details{margin:.5rem 0 .2rem}#d-retired summary{font-family:var(--mono);font-size:.76rem;color:var(--soft);cursor:pointer;padding:.35rem 0}#d-retired summary:hover{color:var(--ink)}#d-retired .lines li{opacity:.75}.acct-hero{font-family:var(--mono);font-size:1.55rem;font-weight:700;letter-spacing:-.03em;margin:.9rem 0 0;line-height:1.3}h1.acct-hero:empty{display:none}.ruleslist{margin:.3rem 0 .9rem;padding-left:1.1rem}.ruleslist li{margin:.25rem 0;color:var(--soft)}.ruleslist b{color:var(--ink);font-weight:600}#tg-body input,#tg-body select{padding:.35rem .5rem;font-size:.85rem;border:1px solid var(--rule);border-radius:12px;background:transparent;color:var(--ink)}#tg-body input[type=number]{width:4.2rem}#tg-body input:focus,#tg-body select:focus{border-color:var(--live);outline:none}#tg-body tr:hover td{background:color-mix(in srgb,var(--rule) 22%,transparent)}#tg-save-msg{display:block;margin-top:.6rem}.packrow{display:flex;gap:.6rem;flex-wrap:wrap;margin:.4rem 0 .9rem}.packchip{display:flex;flex-direction:column;gap:.1rem;border:1px solid var(--rule);border-radius:10px;padding:.6rem .95rem;min-width:6.4rem}.packchip b{font-family:var(--mono);font-size:.95rem}.packchip span{font-size:.78rem;color:var(--soft)}.packchip.now{border-color:var(--live)}.packchip.now i{font-style:normal;font-family:var(--mono);font-size:.66rem;color:var(--live);letter-spacing:.03em}#p-account .card h2{font-size:1.02rem;margin:.15rem 0 .5rem}[id^="anchor-"]{scroll-margin-top:3.4rem}.state.durable{color:var(--live)}.state.pending{color:var(--soft);opacity:.75}.del{background:none;color:var(--soft);border:1px solid var(--rule);padding:.3rem .7rem;font-family:var(--mono);font-size:.74rem}.del:hover{border-color:var(--warn);color:var(--warn)}.del[data-armed=yes]{background:var(--warn);color:var(--card);border-color:var(--warn)}.lines{list-style:none;margin:.55rem 0 0;padding:0;font-size:.92rem}.lines li{display:flex;flex-wrap:wrap;gap:.45rem .85rem;align-items:center;padding:.68rem 0;border-top:1px solid var(--rule)}.lines li:first-child{border-top:0}.lines .grow{flex:1 1 11rem}.lines .when{font-family:var(--mono);font-size:.75rem;color:var(--soft)}.inline{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center;margin-top:.4rem}.inline input{flex:1 1 13rem;width:auto}select{width:100%;padding:.66rem .7rem;border:1px solid var(--rule);border-radius:10px;background:var(--card);color:var(--ink);font:inherit;font-size:.95rem}.why{border-left:2px solid var(--live);padding:.15rem 0 .15rem .8rem;margin:.6rem 0 0;color:var(--soft);font-size:.86rem}.rules{border:1px solid var(--rule);border-radius:12px;padding:1.2rem;margin-top:.8rem}.choice{display:flex;gap:.75rem;align-items:flex-start;padding:.72rem 0;border-top:1px solid var(--rule);cursor:pointer}.choice:first-of-type{border-top:0;padding-top:0}.choice input{width:auto;margin:.22rem 0 0;accent-color:var(--live);flex:none}.choice .t{font-size:.95rem;font-weight:650}.choice .d{color:var(--soft);font-size:.88rem;margin-top:.15rem;display:block}.choice .n{width:4.4rem;padding:.3rem .45rem;font-family:var(--mono);font-size:.85rem;margin:.4rem .5rem 0 0;display:inline-block}.choice[data-on=no] .d,.choice[data-on=no] .n{opacity:.45}.card.next{border-color:var(--live);padding:1.6rem 1.5rem}.card.next .eyebrow{color:var(--live)}h3.lead{font-size:1.24rem;font-weight:750;letter-spacing:-.028em;margin:0 0 .5rem}.pair{margin-top:1.25rem}.pair-field{display:flex;gap:.5rem;flex-wrap:wrap;align-items:stretch;max-width:34rem}.pair-field pre{flex:1 1 17rem;margin:0}.pair-code{flex:1 1 15rem;display:flex;align-items:center;background:var(--paper);border:1px solid var(--live);border-radius:10px;padding:.68rem .9rem;color:var(--ink);font-family:var(--mono);font-size:clamp(1.3rem,4.4vw,1.8rem);font-weight:600;letter-spacing:.12em;line-height:1.24;word-break:break-all}.pair-code[data-long=yes]{font-size:clamp(.94rem,2.8vw,1.1rem);letter-spacing:.05em;font-weight:500}.pair-field .primary,.pair-field .ghost{flex:0 0 auto}.pair .fine{margin-top:.75rem;max-width:66ch}.alt{margin-top:1.15rem;border-top:1px solid var(--rule);padding-top:.9rem}.alt summary{font-family:var(--mono);font-size:.76rem;color:var(--soft);cursor:pointer;list-style:none}.alt summary::-webkit-details-marker{display:none}.alt summary::before{content:"+ ";color:var(--live)}.alt[open] summary::before{content:"− "}.alt summary:hover{color:var(--ink)}.alt p{margin:.7rem 0 .55rem}.projects-card{width:min(86rem,calc(100vw - 3rem));margin-left:50%;transform:translateX(-50%);padding:0;overflow:hidden}.projects-top{display:flex;align-items:flex-start;justify-content:space-between;gap:1.5rem;padding:1.4rem 1.5rem;border-bottom:1px solid var(--rule)}.projects-top h2{font:700 1.25rem/1.2 var(--mono)!important;letter-spacing:-.03em;margin:.2rem 0 .35rem!important}.projects-top p{margin:0;color:var(--soft);font-size:.9rem;max-width:62ch}.projects-privacy{display:flex;align-items:center;gap:.5rem;color:var(--soft);font:500 .69rem var(--mono);white-space:nowrap;padding-top:.15rem}.projects-privacy::before{content:"";width:.48rem;height:.48rem;border-radius:50%;background:var(--ok);box-shadow:0 0 0 3px color-mix(in srgb,var(--ok) 12%,transparent)}.invite-inbox{padding:0 1.5rem;background:color-mix(in srgb,var(--live) 4%,var(--card));border-bottom:1px solid var(--rule)}.invite-inbox:empty{display:none}.invite-card{display:flex;align-items:center;gap:1rem;padding:1rem 0}.invite-card+.invite-card{border-top:1px solid var(--rule)}.member-avatar{display:grid;place-items:center;width:2.2rem;height:2.2rem;flex:none;border:1px solid color-mix(in srgb,var(--live) 42%,var(--rule));border-radius:8px;background:color-mix(in srgb,var(--live) 9%,var(--card));color:var(--live);font:700 .75rem var(--mono);text-transform:uppercase}.invite-copy,.member-copy{min-width:0;flex:1}.invite-copy strong,.member-copy strong{display:block;font-size:.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.invite-copy span,.member-copy span{display:block;color:var(--soft);font-size:.77rem;margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.invite-actions{display:flex;gap:.45rem;flex:none}.invite-actions button{padding:.5rem .75rem;font-size:.76rem}.projects-layout{display:grid;grid-template-columns:15rem minmax(0,1fr);min-height:31rem}.projects-layout.drawer-open{grid-template-columns:14rem minmax(22rem,1fr) 20rem}.project-rail{border-right:1px solid var(--rule);padding:1rem .75rem;background:color-mix(in srgb,var(--seal) 35%,var(--card))}.project-rail-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;padding:0 .35rem .8rem}.project-rail-head span{color:var(--soft);font:600 .66rem var(--mono);letter-spacing:.12em;text-transform:uppercase}.project-add{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;min-height:2rem;padding:.42rem .68rem;border:1px solid var(--live);border-radius:7px;background:color-mix(in srgb,var(--live) 10%,var(--card));color:var(--ink);font:650 .72rem/1 var(--mono)}.project-add:hover{background:color-mix(in srgb,var(--live) 18%,var(--card))}.project-list{list-style:none;margin:0;padding:0;display:grid;gap:.28rem}.project-list button{width:100%;display:flex;align-items:center;gap:.65rem;text-align:left;border:1px solid transparent;background:transparent;color:var(--soft);padding:.7rem .65rem;border-radius:8px;font-size:.82rem}.project-list button:hover{color:var(--ink);background:color-mix(in srgb,var(--rule) 32%,transparent)}.project-list button[aria-current=true]{color:var(--ink);border-color:var(--rule);background:var(--card)}.project-mark{width:.52rem;height:.52rem;border-radius:50%;background:var(--ok);flex:none}.project-mark.pending{background:var(--live);animation:project-pulse 1.5s ease-in-out infinite}@keyframes project-pulse{50%{opacity:.28}}.project-list .project-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.project-list .project-count{color:var(--soft);font:500 .68rem var(--mono)}.project-empty-rail{color:var(--soft);font-size:.78rem;padding:.75rem .45rem;line-height:1.5}.project-main{min-width:0;padding:1.4rem 1.5rem}.project-empty-state{min-height:26rem;display:grid;place-items:center;text-align:center;color:var(--soft)}.project-empty-state>div{max-width:28rem}.project-empty-state .cloud-glyph{width:3.4rem;height:3.4rem;margin:0 auto 1rem;display:grid;place-items:center;border:1px solid var(--rule);border-radius:50%;color:var(--live);font-size:1.4rem}.project-empty-state h3{color:var(--ink);font:700 1.08rem var(--mono);margin:0 0 .5rem}.project-empty-state p{font-size:.86rem;line-height:1.6;margin:0 0 1rem}.project-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1.2rem;padding-bottom:1.25rem;border-bottom:1px solid var(--rule)}.project-head h3{font:700 1.12rem var(--mono);margin:0 0 .35rem}.project-meta{display:flex;flex-wrap:wrap;gap:.45rem .8rem;color:var(--soft);font:500 .7rem var(--mono)}.project-meta .ready{color:var(--ok)}.project-head .primary{padding:.6rem .85rem;font-size:.76rem}.project-wait{margin-top:1.1rem;padding:1rem;border:1px solid color-mix(in srgb,var(--live) 45%,var(--rule));border-radius:9px;background:color-mix(in srgb,var(--live) 5%,var(--card))}.project-wait strong{display:block;font-size:.88rem;color:var(--ink)}.project-wait span{display:block;color:var(--soft);font-size:.8rem;margin-top:.3rem;line-height:1.5}.project-wait button{padding:.4rem .65rem;font-size:.72rem}.project-wait>button{margin-top:.8rem}.project-wait-actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.8rem}.project-wait-actions button{margin:0}.members-head{display:flex;justify-content:space-between;align-items:baseline;margin:1.25rem 0 .3rem}.members-head h4{margin:0;font:600 .7rem var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--soft)}.members-head span{font:500 .68rem var(--mono);color:var(--soft)}.member-list{list-style:none;margin:0;padding:0}.member-row{display:flex;align-items:center;gap:.8rem;padding:.88rem 0;border-bottom:1px solid var(--rule)}.member-row:last-child{border-bottom:0}.member-role{flex:none;color:var(--soft);font:500 .69rem var(--mono);border:1px solid var(--rule);border-radius:999px;padding:.2rem .5rem}.member-role.owner{color:var(--live);border-color:color-mix(in srgb,var(--live) 45%,var(--rule))}.member-row .ghost{flex:none;padding:.4rem .65rem;font-size:.71rem}.team-drawer{border-left:1px solid var(--rule);padding:1.25rem;background:color-mix(in srgb,var(--seal) 28%,var(--card))}.team-drawer[hidden]{display:none}.drawer-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}.drawer-head h3{font:700 .98rem var(--mono);margin:0 0 .25rem}.drawer-head p{color:var(--soft);font-size:.78rem;margin:0;line-height:1.45}.drawer-close{padding:0;border:0;background:transparent;color:var(--soft);font-size:1.1rem}.team-drawer label{margin-top:1.2rem}.team-drawer .primary{width:100%;margin-top:.65rem}.drawer-note{color:var(--soft);font-size:.74rem;line-height:1.5;margin-top:.8rem}.team-drawer .msg{white-space:normal}.project-dialog{width:min(29rem,calc(100vw - 2rem));border:1px solid var(--rule);border-radius:12px;background:var(--card);color:var(--ink);padding:0;box-shadow:0 24px 80px rgb(0 0 0/.55)}.project-dialog::backdrop{background:rgb(0 0 0/.65);backdrop-filter:blur(2px)}.project-dialog form{padding:1.35rem}.project-dialog h3{font:700 1.05rem var(--mono);margin:0 0 .4rem}.project-dialog p{color:var(--soft);font-size:.84rem;line-height:1.55;margin:0}.project-dialog .dialog-actions{display:flex;justify-content:flex-end;gap:.55rem;margin-top:1.2rem}.project-dialog .dialog-actions button{margin:0}.local-task-dialog{width:min(34rem,calc(100vw - 2rem))}.local-task-place{margin:1rem 0 0;padding:1rem;border:1px solid var(--rule);border-radius:9px;background:var(--paper)}.local-task-place strong{display:block;font-size:.83rem;margin-bottom:.3rem}.local-task-place p{margin:0}.local-task-fallback{margin-top:1rem;border-top:1px solid var(--rule);padding-top:.85rem}.local-task-fallback summary{cursor:pointer;color:var(--soft);font:600 .72rem var(--mono)}.local-task-command{display:flex;align-items:center;gap:.5rem;margin-top:.7rem}.local-task-command code{flex:1;padding:.65rem .75rem;border:1px solid var(--rule);border-radius:8px;background:var(--paper);color:var(--ink)}.local-task-command button{flex:none;padding:.55rem .7rem}@media(max-width:70rem){.projects-layout.drawer-open{grid-template-columns:13rem minmax(0,1fr)}.team-drawer{grid-column:1/-1;border-left:0;border-top:1px solid var(--rule)}}@media(max-width:47rem){.projects-card{width:auto;margin-left:0;transform:none;scroll-margin-top:10rem}.projects-top{padding:1.1rem;display:block}.projects-privacy{margin-top:.8rem}.invite-inbox{padding:0 1.1rem}.invite-card{align-items:flex-start;flex-wrap:wrap}.invite-actions{width:100%;padding-left:3.2rem}.projects-layout,.projects-layout.drawer-open{grid-template-columns:1fr;min-height:0}.project-rail{border-right:0;border-bottom:1px solid var(--rule);padding:.9rem}.project-rail-head{padding:0 0 .65rem}.project-list{display:grid;overflow:visible;padding:0}.project-list li{max-width:none}.project-list button{min-width:0}.project-empty-rail{padding:.5rem}.project-main{padding:1.1rem}.project-empty-state{min-height:17rem}.project-head{display:grid;grid-template-columns:minmax(0,1fr);align-items:start}.project-head .primary{justify-self:start;margin-top:.3rem}.members-head{display:block}.members-head span{display:block;margin-top:.35rem}.member-row{display:grid;grid-template-columns:2.2rem minmax(0,1fr);align-items:start}.member-row .member-avatar{grid-column:1;grid-row:1}.member-row .member-copy{grid-column:2;grid-row:1;min-width:0}.member-row .member-role,.member-row .ghost{grid-column:2;justify-self:start;margin-top:.1rem}.team-drawer{grid-column:auto;border-top:1px solid var(--rule);padding:1.1rem}}.boot{display:none;min-height:74vh;flex-direction:column;align-items:flex-start;justify-content:center;max-width:64rem;margin:0 auto;padding:0 1.5rem}body.booting>.boot{display:flex}body.booting>:not(.boot){display:none}.boot .logo{font-weight:750;letter-spacing:-.03em;font-size:1rem}.boot .track{width:min(21rem,100%);height:1px;background:var(--rule);margin-top:1.7rem;overflow:hidden}.boot .track i{display:block;height:100%;width:34%;background:var(--live);animation:cross 1.1s ease-in-out infinite}.boot .track[data-done=yes] i{width:100%;animation:none;transform:none}.boot p{font-family:var(--mono);font-size:.78rem;color:var(--soft);margin:.95rem 0 0}@keyframes cross{from{transform:translateX(-105%)}to{transform:translateX(325%)}}.boot>*{opacity:0;animation:boot-in .3s ease .24s forwards}@keyframes boot-in{to{opacity:1}}@media(prefers-reduced-motion:reduce){.boot>*{opacity:1;animation:none}.boot .track i{width:100%;transform:none}}footer{border-top:1px solid var(--rule);padding:2.4rem 0 3.6rem;color:var(--soft);font-size:.9rem}footer .r{font-family:var(--mono);font-size:.74rem;margin-top:.6rem}:root{--site-wide:80rem}.navbar>.wrap,.wrap.hero-wrap,.wrap.enterprise-wrap{max-width:var(--site-wide)}body.enterprise-page .wrap.enterprise-wrap{max-width:var(--site-wide)}.navbar #nav-logo{flex:none;white-space:nowrap;min-height:44px}body:not(.panel) .pills a,.navlink{min-height:44px;display:inline-flex;align-items:center}body:not(.panel) .pills a{font-size:.78rem;letter-spacing:.06em}.npmchip button{min-height:36px}.hero-demo-progress button{min-height:44px}.hero-demo-progress button::before{inset:21px 0 21px}.enterprise-story-frame{height:640px}.enterprise-story-frame iframe{width:100%;height:640px;transform:none}.enterprise-contact{margin:1rem 0 0;color:var(--ent-soft);font-size:.875rem}.enterprise-contact a{color:inherit;text-underline-offset:4px}body.account-page #account>.wrap{max-width:34rem}body.account-page #account h1.acct-intro{font-size:2.25rem;line-height:1.15;letter-spacing:-.04em}body.account-page #account .pane form{max-width:100%}body.account-page #account form>.primary{width:100%;justify-content:center}#account input{font-size:1rem}.docs-start{padding-bottom:2.5rem;border-bottom:1px solid var(--rule);margin-bottom:2.5rem}.docs-start h1{font-size:clamp(2.25rem,5vw,3.25rem)}.docs-paths{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem;margin-top:2rem}.docs-paths a{display:block;color:var(--ink);text-underline-offset:4px;font-weight:650;min-height:44px}.docs-paths p{font-size:.9rem;color:var(--soft);margin:0}#docs [id]{scroll-margin-top:calc(var(--nav-h) + 24px)}@media(max-width:70rem){.navbar .navlink{display:none}.navbar .npmchip{display:none}.navbar .pills{margin-left:auto;margin-right:0}}@media(max-width:47.5rem){body:not(.panel) #nav-logo{width:auto;min-height:44px;font-size:.88rem;overflow:visible}body:not(.panel) #nav-logo::before{content:none}body:not(.panel) #nav-logo::after{content:" ."}body:not(.panel) .pills{gap:1rem}body:not(.panel) .pills a{font-size:.78rem;letter-spacing:.02em}.hero-demo-progress{height:44px}.hero-demo-progress button::before{inset:21px 0 21px}.enterprise-story{margin-inline:0}.enterprise-story-frame,.enterprise-story-frame iframe{height:1400px}body:not(.panel) .hero{padding-top:3rem;padding-bottom:3rem;gap:1.5rem}.hero-demo{padding:1rem 0 0;margin:0}.docs-paths{grid-template-columns:1fr;gap:1.25rem}}@media(max-width:38rem){body:not(.panel){--nav-h:104px}body:not(.panel) .navbar .row{display:grid;grid-template-columns:1fr;gap:0;padding-top:4px;padding-bottom:4px}body:not(.panel) #nav-logo{justify-self:start}body:not(.panel) .navbar .pills{display:grid;grid-template-columns:repeat(4,1fr);width:100%;gap:0;margin:0}body:not(.panel) .pills a{justify-content:center;padding:.3rem .2rem;min-height:48px;font-size:.78rem;letter-spacing:0}body:not(.panel) .navbar>.wrap{padding-inline:16px}body:not(.panel) .hero{padding-top:2.5rem}}.wrap.hero-wrap{max-width:100rem}body:not(.panel):not(.enterprise-page):not(.docs):not(.account-page) .navbar>.wrap{max-width:100rem}body:not(.panel) .hero{grid-template-columns:minmax(340px,400px) minmax(0,1fr);align-items:start;gap:clamp(2.5rem,4vw,4rem);min-height:0;padding:4rem 0}.hero-copy .mega{font-size:clamp(3.8rem,6.5vw,6rem)}.hero-demo{padding:0;margin:0}.hero-demo-head{align-items:center;min-height:1.25rem}.hero-demo-head h2{font-size:clamp(1.2rem,1.65vw,1.55rem)}@media(max-width:80rem) and (min-width:75.01rem){body:not(.panel) .hero{grid-template-columns:340px minmax(0,1fr);gap:2.5rem}.hero-copy .mega{font-size:5.3rem}}@media(max-width:75rem){body:not(.panel) .hero{grid-template-columns:1fr;gap:2.5rem;padding:3rem 0}.hero-demo{margin:0;padding:0}}@media(min-width:48rem) and (max-width:75rem){.hero-copy .mega br{display:none}.hero-copy .mega{font-size:clamp(3.5rem,6.8vw,5rem);white-space:nowrap}body:not(.panel) .hero .lede{max-width:56ch}}@media(max-width:38rem){body:not(.panel) .hero{padding:2.5rem 0;gap:2rem}}</style><div class="boot" id="boot"><span class="logo">⌑ Sealkeep</span><div class="track" id="boot-track"><i></i></div><p id="boot-note" role="status" aria-live="polite">Signing you in</p></div><div class="navbar"><div class="wrap row"><a class="logo" href="#home" id="nav-logo">⌑ Sealkeep</a><a class="navlink guest" href="#how">How it works</a><a class="navlink guest" href="#get">Features</a><nav class="pills" aria-label="Site"><a href="#home" id="pill-home" class="guest on">Product</a><a href="#enterprise" id="pill-enterprise" class="guest">Enterprise</a><a href="#docs" id="pill-docs" class="guest">Docs</a><a href="#account" id="pill-account">Account</a></nav><span class="npmchip guest"><b class="ver">● v0.10</b><code id="cmd">npm install -g sealkeep</code><button id="copy" type="button">Copy</button></span></div></div><main id="home"><div class="wrap hero-wrap"><div class="hero"><div class="hero-copy"><p class="promise-eyebrow"><i></i>The custody promise</p><h1 class="mega" aria-label="Seal. Keep. Share.">SEAL<span class="dot">.</span><br>KEEP<span class="dot">.</span><br>SHARE<span class="dot">.</span></h1><p class="lede">Your agent's memory — <u>sealed</u> on your machine, <u>kept</u> in storage you choose, <u>shared</u> on your terms.</p><div class="cta-row"><a class="primary" href="#start">Get started →</a><a class="ghost" href="#docs">Read the docs</a></div><p class="meta">Node 22+ · macOS, Linux, Windows · one CLI · npm install -g sealkeep</p></div><section class="hero-demo" aria-label="How SealKeep works"><div class="hero-demo-head"><h2 id="hero-story-title">Preserve your agent memories</h2><span id="hero-story-index">01 / 03</span></div><div class="hero-demo-frame"><iframe id="hero-story" title="Preserve your agent memories" src="visual/index.html?embed=1#agent-memory" loading="eager"></iframe></div><div class="hero-demo-progress" role="group" aria-label="SealKeep capabilities"><button type="button" aria-label="Preserve your agent memories" aria-current="true"></button><button type="button" aria-label="Continue your work anywhere" aria-current="false"></button><button type="button" aria-label="Team cooperation" aria-current="false"></button></div></section></div></div><section class="custody" id="promise" aria-label="The custody promise"><div class="cols"><div class="col"><span class="idx">01 // Local E2EE</span><h3>Sealed on-device</h3><p>Encrypted before a byte leaves your machine. We don't have the keys. We never touch the plaintext.</p><span class="tail">Zero-knowledge architecture</span></div><div class="col"><span class="idx">02 // Sovereignty</span><h3>Kept where you choose</h3><p>Use your own bucket, personal Google Drive, or managed Sealkeep Cloud. Every destination receives only encrypted, verified archives.</p><span class="tail">User-controlled routing</span></div><div class="col"><span class="idx">03 // Collaboration</span><h3>Shared on your terms</h3><p>Bundles, recipients, and live bridges. Share across machines, models, and teams.</p><span class="tail">Cross-model memory</span></div></div></section><div class="wrap"><section class="center" id="how"><span class="kicker">How it works</span><h2>The order is the safety property</h2><p class="sub">Nothing is reclaimed before it is verified. Nothing is verified before it is sealed.</p><div class="term"><header><span class="dots">● ● ●</span><span class="title">you@yours — sealkeep autopilot</span><span class="run">● RUNNING</span></header>
|
|
3
|
+
<pre>$ sealkeep autopilot --reclaim
|
|
4
|
+
<i>✓</i> vault created <b>7a126017…</b> <i>✓</i> recovery phrase shown — <b>24 words</b> · once · not stored
|
|
5
|
+
<i>✓</i> hooks installed — claude, codex <span class="dim">preserve + upload automatically · reclaim explicitly enabled</span>
|
|
6
|
+
|
|
7
|
+
$ sealkeep autopilot status
|
|
8
|
+
sealed <b>412</b> · verified <b>412</b> · reclaimed <b>3.4 GB</b>
|
|
9
|
+
<span class="dim">last cycle 40s ago · next in 5m</span></pre>
|
|
10
|
+
</div><div class="how-grid"><div class="step-card"><span class="num">1</span><h3>Catch</h3><p>Your agent's session hook records that a transcript exists and where it lives. Nothing is read yet.</p><div class="foot">holds no secret · no network call · milliseconds</div></div><div class="step-card"><span class="num">2</span><h3>Seal</h3><p>A worker encrypts the session with a key made on this machine and wrapped twice — once for your phrase and once per trusted device.</p><div class="foot">revoking a laptop rewraps keys, not your data</div></div><div class="step-card"><span class="num">3</span><h3>Verify</h3><p>The sealed archive goes to your chosen storage, and Sealkeep reads it back. Byte count and checksum must match what was computed locally.</p><div class="foot">no match → not durable → local file stays</div></div><div class="step-card hot"><span class="num">4</span><h3>Reclaim</h3><p>Only a verified, searchable, unchanged and idle session becomes eligible. When you enable reclaim, eligible sessions can have their full local bytes removed. Supported top-level (non-subagent) Codex sessions keep a tiny same-ID resume pointer; unsupported and subagent Codex sessions keep their full local original.</p><div class="foot">off by default · verified remote copy required</div></div></div><p class="how-note">Autopilot preserves, indexes, and uploads on a loop. Reclaim joins that loop only after you explicitly enable it, and Sealkeep notifies you when disk is reclaimed or something needs a decision — never for routine sealing, which would only train you to ignore it.</p><div class="belt"><div><span class="ck">Setup</span><div class="b-v">Two commands</div></div><div><span class="ck">Recovery phrase</span><div class="b-v">24 words, shown once</div></div><div><span class="ck">Local originals removed</span><div class="b-v amber">Opt-in · after verification</div></div><div><span class="ck">Transcript bytes we receive</span><div class="b-v amber">Zero</div></div></div></section><section class="center" id="get"><span class="kicker">What you get</span><h2>Six months of reasoning,<br>still searchable</h2><div class="bento"><div class="b-card span2"><div class="b-split"><div><h3>Find the session by what you said in it</h3><p>Six weeks later you remember the bug, not the folder. The content index is sealed with the same machinery as your archives and synced beside them — so every machine of the vault searches your whole history, and your agents search it too, over MCP.</p><div class="foot">metadata instantly · full text on any machine holding your phrase</div></div><div class="b-term">$ sealkeep search "stripe webhook" --content
|
|
11
|
+
<div class="row"><span><b>08424eb6</b> …the stripe webhook was firing twice…</span><span class="dim">12 Aug</span></div><div class="row"><span><b>a91c40f7</b> …switched the webhook to the idempotency key…</span><span class="dim">4 Aug</span></div><div class="row"><span><b>3f7c02d1</b> …stripe webhook signing secret rotated…</span><span class="dim">27 Jul</span></div></div></div></div><div class="b-card glow span2"><div class="b-split"><div><span class="ck" style="color:var(--live)">The failure we had to prevent</span><h3>Archiving must never cost you a resume</h3><p>For supported top-level (non-subagent) Codex rollouts, reclaim leaves a valid three-line transcript at the same native path and session ID. Unsupported and subagent Codex rollouts stay on disk. Resume from an eligible pointer starts immediately; Codex can search preserved memory through the local Sealkeep MCP, while full byte restoration stays an explicit choice.</p></div><div class="b-term"><span class="dim">codex resume 08424eb6</span><i>✓</i> same session ID · lightweight pointer
|
|
12
|
+
called <b>sealkeep_search</b>("where did we stop?")
|
|
13
|
+
<span class="dim">full bytes: sealkeep recover <archive-id> --native --overwrite backup</span></div></div></div><div class="b-card"><h3>Actually frees local space</h3><p>Reclaim is off by default. When enabled, Sealkeep removes an unchanged local original only after its complete encrypted archive is verified remotely and covered by search.</p><div class="b-term"><i>✓</i> verified <b>9f2c1ab4</b> in storage
|
|
14
|
+
<span class="dim">eligible top-level Codex source → same-ID pointer</span><b>1.4 GB</b> reclaimed</div><p class="green">The remote archive stays encrypted, verified and explicitly restorable.</p></div><div class="b-card"><h3>Your bucket sees noise</h3><p>Every path segment — project, date, session — is an HMAC under a key derived from your phrase. No project names, no dates, no session ids.</p><div class="cipherbox" id="cipher" aria-hidden="true"></div><div class="foot">Prefer legible paths in your own bucket? One config flag.</div></div><div class="b-card"><h3>Watch a session live</h3><p>Share an ongoing session on a channel; teammates follow it read-only. Every delta is sealed on your machine before it moves, and followers verify a hash chain before a byte lands in their mirror.</p><div class="b-term">$ sealkeep bridge start s01.jsonl --channel review
|
|
15
|
+
sharing on <b>review</b> · 2 deltas · live
|
|
16
|
+
<span class="dim">follow with: sealkeep bridge watch review</span></div><div class="foot">sealed deltas · hash-chained · read-only for followers</div></div><div class="b-card"><h3>Restores on a machine that never saw it</h3><p>Each archive carries its own envelope, so a restore needs your phrase and an account. Not the original laptop, not a config file, not an index you forgot to copy.</p><div class="foot">hash checked after download<br>checked again after decryption</div></div><div class="b-card span2"><div class="b-split"><div><h3>Your agents know the vault is there</h3><p>One command registers a local MCP server with Claude Code and Codex, so the agent itself can search your sealed history and restore a session it needs.</p><div class="foot">all local · nothing new exposed to the network</div></div><div class="b-term">$ sealkeep mcp install
|
|
17
|
+
<i>✓</i> registered with claude · codex
|
|
18
|
+
<span class="dim">search · restore · status · retention · upload · prepare</span></div></div></div></div></section><div class="zk"><div class="wrap"><div class="cols"><div><span class="eyebrow" style="color:var(--live)">Zero knowledge, spelled out</span><h2>What the control plane can and cannot see</h2><p>It coordinates devices, quotas and upload permissions. Sealing happens before any of that, on your machine.</p><blockquote>Lose your recovery kit and every device you registered, and your archives are unreadable. Support cannot get them back. <strong>That is not a gap in the product — it is the product.</strong></blockquote></div><div class="holds"><span class="h">It holds</span><span class="h">It never receives</span><div>Account and device identifiers</div><div>Transcript content — no endpoint accepts it</div><div>Byte counts and timestamps</div><div>Archive keys — generated and wrapped on your machine</div><div>Checksums of sealed archives</div><div>Your recovery phrase — shown once, stored nowhere</div><div>Encrypted manifests it cannot read</div><div>Device private keys — only public halves are registered</div></div></div></div></div><section class="center" id="words"><span class="kicker">Your only key</span><h2>Twenty-four words,<br>not a base64 string</h2><p class="sub">Words survive bad handwriting, can be read aloud, and carry a checksum — so a misread word is caught and corrected rather than silently locking you out.</p><div class="kit"><div class="head"><span>Shown once · stored nowhere · never sent</span><b>sealkeep recovery kit</b></div><div class="words"><span><i>1</i>acorn</span><span><i>2</i>oxide</span><span><i>3</i>lynx</span><span><i>4</i>atom</span><span><i>5</i>voyage</span><span><i>6</i>harbor</span><span><i>7</i>pine</span><span><i>8</i>delta</span><span><i>9</i>mango</span><span><i>10</i>flint</span><span><i>11</i>east</span><span><i>12</i>clock</span><span><i>13</i>iris</span><span><i>14</i>brass</span><span><i>15</i>stork</span><span><i>16</i>maple</span><span><i>17</i>ridge</span><span><i>18</i>ember</span><span><i>19</i>quilt</span><span><i>20</i>fable</span><span><i>21</i>onyx</span><span><i>22</i>cedar</span><span><i>23</i>dune</span><span><i>24</i>cricket</span></div><p class="verify">Type it back with <code>sealkeep recovery verify</code> to prove your copy works. Case and spacing do not matter; a wrong word is named with suggestions — <em>Word 1 “acron” is not in the list. Did you mean acorn, arrow?</em></p></div></section><section class="center" id="plans"><span class="kicker">Pricing</span><h2>Whose bucket</h2><p class="sub">Not the same product with a different quota. On free, archives go to a bucket <strong style="color:var(--ink)">you own</strong> under your own credentials — we never hold them and cannot count them.</p><div class="pricing"><div class="price-card light"><div class="p-name">Your bucket</div><div class="p-price">$0 <small>forever · S3, R2, GCS, Drive, MinIO</small></div><p class="pick"><strong>Pick this if</strong> you have a cloud account and would rather hold your own data than trust anyone with it.</p><ul><li>Every personal-vault capability — sealing, verify, search, retention, MCP, restore</li><li>Guided setup: console links, filled-in commands, a least-privilege policy</li><li>Credentials go to your OS keychain, never a config file</li><li class="dash">You paste each IAM step yourself</li><li class="dash">No escrow — your recovery kit is the only copy of the key</li></ul><div class="cta"><a class="ghost" href="#docs">Read the storage setup</a></div></div><div class="price-card"><div class="p-name">Our bucket<span class="managed-chip">Managed</span></div><div class="p-price">$6 <small>/month · 100 GB, up to 2 TB</small></div><p class="pick"><strong>Pick this if</strong> you bought “it just works” — and would rather not be the only backup of your own key.</p><ul><li>No provider to name. Quotas and uploads are brokered for you.</li><li>A sealed copy of your key that survives losing the machine, openable only by your phrase</li><li>Storage meter, archive list and device revocation in the account panel</li><li>Your keys and your phrase still never leave your machine</li></ul><div class="packs"><span class="now">100 GB · $6</span><span>500 GB · $12</span><span>2 TB · $30</span></div><div class="cta"><a class="primary" href="#account">Create an account</a></div></div><div class="price-card slim"><div class="p-name">Pro Lite</div><div class="p-price">$2 <small>/month · your storage, our safety net</small></div><p class="pick"><strong>Pick this if</strong> you keep your own bucket or Google Drive — and want the parts of an account that save a bad day.</p><ul><li>Sealed key escrow — a copy we cannot open, your phrase can</li><li>Machines signed in: enroll a new laptop with one code, revoke a stolen one from the panel</li><li>Drive or bucket stays yours — we still hold no bytes</li></ul><div class="cta"><a class="ghost" href="#account">Add the safety net</a></div></div><div class="price-card slim"><div class="p-name">Team<span class="managed-chip">Cowork</span></div><div class="p-price">$9 <small>/seat · project-scoped team access</small></div><p class="pick"><strong>Pick this if</strong> one project's memory belongs to several people — and to their agents.</p><ul><li>Project-scoped members: a colleague opens what the team sealed, nothing else</li><li>Shared sessions require a managed project copy; managed storage is billed separately. Personal Drive and bucket routes stay private to their owner</li><li>Live session bridges, relayed through our plane — ciphertext in, ciphertext out</li><li>The round table: add and revoke members from the dashboard</li></ul><div class="cta"><a class="ghost" href="#account">Start a team</a></div></div></div><p class="price-note">Or skip storage entirely and keep everything local — Sealkeep still seals every archive, it just has nowhere to copy it.</p></section><section id="start"><div class="center"><span class="kicker">Getting started</span><h2>Four steps either way</h2><p class="sub">The phrase step is the one that matters.</p></div><div class="tabs"><button id="t-free" aria-selected="true">Free</button><button id="t-paid" aria-selected="false">Paid</button></div><div class="pane on" id="p-free"><ol class="steps"><li><h3>Install</h3><pre>npm install -g sealkeep</pre></li><li><h3>Choose free, pick a provider</h3><p>Three questions: provider, bucket, prefix.</p><pre>sealkeep start</pre></li><li><h3>Write down the phrase</h3><p>Twenty-four words, shown once, generated here and never sent anywhere.</p></li><li><h3>Create the bucket, then run it</h3><p>Sealkeep prints the console links, the commands, and a policy scoped to your prefix.</p><pre>sealkeep storage setup
|
|
19
|
+
sealkeep autopilot</pre></li></ol></div><div class="pane" id="p-paid"><ol class="steps"><li><h3>Install</h3><pre>npm install -g sealkeep</pre></li><li><h3>Create an account</h3><p>Below, or in the terminal. Nothing else to set up.</p><pre>sealkeep start</pre></li><li><h3>Write down the phrase</h3><p>Your keys are generated on your machine. We store a copy sealed with this phrase, which never reaches us.</p></li><li><h3>Let it run</h3><p>Your machine requests a signed URL and uploads directly. The bytes never pass through our servers.</p><pre>sealkeep autopilot
|
|
20
|
+
sealkeep cloud status</pre></li></ol></div></section><section class="center" id="honestly"><span class="kicker">Product & assurance roadmap</span><h2>What ships now,<br>and what advances next</h2><p class="sub">Production capabilities today, with dated milestones for the next assurance gates.</p><div class="ship"><div><span class="name">Capture and sealing<span class="d">Durable queue, crash-safe, deduplicating, resumable streaming</span></span><span class="tag ok">Shipping</span></div><div><span class="name">Restore<span class="d">Byte-identical explicit restore; supported top-level (non-subagent) Codex sessions keep a same-ID pointer that searches memory over MCP, while unsupported and subagent sessions keep their full local original</span></span><span class="tag ok">Shipping</span></div><div><span class="name">Retention<span class="d">Opt-in deletion of unchanged local originals, only after remote verification and search coverage</span></span><span class="tag ok">Shipping</span></div><div><span class="name">Search<span class="d">Sealed content index, synced across your machines as ciphertext</span></span><span class="tag ok">Shipping</span></div><div><span class="name">Agent integration<span class="d">MCP server for Claude Code and Codex, session hooks, live bridge</span></span><span class="tag ok">Shipping</span></div><div><span class="name">Encryption format<span class="d">Published v2 format, deterministic vectors, and tamper suite; independent-review package in preparation</span></span><span class="tag warn">Audit prep · Q3 ’26</span></div><div><span class="name">Cloud providers<span class="d">R2 production-backed; GCS retirement and S3/Drive production hardening tracked in the Q3 2026 provider milestone</span></span><span class="tag warn">Hardening · Q3 ’26</span></div><div><span class="name">Signed releases<span class="d">npm provenance and signed manifests ship today; native OS signing is the next supply-chain milestone</span></span><span class="tag warn">Roadmap · Q3 ’26</span></div></div><p class="ship-note"><strong>Independent-review preparation is active.</strong> Encryption format v2 uses published authenticated-encryption primitives and ships with deterministic vectors plus byte-by-byte tamper coverage. The public assurance roadmap tracks external cryptographic review as the next gate; no completed third-party audit is claimed today.</p></section><section class="center" id="faq"><span class="kicker">Questions</span><h2>The ones that actually matter</h2><details class="faq" open><summary>What happens if I lose the 24 words?</summary><div>Your phrase is the permanent recovery path, and Google sign-in alone cannot decrypt anything. Paid accounts can also create five one-time backup codes; each can recover the sealed vault key once after every device is lost. Keep the printed Recovery Kit offline and verify it while this machine still works.</div></details><details class="faq"><summary>Can Sealkeep delete my sessions by accident?</summary><div>Automatic reclaim is off by default. If you enable it, Sealkeep removes only the exact unchanged local bytes represented by a complete verified remote archive, after the configured idle and grace periods and after confirming that archive is searchable. Supported top-level (non-subagent) Codex sessions keep a same-ID pointer; unsupported and subagent Codex sessions keep their full local original. The full archive remains explicitly restorable. Deleting the remote archive is a separate account action.</div></details><details class="faq"><summary>Will archiving break <code>--resume</code>?</summary><div>No. For supported top-level (non-subagent) Codex sessions, reclaim leaves a tiny valid transcript under the original session ID, so <code>codex resume <session-id></code> still opens immediately and can call <code>sealkeep_search</code> for relevant memory. Unsupported and subagent Codex sessions keep their full local original. Full restoration is deliberately explicit—use the recovery command shown in that pointer—so startup never waits on a multi-gigabyte download.</div></details><details class="faq"><summary>Can you read my transcripts?</summary><div>No. Sessions are sealed on your machine before anything moves. Storage holds ciphertext, the control plane holds identifiers, counts and checksums, and no endpoint accepts transcript content. We hold no key that opens an archive.</div></details><details class="faq"><summary>Do I need a cloud account?</summary><div>No. The free plan writes to a bucket you already own — S3, R2, GCS, Drive, or anything S3-compatible. Or skip storage entirely and stay local: everything still gets sealed, it just has nowhere to copy.</div></details><details class="faq"><summary>Is the encryption audited?</summary><div>Not independently, yet — and we say so on this page rather than in a footnote. It is authenticated encryption on published primitives (ChaCha20-Poly1305 or AES-256-GCM), with deterministic test vectors, a tamper suite, and a written threat model in the repository.</div></details></section><div class="cta-final"><h2>Two commands.<br>Then nothing.</h2><p class="sub">Install it, write down twenty-four words, and stop choosing between your disk and your history.</p><div class="cta-row"><a class="primary" href="#start">Install Sealkeep</a><a class="ghost" href="#account">Create an account</a></div><p class="meta">No credit card for the free plan — it uses your own bucket</p></div></div></main><section class="enterprise" id="enterprise"><div class="wrap enterprise-wrap"><div class="enterprise-page-hero"><div class="enterprise-copy"><h1>Your code is in Git. Your AI engineering history isn’t.</h1><p>SealKeep preserves the work behind the code—decisions, commands, failed approaches, corrections, test results, and handoffs—in storage your company controls. You can run on-premises or in your cloud, with connected and fully isolated modes stated separately.</p><div class="cta-row"><a class="primary" href="mailto:security@spala.ai?subject=Sealkeep%20continuity%20pilot">Request a continuity pilot →</a><a class="ghost" href="mailto:security@spala.ai?subject=Sealkeep%20enterprise%20architecture%20review">Request an architecture review</a><p class="enterprise-contact">Email <a href="mailto:security@spala.ai">security@spala.ai</a> with your team size and deployment preference.</p></div></div><div class="enterprise-story" aria-label="Interactive Sealkeep enterprise examples"><div class="enterprise-story-frame"><div class="enterprise-story-preview" aria-hidden="true"><div class="enterprise-preview-tabs"><span>Knowledge stays</span><span>Team collaboration</span><span>On-prem isolation</span><span>Incidents</span></div><div class="enterprise-preview-title"><strong>Senior engineering decisions become company memory.</strong><small>Preparing live example</small></div><div class="enterprise-preview-flow"><article><b>Senior engineer</b><i></i><span>Decisions<br>Trade-offs<br>Implementation history</span></article><article><b>Company memory</b><i></i><span>Encrypted project history<br>Source attribution<br>Retention policy</span></article><article><b>Team continues</b><i></i><span>Faster onboarding<br>Trusted context<br>No single-person dependency</span></article></div></div><iframe id="enterprise-story" title="Sealkeep enterprise knowledge and deployment examples" src="visual/index.html?embed=1#enterprise-boundary" loading="eager"></iframe></div></div></div><section class="enterprise-loss-story" aria-labelledby="enterprise-loss-title"><div class="enterprise-loss-copy"><span class="kicker">The work Git cannot keep</span><h2 id="enterprise-loss-title">Your code survives in Git. The work that produced it often doesn’t.</h2><p>A developer spends three days with Claude Code:</p><ol><li><span>01</span>Debugging a production race condition</li><li><span>02</span>Discovering two failed approaches</li><li><span>03</span>Correcting the agent</li><li><span>04</span>Learning an undocumented system constraint</li><li><span>05</span>Arriving at the final fix</li></ol></div><div class="enterprise-loss-ledger" aria-label="What Git and SealKeep preserve"><div class="loss-git"><span>Git keeps</span><strong>The final diff</strong><code>1 file changed<br>8 insertions · 3 deletions</code><small>The end state. Not the path.</small></div><div class="loss-arrow" aria-hidden="true">→</div><div class="loss-sealkeep"><span>SealKeep preserves</span><strong>The work behind it</strong><ul><li>Decisions and reasoning</li><li>Commands and outputs</li><li>Failed approaches</li><li>Human corrections</li><li>Tests and results</li><li>Handoffs and transcript</li></ul></div></div><p class="enterprise-aha">SealKeep makes that engineering history a company-controlled project record.</p></section><div class="enterprise-ownership" aria-label="How engineering systems divide project knowledge"><div><strong>Git</strong><span>WHAT changed</span><p>Final diffs, commits, branches, and tags.</p></div><div><strong>Jira + Linear</strong><span>WHAT was requested</span><p>Intent, tickets, acceptance criteria, and status.</p></div><div><strong>Slack</strong><span>WHAT people discussed</span><p>Useful fragments, scattered across conversations.</p></div><div class="sealkeep-owner"><strong>SealKeep</strong><span>HOW engineer + agent arrived</span><p>Decisions, commands, dead ends, corrections, tests, and handoffs.</p></div></div><div class="enterprise-proof" aria-label="Enterprise deployment commitments"><div><span>01 // Key custody</span><strong>Keys stay on customer endpoints</strong><p>Recovery phrases and device private keys are not sent to the backend or to Spala.</p></div><div><span>02 // Data residency</span><strong>You select every data region</strong><p>Application, PostgreSQL, object storage, logs, and backups stay where you place them.</p></div><div><span>03 // Entitlement</span><strong>Offline licensing is available</strong><p>The isolated edition disables the network licence check or validates entitlement locally.</p></div><div><span>04 // Administration</span><strong>No standing provider access</strong><p>Support access is disabled by default and, if approved, can be time-bounded and logged.</p></div></div><div class="enterprise-section-head"><span class="kicker">Built for high-impact moments</span><h2>Company memory for the moments that cost the most.</h2><p>AI-assisted engineering work becomes durable company memory: searchable by authorised teammates and available as project-scoped context to their coding agents.</p></div><div class="enterprise-value-grid" aria-label="Enterprise AI engineering memory use cases"><article class="enterprise-value featured"><span class="value-num">01 // Employee + contractor offboarding</span><h3>When the engineer leaves, project knowledge stays.</h3><p>In shared Team projects, new archives are encrypted for every accepted project member. Removing a contributor does not remove access already held by remaining members, while personal vaults stay outside project membership.</p><span class="value-proof">Accepted member recipients · managed project copy · personal vault isolation</span></article><article class="enterprise-value featured"><span class="value-num">02 // Incident reconstruction</span><h3>Recover what the engineer and agent actually did.</h3><p><b>Production auth bug introduced Monday.</b> Git shows the code change. Relevant preserved history can show the original request, agent session, terminal commands, failed attempts, human correction, tests that ran, and transcript—without implying automatic commit provenance.</p><span class="value-proof">Local content search · project filters · activity history</span></article><article class="enterprise-value"><span class="value-num">03 // Onboarding + handoffs</span><h3>Continue where somebody else stopped.</h3><p>Give a new hire, on-call engineer, or replacement contractor the preserved context needed to continue: what was attempted, what changed, which constraints mattered, and where the previous session stopped.</p><span class="value-proof">Preserved handoffs · content search · exact native restore</span><div class="value-flow" aria-hidden="true"><i>Previous engineer</i><em>→</em><i>Next engineer</i></div></article><article class="enterprise-value"><span class="value-num">04 // Shared agent context</span><h3>Codex can reuse what Claude learned.</h3><p>Project-scoped memory lets invited teammates move useful historical context between supported coding agents and approved machines without silently retraining a model or broadening project access.</p><span class="value-proof">Project scope · member keys · Claude Code and Codex</span><div class="value-flow" aria-hidden="true"><i>Claude Code</i><em>→</em><i>Codex</i></div></article><article class="enterprise-value"><span class="value-num">05 // Senior → junior transfer</span><h3>Juniors inherit trusted project history.</h3><p>Authorised teammates can search the implementation patterns, decisions, failed approaches, and handoffs preserved by senior engineers, then use that context to move forward rather than rework the past.</p><span class="value-proof">Local MCP recall · project scope · revocable member access</span><div class="value-flow" aria-hidden="true"><i>Senior history</i><em>→</em><i>Junior + agent</i></div></article><article class="enterprise-value"><span class="value-num">06 // Secret control</span><h3>Control what becomes retained evidence.</h3><p>Run deterministic offline scanning before sealing. Known credential patterns and suspicious high-entropy values produce masked findings; high-confidence results can fail a gate without copying the secret into a report.</p><span class="value-proof">Offline scan · masked previews · JSON and exit codes</span><div class="value-flow" aria-hidden="true"><i>Raw context</i><em>→</em><i>Sealed memory</i></div></article></div><div class="enterprise-operations" aria-label="Local security automation interfaces"><div class="enterprise-operations-copy"><span class="kicker">Security operations</span><h3>Because preserved agent history is sensitive, security controls travel with it.</h3><p>Secret scanning supports knowledge retention by controlling what becomes preserved evidence. Investigation and control workflows stay inside the customer boundary.</p></div><div><div class="enterprise-interfaces"><div class="enterprise-interface"><code>sealkeep scan … --json</code><p><b>Pre-seal control:</b> masked findings and high-confidence exit status for scripts, CI gates, or customer monitoring.</p></div><div class="enterprise-interface"><code>sealkeep search … --content --json</code><p><b>Incident investigation:</b> search preserved history locally and keep retrieval scoped to the authorised project.</p></div><div class="enterprise-interface"><code>sealkeep_search via MCP</code><p><b>Agent continuity:</b> let an approved coding agent retrieve relevant project memory through its local SealKeep connection.</p></div><div class="enterprise-interface"><code>sealkeep audit --json</code><p><b>Operational evidence:</b> export the local record of what SealKeep did to files for customer-owned review workflows.</p></div><div class="enterprise-interface"><code>sealkeep autopilot status</code><p><b>Local posture:</b> check whether automatic preservation is configured on the current machine.</p></div></div><p class="enterprise-interface-note">Available today as local CLI JSON and MCP interfaces. Sealkeep does not require a hosted plaintext search API; customer-specific SIEM or REST connectors can be scoped for an isolated enterprise deployment.</p></div></div><div class="enterprise-section-head compact"><span class="kicker">Why the boundary matters</span><h2>Agent sessions contain more than chat.</h2><p>They can include source code, incident details, infrastructure names, customer records, credentials accidentally pasted into a prompt, and decisions that reveal how critical systems work. Encryption is necessary; control over where the system runs is a separate requirement.</p></div><div class="enterprise-needs"><article><span>Residency</span><h3>Keep operational data in the approved region</h3><p>Your organisation selects the runtime, database, object-store, log, backup, and administrator-access locations.</p></article><article><span>Custody</span><h3>A third party does not need the archive or its keys</h3><p>Sessions are encrypted before archival, and storage credentials remain within the customer’s configured secret boundary.</p></article><article><span>Network policy</span><h3>Support strict no-egress environments</h3><p>The fully isolated agreement replaces the runtime licence path with an offline entitlement and removes mandatory Spala dependencies.</p></article><article><span>Control</span><h3>Use your own IAM and operating procedures</h3><p>Your administrators govern access, TLS, monitoring, retention, backups, incident response, and patch deployment.</p></article></div><div class="enterprise-section-head compact"><span class="kicker">Deployment choice</span><h2>“Self-hosted” is not one boundary.</h2><p>The order form names the exact mode so a connected licence path is never mistaken for a fully isolated deployment.</p></div><div class="enterprise-modes" aria-label="Self-hosted deployment choices"><div class="enterprise-mode"><div class="mode-name"><i></i><h3>Connected self-hosted</h3></div><p>The application, PostgreSQL, logs, and archive storage remain customer-operated. The deployment may contact Spala for licence validation; that path and its transmitted fields must be documented for review.</p><ul><li>Customer infrastructure and selected regions</li><li>Customer database and object storage</li><li>Documented outbound licence path</li></ul></div><div class="enterprise-mode isolated"><div class="mode-name"><i></i><h3>Fully isolated self-hosted</h3></div><p>Delivered under a special enterprise agreement with an offline entitlement and no mandatory Spala API, database, object store, authentication, telemetry, or licence call during ordinary operation.</p><ul><li>No mandatory runtime egress to Spala</li><li>No standing HM Software administrator access</li><li>Customer-controlled support window, if requested</li></ul></div></div><div class="enterprise-responsibility"><div class="enterprise-section-head compact"><span class="kicker">Shared responsibility</span><h2>You operate the boundary. We deliver the product.</h2></div><div class="enterprise-responsibility-grid"><div><span>HM Software / Spala</span><ul><li>Exported application bundle and deployment documentation</li><li>Build, version, and integrity information</li><li>Security updates and agreed support</li><li>Disclosure of optional external dependencies</li></ul></div><div><span>Customer</span><ul><li>Runtime, network, PostgreSQL, storage, and TLS</li><li>Identity, access, secrets, regions, and retention</li><li>Monitoring, backups, restores, and patch timing</li><li>Endpoint, recovery-material, and recipient lifecycle</li></ul></div></div></div><div class="enterprise-review"><div class="enterprise-section-head compact"><span class="kicker">Security & procurement review</span><h2>Bring architecture, legal, and operations into the same review.</h2><p>Inspect the public evidence before booking a call. Each artifact is presented as a published statement, public template, or open assurance item; customer-specific terms, regions, subprocessors, retention, support access, and deployment mode are completed in the agreement.</p></div><div class="enterprise-review-grid"><div><span>Contract · public template</span><b>Data Processing Agreement</b><small>Article 28 roles, instructions, assistance, deletion, and audit terms</small><a class="artifact-link" href="/trust.html#dpa">Review template →</a></div><div><span>Security · control statement</span><b>Technical and organisational measures</b><small>Encryption, identity, network, development, incident, and continuity controls</small><a class="artifact-link" href="/trust.html#toms">Review controls →</a></div><div><span>Architecture · published</span><b>Architecture and data flow</b><small>Components, operators, visibility, key custody, residency, and egress paths</small><a class="artifact-link" href="/trust.html#architecture">Inspect diagram →</a></div><div><span>Ownership · published</span><b>Deployment responsibility matrix</b><small>Clear ownership across product delivery, infrastructure, keys, support, and operations</small><a class="artifact-link" href="/trust.html#responsibility">Inspect matrix →</a></div><div><span>Vendors · verification open</span><b>Subprocessor register</b><small>Applicable to Spala-selected hosted services—not customer-selected infrastructure</small><a class="artifact-link" href="/trust.html#subprocessors">Inspect register →</a></div><div><span>Assurance · roadmap</span><b>Threat model, SBOM & audit roadmap</b><small>Known limitations, release evidence, completed tests, and independent-review gates</small><a class="artifact-link" href="/trust.html#assurance">Inspect evidence →</a></div></div><div class="enterprise-assurance"><strong>Assurance stated plainly.</strong><p>Sealkeep does not claim a completed independent cryptographic audit today. The current Team model preserves access through remaining accepted project members; it does not yet claim a separate organization recovery recipient or transferable project-owner role. Test vectors, tamper tests, and deployment rehearsals support review but do not replace customer validation or legal advice.</p></div></div></div><div class="enterprise-docs"><p><strong>Prove continuity on one repository.</strong><span>Preserve one team’s Claude Code and Codex history for 14 days. Then test whether another engineer can find and continue work they did not personally participate in.</span></p><div><a class="primary" href="mailto:security@spala.ai?subject=Sealkeep%20continuity%20pilot">Run a continuity pilot →</a><a href="/trust.html">Open public Trust Center</a><a href="mailto:legal@spala.ai?subject=Sealkeep%20enterprise%20procurement%20pack">Request the procurement pack</a></div></div></section><section id="docs"><div class="wrap"><div class="docs-start"><h1>Start with your first session.</h1><p class="sub">Install Sealkeep, choose where your history lives, and connect your coding agent.</p>
|
|
21
|
+
<pre><code>npm install -g sealkeep
|
|
22
|
+
sealkeep start</code></pre>
|
|
23
|
+
<div class="docs-paths"><div><a href="#docs-storage">Personal storage</a><p>Set up your own bucket or Google Drive.</p></div><div><a href="#docs-team">Team setup</a><p>Connect an account and share a project.</p></div><div><a href="#enterprise">Enterprise deployment</a><p>Explore company-controlled and isolated deployments.</p></div></div></div><div id="docs-team" class="docs-start"><h2>Bring your team’s context together.</h2><p class="sub">Choose the Team plan during setup, sign in, and open Projects in your account to create a shared project and invite teammates.</p><a class="ghost" href="#account">Open your account →</a></div><div id="docs-commands"><span class="eyebrow" style="color:var(--live)">Reference</span><h2 style="font-size:clamp(2rem,5vw,2.9rem)">Commands</h2><p class="sub">Every command takes <code>--json</code>. Run <code>sealkeep help</code> for the full list.</p></div><div class="cmdtable"><div><code>sealkeep autopilot</code><p>Preserve, index and upload automatically; keep original sessions by default</p></div><div><code>sealkeep autopilot --reclaim</code><p>Explicitly allow this machine to reclaim eligible originals</p></div><div><code>sealkeep autopilot status · off</code><p>Check on it, or stop it — every archive stays</p></div><div><code>sealkeep quickstart</code><p>Same setup, no background service</p></div><div><code>sealkeep status · doctor</code><p>What is archived; whether this machine is healthy</p></div><div><code>sealkeep queue run</code><p>Encrypt everything waiting</p></div><div><code>sealkeep daemon</code><p>Watch, seal, index and upload; reclaim only when enabled</p></div><div><code>sealkeep search <query></code><p>Metadata search; <code>--content</code> searches inside sessions</p></div><div><code>sealkeep recover <id> <dest></code><p>Restore original bytes; <code>--native</code> puts them back where they came from</p></div><div><code>sealkeep recover <filename></code><p>The file an agent's resume just complained about, back in place</p></div><div><code>sealkeep archive <path> --stream</code><p>Seal straight to the bucket as a chunk folder, resumable mid-upload</p></div><div><code>sealkeep verify</code><p>Fetch every durable archive back and check it against the hash recorded at upload</p></div><div><code>sealkeep bridge start · watch</code><p>Live-share an ongoing session on a channel; members follow a hash-verified mirror, read-only</p></div><div><code>sealkeep share <id></code><p>A sealed bundle under a one-time passcode — the phrase never travels</p></div><div><code>sealkeep mcp install</code><p>Register the vault as a tool server with Claude Code and Codex</p></div><div><code>sealkeep retention apply</code><p>Dry run; <code>--confirm</code> moves sources to the trash</p></div><div><code>sealkeep recovery kit · verify · seal</code><p>Printable sheet, type-it-back check, and a sealed escrow copy</p></div><div><code>sealkeep ui · tui</code><p>The dashboard, in a browser or the terminal</p></div><div><code>sealkeep audit</code><p>Everything Sealkeep has done to your files</p></div></div><div class="arch"><div class="b-card"><h3 id="docs-storage">A bucket without the usual afternoon</h3><p>The step people give up on is cloud permissions. So Sealkeep writes it out — the console link, the commands with your own bucket and prefix already filled in, and a policy that grants the least it can.</p><div class="b-term">$ sealkeep storage setup --provider r2
|
|
24
|
+
<span class="dim">console link · bucket + prefix filled in · policy printed</span>
|
|
25
|
+
$ sealkeep storage credentials
|
|
26
|
+
<span class="dim">keys → OS keychain, never a config file</span></div><p style="margin-top:.9rem">For S3 that is an IAM policy scoped to <code>bucket/prefix/*</code> with no delete permission and no wildcard resource. For GCS, a service account with an IAM condition pinned to your prefix. Nothing runs automatically — you read each step and paste it yourself.</p><div class="foot">S3 · Cloudflare R2 · Google Cloud Storage · Google Drive · S3-compatible like MinIO<br>credentials → macOS Keychain, Secret Service, or Windows DPAPI</div></div><div class="b-card"><h3>Encryption, format v2</h3><p>A random per-archive key encrypts the session in chunks under ChaCha20-Poly1305 (or AES-256-GCM), and that key is wrapped separately for each recipient — your phrase, and each device you trust.</p><p>Every chunk is authenticated over its own position, the archive's identity, and the total chunk count — so reordering, truncation, splicing and suite downgrade are all detectable rather than silent.</p><div class="b-term">"VLA1" | len | envelopeJson | ciphertext</div><p class="caution">Not independently audited. Deterministic vectors and a full tamper suite; assumptions and limits are in the threat model.</p></div><div class="b-card"><h3>Autopilot and the keystore trade-off</h3><p>To unlock the vault without a human present, autopilot keeps your phrase in the OS keystore. Stated plainly: <strong>anything running as you can read it once you are logged in.</strong></p><div class="b-term">$ sealkeep autopilot --no-remember
|
|
27
|
+
<span class="dim"># supply the phrase per run instead of storing it</span></div><p style="margin-top:.9rem">Either way, the phrase you wrote down stays your disaster-recovery path. Prefer to drive it yourself? <code>sealkeep quickstart</code> does the same setup with no background service.</p></div><div class="b-card"><h3>Proving the archives are really there</h3><p>A backup nobody has tested is a hope. <code>sealkeep verify</code> fetches every durable archive back and checks it against the hash recorded at upload.</p><div class="b-term"><i>ok</i> <b>88de9eb4…</b> 3.9 KB
|
|
28
|
+
<i>ok</i> <b>847571ef…</b> 2.5 MB
|
|
29
|
+
<span style="color:var(--warn)">warn</span> roundtrip-178… listed, no object behind it</div><p style="margin-top:.9rem">It re-downloads rather than trusting a <code>HEAD</code> — an object that lists but cannot be read back is named, not counted.</p></div></div></div></section><section id="account"><div class="wrap"><h1 class="acct-intro">Your account</h1><p class="sub acct-intro">Sign in to manage storage, machines, recovery, and shared projects. New to SealKeep? Create an account here.</p><div class="acct-why acct-intro"><div class="why-card"><span class="ck">Free · $0 · no account</span><h3>Your personal vault in your bucket or Drive</h3><p>Personal sealing, verification, search, retention, MCP recall, and restore. S3, R2, GCS, Google Drive, MinIO. Your credentials, your custody. Shared team projects use managed project memory so every approved member can reach the same history.</p><span class="tailln">sealkeep start → choose free</span></div><div class="why-card"><span class="ck">Pro Lite · $2/mo</span><h3>Your storage, our safety net</h3><p>A sealed copy of your key in escrow — openable only by your phrase — and every machine signed in: enroll a new laptop with one code, revoke a stolen one from this panel.</p><span class="tailln">for people on their own bucket or gdrive</span></div><div class="why-card"><span class="ck">Managed · $6–$30/mo</span><h3>Our bucket, zero setup</h3><p>100 GB to 2 TB of managed storage for your ciphertext. Storage meter, archive list, and device revocation live in this panel once you sign in.</p><span class="tailln">quotas brokered · keys never leave your machine</span></div><div class="why-card"><span class="ck">Team · $9/seat</span><h3>Cowork through our relay</h3><p>Members scoped to a project open what the team sealed — nothing else. Live session bridges relay through our plane: ciphertext in, ciphertext out.</p><span class="tailln">one project · one memory · several people</span></div></div><p class="acct-loop acct-intro">The loop: create the account here → it hands you an enroll code → <code>sealkeep enroll <code></code> on your machine, where the keys are made. This page then becomes your panel: storage, machines, members, revocation.</p><div class="tabs" id="auth-tabs"><button id="t-signup" aria-selected="true">Create account</button><button id="t-signin" aria-selected="false">Sign in</button></div><div class="notice soft" id="connect-auth-intro" style="display:none"><p><strong>A machine is waiting for your approval.</strong> Create an account or sign in here, then review exactly what is asking to connect.</p></div><form class="pane on" id="p-signup"><button class="google" type="button" id="g-up"><svg viewBox="0 0 48 48" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.5 0 6.6 1.2 9 3.6l6.8-6.8C35.6 2.4 30.2 0 24 0 14.6 0 6.5 5.4 2.6 13.2l7.9 6.1C12.4 13.2 17.7 9.5 24 9.5z"/><path fill="#4285F4" d="M46.1 24.6c0-1.6-.1-3.1-.4-4.6H24v9.1h12.4c-.5 2.9-2.2 5.3-4.7 7l7.7 6c4.5-4.2 6.7-10.3 6.7-17.5z"/><path fill="#FBBC05" d="M10.5 28.7c-.5-1.4-.8-2.9-.8-4.7s.3-3.3.8-4.7l-7.9-6.1C1 16.3 0 20 0 24s1 7.7 2.6 10.8l7.9-6.1z"/><path fill="#34A853" d="M24 48c6.5 0 11.9-2.1 15.9-5.8l-7.7-6c-2.1 1.4-4.9 2.3-8.2 2.3-6.3 0-11.6-3.7-13.5-9.8l-7.9 6.1C6.5 42.6 14.6 48 24 48z"/></svg>
|
|
30
|
+
Continue with Google
|
|
31
|
+
</button><div class="or">or use a password</div><div class="row2"><div><label for="su-email">Email</label><input id="su-email" type="email" autocomplete="email" required></div><div><label for="su-label">Account name</label><input id="su-label" autocomplete="name" required></div></div><label for="su-pass">Password</label><input id="su-pass" type="password" autocomplete="new-password" placeholder="at least 12 characters" required minlength="12"><button class="primary" id="su-go">Create account</button><p class="msg" id="su-msg" role="status" aria-live="polite"></p></form><form class="pane" id="p-signin"><button class="google" type="button" id="g-in">
|
|
32
|
+
Continue with Google
|
|
33
|
+
</button><div class="or">or use a password</div><label for="si-email">Email</label><input id="si-email" type="email" autocomplete="email" required><label for="si-pass">Password</label><input id="si-pass" type="password" autocomplete="current-password" required><button class="primary" id="si-go">Sign in</button><p class="msg" id="si-msg" role="status" aria-live="polite"></p></form><div class="pane" id="p-account"><div class="ph" id="a-strip" tabindex="-1"><span class="name" id="a-label">—</span><button class="chip" id="a-plan" type="button" title="See your plan">—</button><span class="who" id="a-who">—</span></div><h1 class="acct-hero" id="a-hero"></h1><p class="msg" id="a-summary" style="margin:.4rem 0 0"></p><nav class="acct-nav" aria-label="Account sections"><a href="?view=overview#account" data-account-view="overview">Overview</a><a href="?view=projects#account" data-account-view="projects">Projects</a><a href="?view=storage#account" data-account-view="storage">Storage</a><a href="?view=machines#account" data-account-view="machines">Machines & recovery</a><a href="?view=account#account" data-account-view="account">Account</a></nav><div class="card next" id="device-connect" data-account-section="overview machines" style="display:none"><span class="eyebrow">Connect a machine</span><h2 class="lead" id="device-connect-title">Review this request</h2><p id="device-connect-copy">Loading the machine details…</p><div class="inline" id="device-connect-actions" style="display:none"><button class="primary" id="device-connect-approve" type="button">Approve this machine</button><button class="ghost" id="device-connect-deny" type="button">Deny</button></div><p class="msg" id="device-connect-msg" role="status" aria-live="polite"></p><p class="why">Approval lets this machine use your account and encrypted storage. It does not give Sealkeep—or this browser—the key that opens an archive.</p></div><div class="card next" id="a-firstrun" data-account-section="overview machines" style="display:none"><span class="eyebrow">Set up</span><h2 class="lead">Connect your first machine</h2><p>Sealkeep runs on your machine — that is where your keys are made and where every session is sealed before it moves. Pair one machine and archiving carries on by itself.</p><div id="pair-home-first"></div></div><div class="notice soft" id="a-verify" data-account-section="overview account" style="display:none"><p><strong>Confirm your email.</strong> It is how we reach you if an archive fails to store. Nothing else is sent to it.</p><button class="primary" id="a-verify-go" type="button">Send the link</button><span class="saved" id="a-verify-msg" role="status" aria-live="polite"></span></div><div class="card" id="c-recovery" data-account-section="overview machines"><span id="anchor-recovery"></span><span class="eyebrow">Recovery key</span><h2 id="e-head">Loading…</h2><p id="e-text"></p><div class="card-remedy" id="e-actions" hidden><button class="primary" id="e-fix" type="button">Open creation steps</button><button class="ghost" id="e-retry" type="button" hidden>Try again</button><span class="msg" id="e-action-msg" role="status" aria-live="polite">Recovery material is created only in a local SealKeep dashboard.</span></div></div><div class="card" id="anchor-storage" data-account-section="overview storage"><span class="eyebrow">Storage</span><div id="a-store-free" style="display:none"><h2>You run the bucket</h2><p>Archives go to storage you own, under your own credentials. Sealkeep never holds them, so there is nothing to meter here.</p><p class="empty">Your machine reports what it has stored. Run <code>sealkeep status</code> there, or open its dashboard with <code>sealkeep ui</code>.</p></div><div id="a-store-paid" style="display:none"><h2>What you keep here</h2><div class="kv"><div class="lead"><span class="k">Stored</span><b id="a-used">—</b><span class="cap" id="a-used-cap">sealed before it arrived</span></div><div><span class="k">Included</span><b id="a-quota-b">—</b><span class="cap">in your plan</span></div><div><span class="k">Archives</span><b id="a-count">—</b><span class="cap">chunk folders count as one</span></div><div><span class="k">Machines</span><b id="a-machines">—</b><span class="cap">signing uploads</span></div><div><span class="k">Destinations</span><b id="a-dest">—</b><span class="cap">where new seals go</span></div></div><div class="bar"><i id="a-bar"></i></div><p class="msg" id="a-quota"></p><div class="card-remedy" id="a-quota-actions" hidden><button class="primary" id="a-quota-fix" type="button">Review storage options</button><span class="msg">Choose more managed capacity or route new archives to storage you own.</span></div></div></div><div class="card" id="c-targets" data-account-section="storage"><span id="anchor-routing"></span><span class="eyebrow">Routing</span><h2>Where new archives go</h2><p>Rules live on your account — every machine of the vault follows the same ones.</p><ul class="ruleslist"><li><b>The first available destination receives new archives.</b></li><li><b>A project pin overrides the ordinary order.</b></li><li><b>Full or capped targets pass to the next in the order.</b></li></ul><p class="msg" id="tg-msg">Loading…</p><div id="tg-wrap" style="display:none;overflow-x:auto"><table><thead><tr><th>Target</th><th>Where</th><th>Order</th><th>Cap (GB)</th><th>Project routing</th><th></th></tr></thead><tbody id="tg-body"></tbody></table></div><div class="inline" style="margin-top:.9rem"><button class="ghost" id="tg-add-drive" type="button">Add Google Drive</button><button class="ghost" id="tg-add-bucket" type="button">Add my own bucket</button><button class="primary" id="tg-save" type="button">Save routing</button><span class="saved" id="tg-save-msg" role="status" aria-live="polite"></span></div><p class="why" id="tg-note">These rules choose storage; they do not grant project access. Project and folder identities are detected on each Sealkeep machine and never inferred from a typed name here. Use the local dashboard's project picker to pin a stable project identity to a destination. Open <b>Projects</b> here only to manage a shared memory pool and its members.</p><p class="why">Google Drive is a personal-vault destination, not the shared-project memory plane. It holds whole archive objects and skips streaming; big under-pressure seals route to cloud or a bucket on their own. Shared team sessions always keep their managed project copy in Sealkeep Cloud. Adding Drive here sets the personal routing rule — the one-time consent happens on each machine.</p></div><div class="card" id="anchor-machines" data-account-section="machines"><span class="eyebrow">Machines</span><h2>Connected machines</h2><p>Each connected machine signs its own uploads. Revoking one stops it writing here; the archives it already stored are untouched.</p><div id="pair-top"></div><ul class="lines" id="d-list"></ul><div id="d-retired"></div><p class="empty" id="d-msg">Loading…</p><!-- One pairing block exists on the page. It leads the panel while no
|
|
34
|
+
machine is connected, and JS moves it back into this slot once one
|
|
35
|
+
is, so the same code is never printed in two places. --><div id="pair-home-more"><div class="pair" id="pair"><p class="side" id="pair-side" style="display:none">Pairing code</p><!-- Grouped and labelled, so the copy button is announced as
|
|
36
|
+
belonging to the code rather than as a bare "Copy". --><div class="pair-field" id="pair-shown" role="group" aria-labelledby="pair-side" style="display:none"><code class="pair-code" id="pair-code"></code><button class="primary" id="pair-copy" type="button">Copy</button></div><p class="fine" id="pair-note"></p><div class="inline" id="pair-ask"><button class="ghost" id="pair-go" type="button">Show a pairing code</button><span class="saved" id="pair-msg" role="status" aria-live="polite"></span></div><details class="alt" id="pair-alt" style="display:none"><summary>Set the machine up from the terminal instead</summary><p class="empty">The same code, typed as one command. Needs Node 22 or newer.</p><div class="pair-field"><pre id="pair-cli"></pre>
|
|
37
|
+
<button class="ghost" id="pair-cli-copy" type="button">Copy</button></div></details></div></div></div><div class="card projects-card" id="anchor-team" data-account-section="projects"><div class="projects-top"><div><span class="eyebrow">Project memory</span><h2>Your projects and the people inside them</h2><p>Create a private memory pool, then invite only the people who should read, write, and search that project.</p></div><div class="projects-privacy">Keys stay on your machines</div></div><div class="invite-inbox" id="team-incoming" aria-live="polite"></div><div class="projects-layout" id="projects-layout"><aside class="project-rail" aria-label="Projects"><div class="project-rail-head"><span>Projects</span><button class="project-add" id="team-project-new" type="button">+ New project</button></div><ul class="project-list" id="team-project-list"></ul><p class="project-empty-rail" id="team-project-empty">Loading your projects…</p></aside><main class="project-main" id="team-project-detail"></main><aside class="team-drawer" id="team-invite-drawer" hidden><div class="drawer-head"><div><h3>Invite a teammate</h3><p id="team-drawer-project">Project access</p></div><button class="drawer-close" id="team-drawer-close" type="button" aria-label="Close invitation panel">×</button></div><label for="team-invite-email">Their SealKeep account email</label><input id="team-invite-email" type="email" autocomplete="email" placeholder="ann@company.com"><button class="primary" id="team-invite-send" type="button">Send invitation</button><p class="drawer-note">They receive access only after accepting. Your connected machine prepares their encrypted project key automatically.</p><p class="msg" id="team-invite-msg" role="status" aria-live="polite"></p></aside></div></div><dialog class="project-dialog" id="team-project-dialog"><form method="dialog" id="team-project-form"><h3>Create a project</h3><p>Name it now. It stays private until you invite someone.</p><label for="team-project-name">Project name</label><input id="team-project-name" maxlength="120" autocomplete="off" placeholder="Checkout platform"><p class="msg" id="team-project-msg" role="status" aria-live="polite"></p><div class="dialog-actions"><button class="ghost" value="cancel" type="button" id="team-project-cancel">Cancel</button><button class="primary" value="default" type="submit" id="team-project-create">Create project</button></div></form></dialog><dialog class="project-dialog local-task-dialog" id="local-task-dialog"><form method="dialog"><span class="eyebrow">On your machine</span><h3 id="local-task-title">Continue in the local SealKeep dashboard</h3><p id="local-task-copy"></p><div class="local-task-place"><strong id="local-task-place-title">Where to go</strong><p id="local-task-place-copy"></p></div><a id="local-task-open" class="primary" hidden target="_blank" rel="noopener noreferrer">Open this computer’s SealKeep</a><p style="margin-top:.8rem">If this account opened from your local dashboard, return to that tab—or use Back if it replaced the page. The browser already knows its exact port.</p><details class="local-task-fallback"><summary>No SealKeep window open?</summary><p>Run this on that computer. SealKeep reuses its running dashboard or opens the correct fallback port automatically.</p><div class="local-task-command"><code id="local-task-command">sealkeep ui</code><button class="ghost" id="local-task-copy-command" type="button">Copy</button></div></details><div class="dialog-actions"><button class="primary" value="close" type="submit">Done</button></div></form></dialog><div class="card" id="anchor-archives" data-account-section="storage" style="display:none"><span class="eyebrow">Archives</span><h2>What is stored</h2><p id="s-intro">Names live on your machines — your storage holds sealed archives it cannot read, listed here by reference. Deleting one removes the stored copy; if your machine already reclaimed the source, this is the last copy.</p><p class="msg" id="s-msg">Loading…</p><div id="s-wrap" style="display:none;overflow-x:auto"><table><thead><tr><th>Archive</th><th>Size</th><th>State</th><th>Added</th><th></th></tr></thead><tbody id="s-body"></tbody></table><p class="msg" id="s-foot"></p></div></div><div class="card" id="a-pack-card" data-account-section="account"><span id="anchor-plan"></span><span class="eyebrow">Your package</span><h2 id="a-pack-current">—</h2><p class="msg" id="a-pack-context" style="margin:0 0 .8rem"></p><div class="packrow" id="a-packs"></div><p class="empty" id="a-plan-free-note"></p><p class="empty" id="a-pack-alt">Not storing with us? Your own bucket is free with the same encryption, and Pro Lite keeps key escrow and machine sign-in with no metered storage.</p><div class="kv" id="a-sub-facts" style="margin-top:1rem"><div><span class="k">Subscription</span><b id="a-sub-state">—</b></div><div><span class="k">Renews</span><b id="a-sub-end">—</b></div><div><span class="k">Billed through</span><b id="a-sub-prov">—</b></div></div><p class="empty" id="a-sub-note"></p><div class="card-remedy" id="a-sub-actions" hidden><a class="primary" href="mailto:hello@spala.ai?subject=SealKeep%20subscription%20support">Fix subscription</a><span class="msg">This opens a message from the account email so support can restore uploads.</span></div><p class="why" id="a-pack-note">Changing your package is handled by hand today. Email hello@spala.ai from this address and we will move you within one business day — your archives keep uploading throughout.</p></div><div class="card" id="account-upload-rules" data-account-section="account"><span class="eyebrow">What gets uploaded</span><h2>Your upload rules</h2><p class="why" style="margin:.1rem 0 .8rem">Saved to your account — every machine you connect follows these, the same way routing rules do.</p><p>Everything you do not choose here stays local and is never uploaded.</p><p class="empty" id="r-note" style="margin-bottom:.9rem">Enforced on your machine, before a session is read or sealed. Anything held back is never uploaded, so this is a rule about what leaves — not a filter we apply after it arrives.</p><div class="rules"><label class="choice" data-on="yes"><input type="radio" name="sync" value="all" checked><span><span class="t">Every session</span><span class="d">Archive all of it. The safest choice, and the one that uses the most storage.</span></span></label><label class="choice" data-on="no"><input type="radio" name="sync" value="recent"><span><span class="t">Only the most recent</span><span class="d">Keep a rolling window. Older sessions stay local and are never uploaded.</span><input class="n" id="r-last" type="number" min="1" max="999" value="20" aria-label="How many recent sessions"><span class="d" style="display:inline">sessions</span></span></label><label class="choice" data-on="no"><input type="radio" name="sync" value="settled"><span><span class="t">Only once they have settled</span><span class="d">Wait before uploading, so work you are still iterating on stays put.</span><input class="n" id="r-age" type="number" min="1" max="365" value="7" aria-label="Days before a session is uploaded"><span class="d" style="display:inline">days old</span></span></label><button class="primary" id="r-save" type="button" style="margin-top:1.1rem">Save rules</button><span class="saved" id="r-msg" role="status" aria-live="polite"></span></div></div><div class="card" id="account-activity" data-account-section="account"><span class="eyebrow">Activity</span><h2>What happened lately</h2><p>Every change made to this account, newest first.</p><ul class="lines" id="v-list"></ul><p class="empty" id="v-msg">Loading…</p></div><div class="card" id="account-profile" data-account-section="account"><span class="eyebrow">Account</span><h2>Name and email</h2><label for="a-name">Account name</label><div class="inline"><input id="a-name" maxlength="60" autocomplete="off"><button class="ghost" id="a-rename" type="button">Save name</button><span class="saved" id="a-rename-msg" role="status" aria-live="polite"></span></div><!-- The setup commands used to be printed here as well as in the
|
|
38
|
+
first-run card. There is now one place to connect a machine, and it
|
|
39
|
+
is the pairing block above. --><p class="empty" style="margin-top:1.4rem">Your keys are made on your machine, so this page holds nothing that opens an archive. Signing out here does not stop a connected machine from archiving.</p><button class="ghost" id="a-out" style="margin-top:1rem">Sign out</button></div></div></div></section><footer><div class="wrap" style="display:flex;gap:1.5rem;flex-wrap:wrap;align-items:center"><span class="logo">⌑ Sealkeep</span><code style="font-size:.78rem;color:var(--soft)">npm install -g sealkeep</code><span style="flex:1"></span><span class="r" style="margin:0">unaudited encryption format · stated plainly</span></div><div class="wrap" style="padding-top:1rem"><p style="font-size:.9rem;line-height:1.6;color:var(--soft)">Sealkeep is a product of HM Software LLC, the company behind Spala AI.</p><nav aria-label="Company" style="display:flex;gap:1.25rem;flex-wrap:wrap;margin-top:.75rem;font-size:.85rem"><a href="https://spala.ai/about/">About the company</a><a href="https://spala.ai/terms/">Company terms</a><a href="https://spala.ai/privacy/">Company privacy policy</a></nav></div></footer><script>
|
|
40
|
+
if (location.hostname === "shared.spala.ai" && location.pathname.startsWith("/p04946") && !location.pathname.startsWith('/p04946/api/__internal/frontend-hosting/uploads/')) {
|
|
41
|
+
location.replace(`https://sealkeep.spala.ai/${location.search}${location.hash}`);
|
|
42
|
+
}
|
|
43
|
+
const $ = (id) => document.getElementById(id);
|
|
44
|
+
const API = location.hostname === "shared.spala.ai"
|
|
45
|
+
? "/p04946/api"
|
|
46
|
+
: "https://shared.spala.ai/p04946/api";
|
|
47
|
+
const TOKEN = "sealkeep.token";
|
|
48
|
+
const OWNER_KEY = "sealkeep.ownerKey";
|
|
49
|
+
const CONNECT_REQUEST = "sealkeep.connectRequest";
|
|
50
|
+
const connectHash = location.hash.startsWith("#connect=")
|
|
51
|
+
? decodeURIComponent(location.hash.slice("#connect=".length))
|
|
52
|
+
: "";
|
|
53
|
+
if (/^[A-Za-z0-9._~-]{1,200}$/.test(connectHash)) sessionStorage.setItem(CONNECT_REQUEST, connectHash);
|
|
54
|
+
const pendingConnectRequest = () => sessionStorage.getItem(CONNECT_REQUEST) || "";
|
|
55
|
+
const keepOwnerKey = (key) => { if (key) sessionStorage.setItem(OWNER_KEY, key); };
|
|
56
|
+
const motionOK = () => !matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
57
|
+
(function heroStories() {
|
|
58
|
+
const frame = $("hero-story");
|
|
59
|
+
const title = $("hero-story-title");
|
|
60
|
+
const indexLabel = $("hero-story-index");
|
|
61
|
+
const controls = [...document.querySelectorAll(".hero-demo-progress button")];
|
|
62
|
+
if (!frame || !title || !indexLabel) return;
|
|
63
|
+
const stories = [
|
|
64
|
+
{ mode: "memory", title: "Preserve your agent memories", hash: "#agent-memory" },
|
|
65
|
+
{ mode: "continue", title: "Continue your work anywhere", hash: "#continue-anywhere" },
|
|
66
|
+
{ mode: "team", title: "Team cooperation", hash: "#team-cooperation" }
|
|
67
|
+
];
|
|
68
|
+
let current = 0;
|
|
69
|
+
let load = 0;
|
|
70
|
+
const fit = () => {
|
|
71
|
+
const available = frame.parentElement.clientWidth;
|
|
72
|
+
if (available <= 760) {
|
|
73
|
+
const height = stories[current].mode === "team" ? 1280 : 1020;
|
|
74
|
+
frame.style.width = "100%";
|
|
75
|
+
frame.style.height = `${height}px`;
|
|
76
|
+
frame.style.transform = "none";
|
|
77
|
+
frame.parentElement.style.height = `${height}px`;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
frame.style.width = "100%";
|
|
81
|
+
frame.style.height = "560px";
|
|
82
|
+
frame.style.transform = "none";
|
|
83
|
+
frame.parentElement.style.height = "560px";
|
|
84
|
+
};
|
|
85
|
+
const show = (next) => {
|
|
86
|
+
current = (next + stories.length) % stories.length;
|
|
87
|
+
const story = stories[current];
|
|
88
|
+
title.textContent = story.title;
|
|
89
|
+
indexLabel.textContent = `${String(current + 1).padStart(2, "0")} / 03`;
|
|
90
|
+
frame.title = story.title;
|
|
91
|
+
controls.forEach((button, index) => button.setAttribute("aria-current", String(index === current)));
|
|
92
|
+
frame.src = `visual/index.html?embed=1&cycle=${++load}${story.hash}`;
|
|
93
|
+
fit();
|
|
94
|
+
};
|
|
95
|
+
controls.forEach((button, index) => button.addEventListener("click", () => show(index)));
|
|
96
|
+
new ResizeObserver(fit).observe(frame.parentElement);
|
|
97
|
+
fit();
|
|
98
|
+
window.addEventListener("message", (event) => {
|
|
99
|
+
if (event.source !== frame.contentWindow || event.data?.type !== "sealkeep-story-complete"
|
|
100
|
+
|| event.data.mode !== stories[current].mode) return;
|
|
101
|
+
show(current + 1);
|
|
102
|
+
});
|
|
103
|
+
})();
|
|
104
|
+
const params = new URLSearchParams(location.search);
|
|
105
|
+
const authCode = params.get("code");
|
|
106
|
+
const authStates = params.getAll("state");
|
|
107
|
+
const authState = authStates.length === 0 ? null : authStates.length === 1 ? authStates[0] : "";
|
|
108
|
+
const confirmToken = params.get("confirm");
|
|
109
|
+
const confirmAccount = params.get("account");
|
|
110
|
+
const PROJECT_DRAFT_NAME = "sealkeep.projectDraftName";
|
|
111
|
+
const GOOGLE_OAUTH_STATE = "sealkeep.googleOauthState";
|
|
112
|
+
const linkedProjectDraft = (params.get("newProject") || "").trim().slice(0, 120);
|
|
113
|
+
if (linkedProjectDraft) sessionStorage.setItem(PROJECT_DRAFT_NAME, linkedProjectDraft);
|
|
114
|
+
const savedToken = sessionStorage.getItem(TOKEN);
|
|
115
|
+
const returning = Boolean(authCode) || sessionStorage.getItem("sealkeep.returning") === "1";
|
|
116
|
+
const resuming = Boolean(authCode || (confirmToken && confirmAccount) || savedToken);
|
|
117
|
+
if (resuming) {
|
|
118
|
+
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
|
|
119
|
+
document.body.classList.add("booting");
|
|
120
|
+
}
|
|
121
|
+
const bootSay = (text) => { $("boot-note").textContent = text; };
|
|
122
|
+
function enterPanel() {
|
|
123
|
+
window.scrollTo({ top: 0, behavior: "instant" });
|
|
124
|
+
$("a-strip").focus({ preventScroll: true });
|
|
125
|
+
}
|
|
126
|
+
function bootExit() {
|
|
127
|
+
document.body.classList.remove("booting");
|
|
128
|
+
if (document.body.classList.contains("panel")) enterPanel();
|
|
129
|
+
}
|
|
130
|
+
function bootDone(confirmed) {
|
|
131
|
+
if (!document.body.classList.contains("booting")) return Promise.resolve();
|
|
132
|
+
if (!confirmed) { bootExit(); return Promise.resolve(); }
|
|
133
|
+
bootSay("Signed in");
|
|
134
|
+
$("boot-track").dataset.done = "yes";
|
|
135
|
+
return new Promise((finished) => setTimeout(() => { bootExit(); finished(); }, motionOK() ? 460 : 0));
|
|
136
|
+
}
|
|
137
|
+
(function seal() {
|
|
138
|
+
const el = $("cipher");
|
|
139
|
+
const glyphs = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789+/";
|
|
140
|
+
const target = "vGx8Kq2mNbZ4tRw9YpLd3cHfA7sJeU1oQiT6vXn0MyBkR5aWzDgP4uCjE8hSlF2N"
|
|
141
|
+
+ "kR9wQ7pAzXm3TbYvL6dHcJ1fUsN4oiGeMy0BW5aKgPjD8uCE2hSlFqT6vXn0Myx";
|
|
142
|
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) { el.textContent = target; return; }
|
|
143
|
+
let frame = 0;
|
|
144
|
+
const total = 26;
|
|
145
|
+
const tick = () => {
|
|
146
|
+
const settled = Math.floor((frame / total) * target.length);
|
|
147
|
+
let out = target.slice(0, settled);
|
|
148
|
+
for (let i = settled; i < target.length; i++) out += glyphs[(Math.random() * glyphs.length) | 0];
|
|
149
|
+
el.textContent = out;
|
|
150
|
+
if (frame++ < total) requestAnimationFrame(tick);
|
|
151
|
+
else el.textContent = target;
|
|
152
|
+
};
|
|
153
|
+
requestAnimationFrame(tick);
|
|
154
|
+
})();
|
|
155
|
+
function copies(buttonId, read) {
|
|
156
|
+
const button = $(buttonId);
|
|
157
|
+
button.addEventListener("click", async () => {
|
|
158
|
+
const label = button.dataset.label || button.textContent;
|
|
159
|
+
button.dataset.label = label;
|
|
160
|
+
try { await navigator.clipboard.writeText(read()); button.textContent = "Copied"; }
|
|
161
|
+
catch { button.textContent = "Copy failed"; }
|
|
162
|
+
setTimeout(() => { button.textContent = label; }, 1600);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
copies("copy", () => $("cmd").textContent.trim());
|
|
166
|
+
copies("pair-copy", () => $("pair-code").textContent.trim());
|
|
167
|
+
copies("pair-cli-copy", () => $("pair-cli").textContent.trim());
|
|
168
|
+
copies("local-task-copy-command", () => $("local-task-command").textContent.trim());
|
|
169
|
+
function localTaskContent(kind, subject = "") {
|
|
170
|
+
if (kind === "recovery") return {
|
|
171
|
+
title: "Create the Recovery Kit locally",
|
|
172
|
+
copy: "Use a computer that already holds this vault key. This account page cannot receive the key or discover another computer's private dashboard address. If you opened it from your local dashboard, the button below returns there.",
|
|
173
|
+
place: "In the local SealKeep dashboard, open Settings. Under Backup Recovery Kit, choose Create Recovery Kit."
|
|
174
|
+
};
|
|
175
|
+
if (kind === "folders") return {
|
|
176
|
+
title: subject ? `Choose folders for ${subject}` : "Choose this project's folders",
|
|
177
|
+
copy: "Folder paths stay on that computer. They are not sent to this account page, and each teammate chooses their own paths.",
|
|
178
|
+
place: `In the local SealKeep dashboard, open Shared. Under Connected project memory, find ${subject || "the project"}, choose one or more work folders, then Save folder mapping.`
|
|
179
|
+
};
|
|
180
|
+
if (kind === "project-routing") return {
|
|
181
|
+
title: subject ? `Choose projects for ${subject}` : "Choose projects for this destination",
|
|
182
|
+
copy: "Project routing uses the stable identity detected from each checkout—not a typed display name. Those identities stay on your SealKeep machines, so this account page cannot safely create or change a project pin.",
|
|
183
|
+
place: `In the local SealKeep dashboard, open Settings. Under Storage, find ${subject || "the destination"}, choose + pin project, then select the detected project. SealKeep saves the stable project identity to your account so every machine follows the same route.`
|
|
184
|
+
};
|
|
185
|
+
if (kind === "project") return {
|
|
186
|
+
title: "Let a SealKeep machine finish the project",
|
|
187
|
+
copy: "The project name is already queued. An owner machine that holds this vault key creates and encrypts the project key automatically; the cloud cannot create or open it.",
|
|
188
|
+
place: "Leave SealKeep running on an owner's computer. You can close this page—the project appears here after that machine checks in."
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
title: subject ? `Connect ${subject} locally` : "Connect storage locally",
|
|
192
|
+
copy: "Provider credentials stay on each computer and are never entered into this account page.",
|
|
193
|
+
place: `In the local SealKeep dashboard, open Settings, find ${subject || "the destination"}, then connect or test it on that machine.`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function rememberLocalDashboardReturn() {
|
|
197
|
+
try {
|
|
198
|
+
const incoming = new URL(location.href).searchParams.get('local_return');
|
|
199
|
+
const remembered = incoming || sessionStorage.getItem('sealkeep.localReturn');
|
|
200
|
+
if (!remembered) return null;
|
|
201
|
+
const local = new URL(remembered);
|
|
202
|
+
if (local.protocol !== 'http:' || !['127.0.0.1','localhost','[::1]'].includes(local.hostname) || local.username || local.password || !['/','/index.html'].includes(local.pathname) || local.search || local.hash) return null;
|
|
203
|
+
sessionStorage.setItem('sealkeep.localReturn', local.href);
|
|
204
|
+
return local;
|
|
205
|
+
} catch { return null; }
|
|
206
|
+
}
|
|
207
|
+
rememberLocalDashboardReturn();
|
|
208
|
+
function openLocalTask(kind, subject = "", projectKey = "") {
|
|
209
|
+
const task = localTaskContent(kind, subject);
|
|
210
|
+
$("local-task-title").textContent = task.title;
|
|
211
|
+
$("local-task-copy").textContent = (/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) ? "Continue on a connected computer. " : "") + task.copy;
|
|
212
|
+
$("local-task-place-copy").textContent = task.place;
|
|
213
|
+
const localLink = $("local-task-open");
|
|
214
|
+
localLink.hidden = true;
|
|
215
|
+
const local = rememberLocalDashboardReturn();
|
|
216
|
+
if (local) {
|
|
217
|
+
local.hash = kind === 'recovery' ? 'recovery-kit' : ['folders','project'].includes(kind) ? 'shared' : 'storage-targets';
|
|
218
|
+
if (kind === 'folders' && projectKey) local.hash = `shared?${new URLSearchParams({ project: projectKey })}`;
|
|
219
|
+
localLink.href = local.href; localLink.hidden = false;
|
|
220
|
+
}
|
|
221
|
+
$("local-task-dialog").showModal();
|
|
222
|
+
}
|
|
223
|
+
$("e-fix").addEventListener("click", () => openLocalTask("recovery"));
|
|
224
|
+
$("a-quota-fix").addEventListener("click", () => showAccountView("account"));
|
|
225
|
+
const startTabs = { free: [$("t-free"), $("p-free")], paid: [$("t-paid"), $("p-paid")] };
|
|
226
|
+
for (const [key, [tab]] of Object.entries(startTabs))
|
|
227
|
+
tab.addEventListener("click", () => {
|
|
228
|
+
for (const [other, [b, pane]] of Object.entries(startTabs)) {
|
|
229
|
+
b.setAttribute("aria-selected", String(other === key));
|
|
230
|
+
pane.classList.toggle("on", other === key);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
const say = (el, text, cls) => { const n = $(el); n.textContent = text; n.className = "msg " + (cls || ""); };
|
|
234
|
+
const errText = (error) => {
|
|
235
|
+
let text = String((error && error.message) || error || "Something went wrong");
|
|
236
|
+
text = text.replace(/^\s*Error at step\s+[^:]{0,60}:\s*/i, "");
|
|
237
|
+
text = text.replace(/^\s*(Error|Exception|Failure)\s*:\s*/i, "");
|
|
238
|
+
if (text) text = text.charAt(0).toUpperCase() + text.slice(1);
|
|
239
|
+
return text.slice(0, 200);
|
|
240
|
+
};
|
|
241
|
+
const bytes = (n) => {
|
|
242
|
+
n = Number(n) || 0;
|
|
243
|
+
for (const [u, d] of [["GB", 1024 ** 3], ["MB", 1024 ** 2], ["KB", 1024]])
|
|
244
|
+
if (n >= d) return (n / d).toFixed(1) + " " + u;
|
|
245
|
+
return n + " B";
|
|
246
|
+
};
|
|
247
|
+
const ago = (iso) => {
|
|
248
|
+
const s = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
249
|
+
if (!isFinite(s)) return "—";
|
|
250
|
+
for (const [n, u] of [[86400 * 30, "mo"], [86400, "d"], [3600, "h"], [60, "m"]])
|
|
251
|
+
if (s >= n) return Math.floor(s / n) + u + " ago";
|
|
252
|
+
return "just now";
|
|
253
|
+
};
|
|
254
|
+
async function api(path, { method = "GET", body, token } = {}) {
|
|
255
|
+
const headers = {};
|
|
256
|
+
if (body) headers["content-type"] = "application/json";
|
|
257
|
+
if (token) headers.authorization = "Bearer " + token;
|
|
258
|
+
let res;
|
|
259
|
+
try {
|
|
260
|
+
res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error("No answer from the service. Check your connection and try again.");
|
|
263
|
+
}
|
|
264
|
+
let data = null;
|
|
265
|
+
let parsed = true;
|
|
266
|
+
try { data = await res.json(); } catch { parsed = false; }
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
const message = parsed
|
|
269
|
+
? String(data?.error || `${res.status} ${res.statusText}`)
|
|
270
|
+
: `The service did not answer (HTTP ${res.status}). Try again in a minute.`;
|
|
271
|
+
const error = new Error(message.slice(0, 200));
|
|
272
|
+
error.status = res.status;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
function showPane(which) {
|
|
278
|
+
const wasPanel = document.body.classList.contains("panel");
|
|
279
|
+
for (const [id, on] of [["p-signup", which === "signup"], ["p-signin", which === "signin"], ["p-account", which === "account"]])
|
|
280
|
+
$(id).classList.toggle("on", on);
|
|
281
|
+
document.body.classList.toggle("panel", which === "account");
|
|
282
|
+
$("auth-tabs").style.display = which === "account" ? "none" : "";
|
|
283
|
+
for (const [b, key] of [["t-signup", "signup"], ["t-signin", "signin"]])
|
|
284
|
+
$(b).setAttribute("aria-selected", String(which === key));
|
|
285
|
+
if (which === "account" && !wasPanel && !document.body.classList.contains("booting")) enterPanel();
|
|
286
|
+
}
|
|
287
|
+
async function loadDeviceConnect(token) {
|
|
288
|
+
const requestId = pendingConnectRequest();
|
|
289
|
+
if (!requestId) return;
|
|
290
|
+
on("connect-auth-intro", false);
|
|
291
|
+
on("device-connect", true);
|
|
292
|
+
on("device-connect-actions", false);
|
|
293
|
+
$("device-connect-title").textContent = "Review this request";
|
|
294
|
+
$("device-connect-copy").textContent = "Loading the machine details…";
|
|
295
|
+
$("device-connect-msg").textContent = "";
|
|
296
|
+
try {
|
|
297
|
+
const request = await api(`/v1/cloud/device-authorizations/${encodeURIComponent(requestId)}`, { token });
|
|
298
|
+
const agents = Array.isArray(request.agents) ? request.agents.filter((value) => typeof value === "string") : [];
|
|
299
|
+
const os = { macos: "macOS", linux: "Linux", windows: "Windows" }[request.os_family] || "device";
|
|
300
|
+
$("device-connect-title").textContent = request.label || "Unnamed machine";
|
|
301
|
+
$("device-connect-copy").textContent = `${os}${agents.length ? " · " + agents.join(" + ") : ""} wants to connect to this Sealkeep account.`;
|
|
302
|
+
if (request.state === "pending") {
|
|
303
|
+
on("device-connect-actions", true);
|
|
304
|
+
const expiry = request.expires_at && Number.isFinite(Date.parse(request.expires_at))
|
|
305
|
+
? new Date(request.expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
306
|
+
: "";
|
|
307
|
+
$("device-connect-msg").textContent = expiry ? `Waiting for your decision · expires at ${expiry}` : "Waiting for your decision";
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
sessionStorage.removeItem(CONNECT_REQUEST);
|
|
311
|
+
history.replaceState(null, "", location.pathname + location.search + "#account");
|
|
312
|
+
if (request.state === "approved") $("device-connect-msg").textContent = "Approved. The machine is finishing securely now.";
|
|
313
|
+
else if (request.state === "claimed") $("device-connect-msg").textContent = "Connected. You can close this page.";
|
|
314
|
+
else if (request.state === "denied") $("device-connect-msg").textContent = "Denied. This machine received no account access.";
|
|
315
|
+
else $("device-connect-msg").textContent = "This request expired. Start again from the machine.";
|
|
316
|
+
} catch (error) {
|
|
317
|
+
$("device-connect-copy").textContent = "This request could not be opened.";
|
|
318
|
+
$("device-connect-msg").textContent = errText(error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function decideDeviceConnect(decision) {
|
|
322
|
+
const requestId = pendingConnectRequest();
|
|
323
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
324
|
+
if (!requestId || !token) return;
|
|
325
|
+
const approve = $("device-connect-approve"), deny = $("device-connect-deny");
|
|
326
|
+
approve.disabled = true; deny.disabled = true;
|
|
327
|
+
$("device-connect-msg").textContent = decision === "approve" ? "Approving…" : "Denying…";
|
|
328
|
+
try {
|
|
329
|
+
await api(`/v1/cloud/device-authorizations/${encodeURIComponent(requestId)}/${decision}`, { method: "POST", token });
|
|
330
|
+
on("device-connect-actions", false);
|
|
331
|
+
sessionStorage.removeItem(CONNECT_REQUEST);
|
|
332
|
+
history.replaceState(null, "", location.pathname + location.search + "#account");
|
|
333
|
+
$("device-connect-msg").textContent = decision === "approve"
|
|
334
|
+
? "Approved. The machine is finishing securely now."
|
|
335
|
+
: "Denied. This machine received no account access.";
|
|
336
|
+
} catch (error) {
|
|
337
|
+
$("device-connect-msg").textContent = errText(error);
|
|
338
|
+
approve.disabled = false; deny.disabled = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
$("device-connect-approve").addEventListener("click", () => decideDeviceConnect("approve"));
|
|
342
|
+
$("device-connect-deny").addEventListener("click", () => decideDeviceConnect("deny"));
|
|
343
|
+
function goTo(el) {
|
|
344
|
+
if (!el) return;
|
|
345
|
+
const start = window.scrollY;
|
|
346
|
+
const nav = document.querySelector(".acct-nav");
|
|
347
|
+
const clearance = (nav ? nav.getBoundingClientRect().height : 0) + 12;
|
|
348
|
+
const target = Math.max(0, el.getBoundingClientRect().top + start - clearance);
|
|
349
|
+
const smooth = motionOK();
|
|
350
|
+
window.scrollTo({ top: target, behavior: smooth ? "smooth" : "instant" });
|
|
351
|
+
if (!smooth) return;
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
if (Math.abs(window.scrollY - start) < 4 && Math.abs(target - start) > 24) {
|
|
354
|
+
window.scrollTo({ top: target, behavior: "instant" });
|
|
355
|
+
}
|
|
356
|
+
}, 320);
|
|
357
|
+
}
|
|
358
|
+
const ACCOUNT_VIEWS = new Set(["overview", "projects", "storage", "machines", "account"]);
|
|
359
|
+
let activeAccountView = "overview";
|
|
360
|
+
const requestedAccountView = () => {
|
|
361
|
+
const wanted = new URLSearchParams(location.search).get("view") || sessionStorage.getItem("sealkeep.returnView") || "overview";
|
|
362
|
+
return ACCOUNT_VIEWS.has(wanted) ? wanted : "overview";
|
|
363
|
+
};
|
|
364
|
+
function showAccountView(view, { writeHistory = true } = {}) {
|
|
365
|
+
const next = ACCOUNT_VIEWS.has(view) ? view : "overview";
|
|
366
|
+
activeAccountView = next;
|
|
367
|
+
for (const section of document.querySelectorAll("[data-account-section]")) {
|
|
368
|
+
section.hidden = !section.dataset.accountSection.split(/\s+/).includes(next);
|
|
369
|
+
}
|
|
370
|
+
for (const link of document.querySelectorAll(".acct-nav [data-account-view]")) {
|
|
371
|
+
link.setAttribute("aria-current", link.dataset.accountView === next ? "page" : "false");
|
|
372
|
+
}
|
|
373
|
+
if (writeHistory) history.replaceState(null, "", `${location.pathname}?view=${encodeURIComponent(next)}#account`);
|
|
374
|
+
window.scrollTo({ top: 0, behavior: "instant" });
|
|
375
|
+
}
|
|
376
|
+
for (const link of document.querySelectorAll(".acct-nav [data-account-view]")) {
|
|
377
|
+
link.addEventListener("click", (event) => {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
showAccountView(link.dataset.accountView);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function goAccount() {
|
|
383
|
+
if (document.body.classList.contains("panel")) { window.scrollTo(0, 0); return; }
|
|
384
|
+
$("account").scrollIntoView({ behavior: motionOK() ? "smooth" : "auto", block: "start" });
|
|
385
|
+
}
|
|
386
|
+
for (const link of document.querySelectorAll('a[href="#account"]'))
|
|
387
|
+
link.addEventListener("click", (event) => {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
if (location.hash !== "#account") location.hash = "account";
|
|
390
|
+
else goAccount();
|
|
391
|
+
});
|
|
392
|
+
addEventListener("hashchange", () => { if (location.hash === "#account") goAccount(); });
|
|
393
|
+
const on = (id, show) => { $(id).style.display = show ? "" : "none"; };
|
|
394
|
+
const day = (iso) => {
|
|
395
|
+
const d = new Date(iso);
|
|
396
|
+
return isFinite(d.getTime())
|
|
397
|
+
? d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
|
|
398
|
+
: "—";
|
|
399
|
+
};
|
|
400
|
+
function renderAccount(user) {
|
|
401
|
+
const paid = user.plan === "pro" || user.plan === "team";
|
|
402
|
+
$("a-label").textContent = user.label || "Your vault";
|
|
403
|
+
$("a-name").value = user.label || "";
|
|
404
|
+
const chipPack = PACKAGES.find((p) => p.id === packageIdFor(user));
|
|
405
|
+
$("a-plan").textContent = chipPack ? chipPack.name : "Custom";
|
|
406
|
+
$("a-plan").className = "chip" + (paid || user.plan === "pro_lite" ? " pro" : "");
|
|
407
|
+
$("a-plan").onclick = () => showAccountView("account");
|
|
408
|
+
$("a-who").textContent = [
|
|
409
|
+
user.email,
|
|
410
|
+
"joined " + day(user.created_at),
|
|
411
|
+
user.auth_provider === "google" ? "signed in with Google" : null,
|
|
412
|
+
user.state === "suspended" ? "SUSPENDED" : null
|
|
413
|
+
].filter(Boolean).join(" · ");
|
|
414
|
+
on("a-verify", !user.email_verified_at);
|
|
415
|
+
lastPaid = paid;
|
|
416
|
+
applyStorageView();
|
|
417
|
+
on("anchor-archives", paid);
|
|
418
|
+
$("a-plan-free-note").textContent = paid
|
|
419
|
+
? ""
|
|
420
|
+
: "You are on your own storage: unlimited archives, same encryption, your provider's bill. Managed storage means we run the bucket and meter what you store — we still hold no key that opens any of it.";
|
|
421
|
+
on("a-sub-facts", paid);
|
|
422
|
+
on("a-plan-free-note", !paid);
|
|
423
|
+
if (paid) {
|
|
424
|
+
const quota = Number(user.quota_bytes) || 0, used = Number(user.used_bytes) || 0;
|
|
425
|
+
$("a-used").textContent = bytes(used);
|
|
426
|
+
$("a-quota-b").textContent = quota ? bytes(quota) : "—";
|
|
427
|
+
$("a-bar").style.width = quota ? Math.min(100, (used / quota) * 100) + "%" : "0%";
|
|
428
|
+
$("a-quota").textContent = quota
|
|
429
|
+
? bytes(used) + " of " + bytes(quota) + " used" + (used >= quota * 0.9 ? " — close to the limit" : "")
|
|
430
|
+
: "";
|
|
431
|
+
$("a-quota-actions").hidden = !(quota && used >= quota * 0.9);
|
|
432
|
+
const sub = user.subscription;
|
|
433
|
+
$("a-sub-state").textContent = sub ? sub.state : "not recorded";
|
|
434
|
+
$("a-sub-end").textContent = sub && sub.current_period_end ? day(sub.current_period_end) : "—";
|
|
435
|
+
$("a-sub-prov").textContent = sub && sub.provider ? sub.provider : "—";
|
|
436
|
+
$("a-sub-note").textContent = sub && sub.state === "active"
|
|
437
|
+
? "To change or cancel, write to hello@spala.ai from this address."
|
|
438
|
+
: "We cannot see an active subscription for this account, so uploads are paused. This is ours to fix. Email hello@spala.ai from this address and we will sort it out — nothing already stored is affected, and your machine keeps sealing sessions locally in the meantime.";
|
|
439
|
+
$("a-sub-actions").hidden = Boolean(sub && sub.state === "active");
|
|
440
|
+
} else {
|
|
441
|
+
$("a-quota-actions").hidden = true;
|
|
442
|
+
$("a-sub-actions").hidden = true;
|
|
443
|
+
}
|
|
444
|
+
rulesFrom(user);
|
|
445
|
+
packsFrom(user);
|
|
446
|
+
showPane("account");
|
|
447
|
+
}
|
|
448
|
+
const ACTIONS = {
|
|
449
|
+
"sync_rules.update": "Changed what leaves your machine",
|
|
450
|
+
"device.revoke": "Revoked a machine",
|
|
451
|
+
"email.confirmation_sent": "Sent an email confirmation link",
|
|
452
|
+
"email.confirmed": "Confirmed the email address",
|
|
453
|
+
"auth.google": "Signed in with Google"
|
|
454
|
+
};
|
|
455
|
+
async function loadDevices(token) {
|
|
456
|
+
const list = $("d-list");
|
|
457
|
+
let deviceCount = 0;
|
|
458
|
+
list.replaceChildren();
|
|
459
|
+
try {
|
|
460
|
+
const { devices } = await api("/v1/cloud/devices", { token });
|
|
461
|
+
deviceCount = devices.length;
|
|
462
|
+
if (teamState.token !== token) return deviceCount;
|
|
463
|
+
const active = devices.filter((d) => d.state !== "revoked");
|
|
464
|
+
teamState.machineCount = active.length;
|
|
465
|
+
teamState.machinesLoaded = true;
|
|
466
|
+
if (teamState.loaded) renderTeamProjects();
|
|
467
|
+
if (!devices.length) {
|
|
468
|
+
$("d-msg").textContent = "No machines connected yet.";
|
|
469
|
+
return deviceCount;
|
|
470
|
+
}
|
|
471
|
+
$("d-msg").textContent = "";
|
|
472
|
+
const retired = devices.filter((d) => d.state === "revoked");
|
|
473
|
+
const foldHost = $("d-retired");
|
|
474
|
+
foldHost.replaceChildren();
|
|
475
|
+
if (retired.length) {
|
|
476
|
+
const fold = document.createElement("details");
|
|
477
|
+
const summary = document.createElement("summary");
|
|
478
|
+
summary.textContent = `${retired.length} revoked machine${retired.length === 1 ? "" : "s"} — kept as the record; existing archives untouched, no new leases`;
|
|
479
|
+
const inner = document.createElement("ul");
|
|
480
|
+
inner.className = "lines";
|
|
481
|
+
for (const d of retired) {
|
|
482
|
+
const li = document.createElement("li");
|
|
483
|
+
const name = document.createElement("span");
|
|
484
|
+
name.className = "grow";
|
|
485
|
+
name.textContent = d.label || "Unnamed machine";
|
|
486
|
+
const when = document.createElement("span");
|
|
487
|
+
when.className = "when";
|
|
488
|
+
when.textContent = "revoked " + ago(d.revoked_at);
|
|
489
|
+
li.append(name, when);
|
|
490
|
+
inner.append(li);
|
|
491
|
+
}
|
|
492
|
+
fold.append(summary, inner);
|
|
493
|
+
foldHost.append(fold);
|
|
494
|
+
}
|
|
495
|
+
for (const d of active) {
|
|
496
|
+
const li = document.createElement("li");
|
|
497
|
+
const name = document.createElement("span");
|
|
498
|
+
name.className = "grow";
|
|
499
|
+
name.textContent = d.label || "Unnamed machine";
|
|
500
|
+
const state = document.createElement("span");
|
|
501
|
+
const revoked = d.state === "revoked";
|
|
502
|
+
state.className = "state " + (revoked ? "pending" : "durable");
|
|
503
|
+
state.textContent = revoked ? "revoked" : "active";
|
|
504
|
+
const seen = document.createElement("span");
|
|
505
|
+
seen.className = "when";
|
|
506
|
+
seen.textContent = revoked ? "revoked " + ago(d.revoked_at) : "enrolled " + ago(d.created_at);
|
|
507
|
+
li.append(name, state, seen);
|
|
508
|
+
if (!revoked) {
|
|
509
|
+
const btn = document.createElement("button");
|
|
510
|
+
btn.className = "del"; btn.type = "button"; btn.textContent = "Revoke";
|
|
511
|
+
btn.addEventListener("click", async () => {
|
|
512
|
+
if (btn.dataset.armed !== "yes") {
|
|
513
|
+
btn.dataset.armed = "yes";
|
|
514
|
+
btn.textContent = "Revoke this machine?";
|
|
515
|
+
btn.title = "It blocks new leases; it does not reach into your bucket. Existing archives are untouched. This machine loses its right to delete now, and its sign-in lapses within 7 days — it cannot renew one without you.";
|
|
516
|
+
setTimeout(() => {
|
|
517
|
+
if (btn.dataset.armed === "yes") { btn.dataset.armed = "no"; btn.textContent = "Revoke"; }
|
|
518
|
+
}, 4000);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
btn.disabled = true; btn.textContent = "Revoking";
|
|
522
|
+
try {
|
|
523
|
+
await api("/v1/cloud/devices/" + encodeURIComponent(d.id) + "/revoke", { method: "POST", token });
|
|
524
|
+
await loadDevices(token);
|
|
525
|
+
await loadAudit(token);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
$("d-msg").textContent = errText(error);
|
|
528
|
+
btn.disabled = false; btn.textContent = "Revoke";
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
li.append(btn);
|
|
532
|
+
}
|
|
533
|
+
list.append(li);
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (teamState.token !== token) return deviceCount;
|
|
537
|
+
teamState.machinesLoaded = false;
|
|
538
|
+
if (teamState.loaded) renderTeamProjects();
|
|
539
|
+
$("d-msg").textContent = errText(error);
|
|
540
|
+
}
|
|
541
|
+
return deviceCount;
|
|
542
|
+
}
|
|
543
|
+
async function loadAudit(token) {
|
|
544
|
+
const list = $("v-list");
|
|
545
|
+
list.replaceChildren();
|
|
546
|
+
try {
|
|
547
|
+
const { events } = await api("/v1/cloud/audit", { token });
|
|
548
|
+
if (!events.length) { $("v-msg").textContent = "Nothing has changed on this account yet."; return; }
|
|
549
|
+
$("v-msg").textContent = "";
|
|
550
|
+
for (const e of events) {
|
|
551
|
+
const li = document.createElement("li");
|
|
552
|
+
const what = document.createElement("span");
|
|
553
|
+
what.className = "grow";
|
|
554
|
+
what.textContent = ACTIONS[e.action] || e.action;
|
|
555
|
+
const when = document.createElement("span");
|
|
556
|
+
when.className = "when";
|
|
557
|
+
when.textContent = ago(e.at);
|
|
558
|
+
li.append(what, when);
|
|
559
|
+
if (e.outcome === "denied") {
|
|
560
|
+
const bad = document.createElement("span");
|
|
561
|
+
bad.className = "state pending";
|
|
562
|
+
bad.textContent = "did not complete";
|
|
563
|
+
li.append(bad);
|
|
564
|
+
}
|
|
565
|
+
list.append(li);
|
|
566
|
+
}
|
|
567
|
+
} catch (error) { $("v-msg").textContent = errText(error); }
|
|
568
|
+
}
|
|
569
|
+
let lastEscrowToken = "", lastEscrowAtRisk = false, escrowLoadGeneration = 0;
|
|
570
|
+
const recoveryRemedy = (state) => {
|
|
571
|
+
const actionable = state !== "held";
|
|
572
|
+
$("e-actions").hidden = !actionable;
|
|
573
|
+
$("e-fix").hidden = state === "error";
|
|
574
|
+
$("e-retry").hidden = state !== "error";
|
|
575
|
+
$("e-action-msg").textContent = state === "error"
|
|
576
|
+
? "Retry the cloud status check. Nothing on your machine will be changed."
|
|
577
|
+
: "The key stays local; this shows the exact steps for the machine that holds it.";
|
|
578
|
+
};
|
|
579
|
+
$("e-retry").addEventListener("click", () => loadEscrow(lastEscrowToken, lastEscrowAtRisk));
|
|
580
|
+
async function loadEscrow(token, atRisk) {
|
|
581
|
+
if (!token || sessionStorage.getItem(TOKEN) !== token) return;
|
|
582
|
+
const generation = ++escrowLoadGeneration;
|
|
583
|
+
const accountGeneration = accountLoadGeneration;
|
|
584
|
+
const isCurrent = () => generation === escrowLoadGeneration
|
|
585
|
+
&& accountGeneration === accountLoadGeneration && sessionStorage.getItem(TOKEN) === token;
|
|
586
|
+
lastEscrowToken = token; lastEscrowAtRisk = atRisk;
|
|
587
|
+
try {
|
|
588
|
+
const [rows, recoveryKit] = await Promise.all([
|
|
589
|
+
api("/v1/cloud/escrow", { token }),
|
|
590
|
+
api("/v1/cloud/recovery-kit/status", { token })
|
|
591
|
+
]);
|
|
592
|
+
if (!isCurrent()) return;
|
|
593
|
+
const payload = rows && typeof rows === "object" && !Array.isArray(rows) && "escrow" in rows ? rows.escrow : rows;
|
|
594
|
+
if (payload != null && !Array.isArray(payload) && (typeof payload !== "object" || !("sealed" in payload))) {
|
|
595
|
+
throw new Error("The recovery backup status response was not recognized. Try checking again.");
|
|
596
|
+
}
|
|
597
|
+
const held = (Array.isArray(payload) ? payload : payload ? [payload] : []).filter(row => Boolean(row?.sealed));
|
|
598
|
+
const remaining = recoveryKit?.available ? Number(recoveryKit.remaining || 0) : 0;
|
|
599
|
+
const hasUsableCodes = remaining > 0;
|
|
600
|
+
if (held.length && hasUsableCodes) {
|
|
601
|
+
recoveryRemedy("held");
|
|
602
|
+
$("e-head").textContent = "Recovery Kit held";
|
|
603
|
+
$("e-text").textContent = `Stored ${ago(held[0].created_at)}. ${remaining} unused one-time backup code${remaining === 1 ? " remains" : "s remain"}, plus your permanent 24-word phrase. Every key stays encrypted before it reaches us.`;
|
|
604
|
+
} else if (held.length) {
|
|
605
|
+
recoveryRemedy("missing");
|
|
606
|
+
$("e-head").textContent = recoveryKit?.available ? "Backup codes used up" : "Add backup codes";
|
|
607
|
+
$("e-text").textContent = "A sealed phrase-protected key copy is held, but there is no unused one-time code. Create a Recovery Kit on a connected machine that holds this vault key so a fresh machine can recover after Google sign-in.";
|
|
608
|
+
$("e-fix").textContent = recoveryKit?.available ? "Open replacement steps" : "Open creation steps";
|
|
609
|
+
} else if (atRisk) {
|
|
610
|
+
recoveryRemedy("missing");
|
|
611
|
+
$("e-head").textContent = "No sealed copy yet";
|
|
612
|
+
$("e-text").textContent = "You have archives stored. If every machine holding your key is lost, those archives cannot be opened. Create a Recovery Kit from one of those machines now; SealKeep stores only an encrypted copy it cannot open.";
|
|
613
|
+
$("e-fix").textContent = "Open creation steps";
|
|
614
|
+
} else {
|
|
615
|
+
recoveryRemedy("missing");
|
|
616
|
+
$("e-head").textContent = "Store a sealed copy of your key";
|
|
617
|
+
$("e-text").textContent = "Your key stays on your connected machines. From one of them, generate a Backup Recovery Kit. It creates five one-time codes and stores only ciphertext here; we cannot open it.";
|
|
618
|
+
$("e-fix").textContent = "Open creation steps";
|
|
619
|
+
}
|
|
620
|
+
$("c-recovery").classList.toggle("warncard", !held.length && Boolean(atRisk));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (!isCurrent()) return;
|
|
623
|
+
recoveryRemedy("error");
|
|
624
|
+
$("e-head").textContent = "Could not verify recovery backup status";
|
|
625
|
+
$("e-text").textContent = errText(error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const FAMILY_REF = /^(.+)\.(chunk-\d{6}|envelope\.skmeta)$/;
|
|
629
|
+
function foldFamilies(rows) {
|
|
630
|
+
const families = new Map();
|
|
631
|
+
const plain = [];
|
|
632
|
+
for (const row of rows) {
|
|
633
|
+
const match = FAMILY_REF.exec(String(row.vault_ref));
|
|
634
|
+
if (!match) { plain.push(row); continue; }
|
|
635
|
+
const fam = families.get(match[1]) ?? { rows: [], chunkBytes: 0, chunks: 0, members: [], allDurable: true };
|
|
636
|
+
fam.rows.push(row);
|
|
637
|
+
fam.members.push(row.vault_ref);
|
|
638
|
+
if (match[2].startsWith("chunk-")) { fam.chunkBytes += Number(row.bytes) || 0; fam.chunks++; }
|
|
639
|
+
if (row.state !== "durable") fam.allDurable = false;
|
|
640
|
+
families.set(match[1], fam);
|
|
641
|
+
}
|
|
642
|
+
const folded = [...families.entries()].map(([id, fam]) => ({
|
|
643
|
+
...fam.rows[0], vault_ref: id, bytes: fam.chunkBytes, chunked: true,
|
|
644
|
+
chunkCount: fam.chunks, members: fam.members, state: fam.allDurable ? "durable" : "pending"
|
|
645
|
+
}));
|
|
646
|
+
return [...plain, ...folded];
|
|
647
|
+
}
|
|
648
|
+
let targetRows = [];
|
|
649
|
+
let targetBaselineJson = null;
|
|
650
|
+
let targetBaselineUpdatedAt = null;
|
|
651
|
+
let targetBaselineLoaded = false;
|
|
652
|
+
let targetAccountToken = "";
|
|
653
|
+
let targetAccountGeneration = 0;
|
|
654
|
+
let targetLoadGeneration = 0;
|
|
655
|
+
let targetSaveGeneration = 0;
|
|
656
|
+
let accountLoadGeneration = 0;
|
|
657
|
+
let planIsPaid = false;
|
|
658
|
+
let liveCloudBytes = 0;
|
|
659
|
+
const providerLabel = (p) => ({ vaultline: "Sealkeep Cloud", gdrive: "Google Drive", r2: "Cloudflare R2", s3: "S3-compatible", b2: "Backblaze B2", gcs: "Google Cloud Storage" }[p] || p);
|
|
660
|
+
function setTargetRoutingAvailable(available) {
|
|
661
|
+
for (const id of ["tg-add-drive", "tg-add-bucket", "tg-save"]) $(id).disabled = !available;
|
|
662
|
+
for (const control of $("tg-body").querySelectorAll("button,input,select")) control.disabled = !available;
|
|
663
|
+
}
|
|
664
|
+
function resetTargetRoutingState(token = "") {
|
|
665
|
+
targetAccountToken = token;
|
|
666
|
+
targetAccountGeneration += 1;
|
|
667
|
+
targetLoadGeneration += 1;
|
|
668
|
+
targetSaveGeneration += 1;
|
|
669
|
+
targetRows = [];
|
|
670
|
+
targetBaselineJson = null;
|
|
671
|
+
targetBaselineUpdatedAt = null;
|
|
672
|
+
targetBaselineLoaded = false;
|
|
673
|
+
renderTargets();
|
|
674
|
+
setTargetRoutingAvailable(false);
|
|
675
|
+
}
|
|
676
|
+
function targetRoutingAccountIsCurrent(token, generation) {
|
|
677
|
+
return Boolean(token)
|
|
678
|
+
&& token === targetAccountToken
|
|
679
|
+
&& token === sessionStorage.getItem(TOKEN)
|
|
680
|
+
&& generation === targetAccountGeneration;
|
|
681
|
+
}
|
|
682
|
+
function accountLoadIsCurrent(token, generation) {
|
|
683
|
+
return Boolean(token)
|
|
684
|
+
&& token === sessionStorage.getItem(TOKEN)
|
|
685
|
+
&& generation === accountLoadGeneration;
|
|
686
|
+
}
|
|
687
|
+
function targetConnectionSummary(target) {
|
|
688
|
+
const reports = Object.values(target.connectionHealth || {});
|
|
689
|
+
const fresh = reports.filter((report) => report && Date.now() - Date.parse(report.checkedAt || 0) < 3 * 60 * 1000);
|
|
690
|
+
const saved = fresh.filter((report) => report.connected === true).length;
|
|
691
|
+
const missing = fresh.filter((report) => report.connected === false).length;
|
|
692
|
+
const verified = fresh.filter((report) => report.connected === true && report.status === "credentials_verified" && Number.isFinite(Date.parse(report.verifiedAt || ""))).length;
|
|
693
|
+
if (!fresh.length) return { kind: "unknown", text: "Machine credential state not reported yet", action: "Open Settings" };
|
|
694
|
+
if (!saved) return { kind: "disconnected", text: `${missing} online machine${missing === 1 ? "" : "s"} need${missing === 1 ? "s" : ""} credentials`, action: "Add credentials" };
|
|
695
|
+
if (missing) return { kind: "partial", text: `${saved} have credentials · ${missing} need${missing === 1 ? "s" : ""} credentials`, action: "Manage credentials" };
|
|
696
|
+
return { kind: "connected", text: verified === saved
|
|
697
|
+
? `Tested credentials saved on ${saved} online machine${saved === 1 ? "" : "s"}`
|
|
698
|
+
: `Credentials saved on ${saved} online machine${saved === 1 ? "" : "s"} · not a live reachability check`, action: "Manage credentials" };
|
|
699
|
+
}
|
|
700
|
+
function canonicalStorageEndpoint(value, label) {
|
|
701
|
+
let parsed;
|
|
702
|
+
try { parsed = new URL(value); } catch { throw new Error(`${label} needs a valid endpoint URL.`); }
|
|
703
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error("A storage endpoint must use https (or http for a loopback test service).");
|
|
704
|
+
if (parsed.username || parsed.password) throw new Error("A storage endpoint cannot contain credentials.");
|
|
705
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash || value.includes("?") || value.includes("#")) {
|
|
706
|
+
throw new Error("A storage endpoint must be only an origin, without a path, query, or fragment.");
|
|
707
|
+
}
|
|
708
|
+
const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
709
|
+
const loopback = host === "localhost" || host.endsWith(".localhost") || host === "[::1]" || host === "::1" || host.startsWith("127.");
|
|
710
|
+
if (parsed.protocol === "http:" && !loopback) throw new Error("A storage endpoint must use https unless it is a loopback test service.");
|
|
711
|
+
return parsed.origin;
|
|
712
|
+
}
|
|
713
|
+
function renderTargets() {
|
|
714
|
+
const body = $("tg-body");
|
|
715
|
+
body.replaceChildren();
|
|
716
|
+
if (!targetRows.length) {
|
|
717
|
+
$("tg-msg").className = "empty";
|
|
718
|
+
$("tg-msg").textContent = planIsPaid
|
|
719
|
+
? "No rules yet — everything goes to Sealkeep cloud. Add a destination here, or set rules from a machine: sealkeep storage targets add …"
|
|
720
|
+
: "No rules yet — each machine seals to the bucket configured on it. Add a destination here to route by project or cap it.";
|
|
721
|
+
$("tg-wrap").style.display = "none";
|
|
722
|
+
setTargetRoutingAvailable(targetBaselineLoaded);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
$("tg-msg").textContent = "";
|
|
726
|
+
targetRows.sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
|
|
727
|
+
for (const t of targetRows) {
|
|
728
|
+
const tr = document.createElement("tr");
|
|
729
|
+
const name = document.createElement("td");
|
|
730
|
+
const byo = ["r2", "s3", "b2", "gcs"].includes(t.provider);
|
|
731
|
+
if (t.__editable) {
|
|
732
|
+
const sel = document.createElement("select");
|
|
733
|
+
for (const [v, l] of [["r2", "Cloudflare R2"], ["s3", "S3-compatible"], ["b2", "Backblaze B2"], ["gcs", "Google Cloud Storage"]]) {
|
|
734
|
+
const o = document.createElement("option"); o.value = v; o.textContent = l; sel.append(o);
|
|
735
|
+
}
|
|
736
|
+
sel.value = t.provider;
|
|
737
|
+
sel.setAttribute("aria-label", "Provider");
|
|
738
|
+
sel.addEventListener("change", () => {
|
|
739
|
+
t.provider = sel.value;
|
|
740
|
+
delete t.connectionHealth;
|
|
741
|
+
if (t.provider !== "r2" && t.provider !== "s3") delete t.endpoint;
|
|
742
|
+
renderTargets();
|
|
743
|
+
});
|
|
744
|
+
name.append(sel);
|
|
745
|
+
} else name.textContent = providerLabel(t.provider);
|
|
746
|
+
const where = document.createElement("td");
|
|
747
|
+
if (t.__editable) {
|
|
748
|
+
const editor = document.createElement("div");
|
|
749
|
+
editor.className = "target-place-editor";
|
|
750
|
+
const field = (labelText, placeholder, value, change) => {
|
|
751
|
+
const label = document.createElement("label");
|
|
752
|
+
const caption = document.createElement("span"); caption.textContent = labelText;
|
|
753
|
+
const input = document.createElement("input"); input.placeholder = placeholder; input.value = value || "";
|
|
754
|
+
input.addEventListener("input", () => { change(input.value.trim()); delete t.connectionHealth; });
|
|
755
|
+
label.append(caption, input); editor.append(label); return input;
|
|
756
|
+
};
|
|
757
|
+
field("Bucket", "archive-bucket", t.bucket, (value) => { t.bucket = value; });
|
|
758
|
+
field("Prefix", "sealkeep", t.prefix, (value) => { t.prefix = value || undefined; });
|
|
759
|
+
if (t.provider === "s3" || t.provider === "b2") {
|
|
760
|
+
field("Region", t.provider === "b2" ? "us-west-004" : "us-east-1", t.region, (value) => { t.region = value || undefined; });
|
|
761
|
+
}
|
|
762
|
+
if (t.provider === "r2" || t.provider === "s3") {
|
|
763
|
+
const endpoint = field(t.provider === "r2" ? "Endpoint (required)" : "Custom endpoint (optional)", t.provider === "r2" ? "https://<account-id>.r2.cloudflarestorage.com" : "https://s3.example.com", t.endpoint, (value) => { t.endpoint = value || undefined; });
|
|
764
|
+
if (t.provider === "r2") endpoint.required = true;
|
|
765
|
+
}
|
|
766
|
+
where.append(editor);
|
|
767
|
+
} else {
|
|
768
|
+
const wc = document.createElement("code");
|
|
769
|
+
wc.textContent = t.provider === "vaultline" ? "managed"
|
|
770
|
+
: t.provider === "gdrive" ? "folder " + (t.bucket || "Sealkeep")
|
|
771
|
+
: [t.bucket, t.prefix].filter(Boolean).join("/") || "—";
|
|
772
|
+
where.append(wc);
|
|
773
|
+
if (byo) {
|
|
774
|
+
const fixed = document.createElement("span");
|
|
775
|
+
fixed.className = "cellsub";
|
|
776
|
+
fixed.textContent = "Destination identity is fixed — add a new route to change provider, bucket, region, prefix, or endpoint.";
|
|
777
|
+
where.append(fixed);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const held = document.createElement("span");
|
|
781
|
+
held.className = "cellsub";
|
|
782
|
+
if (t.provider === "vaultline") held.textContent = "currently stores " + bytes(liveCloudBytes);
|
|
783
|
+
else {
|
|
784
|
+
const health = targetConnectionSummary(t);
|
|
785
|
+
held.textContent = t.provider === "gdrive" ? `Personal vault · ${health.text}` : health.text;
|
|
786
|
+
held.classList.add("connection-" + health.kind);
|
|
787
|
+
const reconnect = document.createElement("button");
|
|
788
|
+
reconnect.type = "button";
|
|
789
|
+
reconnect.className = "target-reconnect";
|
|
790
|
+
reconnect.textContent = health.action;
|
|
791
|
+
reconnect.title = `Show where to connect ${providerLabel(t.provider)} on a SealKeep machine`;
|
|
792
|
+
reconnect.addEventListener("click", () => openLocalTask("storage", providerLabel(t.provider)));
|
|
793
|
+
held.append(document.createTextNode(" · "), reconnect);
|
|
794
|
+
}
|
|
795
|
+
if (t.enabled === false) {
|
|
796
|
+
held.prepend(document.createTextNode("Paused for new archives · existing copies remain available · "));
|
|
797
|
+
held.classList.add("connection-paused");
|
|
798
|
+
}
|
|
799
|
+
where.append(held);
|
|
800
|
+
const pr = document.createElement("td");
|
|
801
|
+
const at = targetRows.indexOf(t);
|
|
802
|
+
const rank = document.createElement("span");
|
|
803
|
+
rank.className = "cellsub";
|
|
804
|
+
const ordinal = `${at + 1}${["st", "nd", "rd"][at] ?? "th"}`;
|
|
805
|
+
rank.textContent = t.enabled === false
|
|
806
|
+
? `${ordinal} in order — paused`
|
|
807
|
+
: at === 0 ? "1st — new seals go here" : ordinal;
|
|
808
|
+
const up = document.createElement("button"); up.type = "button"; up.className = "ghost"; up.textContent = "▲";
|
|
809
|
+
up.disabled = at === 0;
|
|
810
|
+
up.setAttribute("aria-label", "Move " + providerLabel(t.provider) + " earlier");
|
|
811
|
+
const down = document.createElement("button"); down.type = "button"; down.className = "ghost"; down.textContent = "▼";
|
|
812
|
+
down.disabled = at === targetRows.length - 1;
|
|
813
|
+
down.setAttribute("aria-label", "Move " + providerLabel(t.provider) + " later");
|
|
814
|
+
const swap = (delta) => {
|
|
815
|
+
const to = at + delta;
|
|
816
|
+
[targetRows[at], targetRows[to]] = [targetRows[to], targetRows[at]];
|
|
817
|
+
targetRows.forEach((row, index) => { row.priority = (index + 1) * 10; });
|
|
818
|
+
renderTargets();
|
|
819
|
+
say("tg-save-msg", "Order changed — not saved yet. Save routing makes it real on every machine.");
|
|
820
|
+
};
|
|
821
|
+
up.addEventListener("click", () => swap(-1));
|
|
822
|
+
down.addEventListener("click", () => swap(1));
|
|
823
|
+
pr.append(up, down, document.createTextNode(" "), rank);
|
|
824
|
+
const cap = document.createElement("td");
|
|
825
|
+
const capIn = document.createElement("input"); capIn.type = "number"; capIn.className = "n"; capIn.min = "1"; capIn.max = "99999"; capIn.placeholder = "none";
|
|
826
|
+
if (t.maxGb) capIn.value = t.maxGb;
|
|
827
|
+
capIn.setAttribute("aria-label", "Cap in GB for " + providerLabel(t.provider));
|
|
828
|
+
capIn.addEventListener("input", () => { t.maxGb = capIn.value ? Number(capIn.value) : undefined; });
|
|
829
|
+
cap.append(capIn);
|
|
830
|
+
const proj = document.createElement("td");
|
|
831
|
+
const stablePin = /^(?:g1|p1):[a-f0-9]{64}$/;
|
|
832
|
+
const pins = Array.isArray(t.projects) ? t.projects : [];
|
|
833
|
+
const stablePins = pins.filter((project) => stablePin.test(project));
|
|
834
|
+
const legacyPins = pins.length - stablePins.length;
|
|
835
|
+
const pinSummary = document.createElement("span");
|
|
836
|
+
pinSummary.className = "cellsub";
|
|
837
|
+
pinSummary.textContent = pins.length === 0
|
|
838
|
+
? "Every personal project follows this route"
|
|
839
|
+
: legacyPins > 0
|
|
840
|
+
? `${pins.length} existing pin${pins.length === 1 ? "" : "s"} · ${legacyPins} older name pin${legacyPins === 1 ? " needs" : "s need"} review`
|
|
841
|
+
: `${stablePins.length} project${stablePins.length === 1 ? "" : "s"} pinned by stable identity`;
|
|
842
|
+
const managePins = document.createElement("button");
|
|
843
|
+
managePins.type = "button";
|
|
844
|
+
managePins.className = "target-reconnect";
|
|
845
|
+
managePins.textContent = pins.length ? "Review on a machine" : "Choose on a machine";
|
|
846
|
+
managePins.title = `Use detected project identities for ${providerLabel(t.provider)}`;
|
|
847
|
+
managePins.addEventListener("click", () => openLocalTask("project-routing", providerLabel(t.provider)));
|
|
848
|
+
proj.append(pinSummary, document.createTextNode(" "), managePins);
|
|
849
|
+
const act = document.createElement("td");
|
|
850
|
+
const rm = document.createElement("button"); rm.className = "del"; rm.type = "button";
|
|
851
|
+
const removableDraft = t.__editable || t.__new;
|
|
852
|
+
rm.textContent = removableDraft ? "Remove" : t.enabled === false ? "Resume new archives" : "Pause new archives";
|
|
853
|
+
rm.addEventListener("click", () => {
|
|
854
|
+
if (removableDraft) {
|
|
855
|
+
targetRows = targetRows.filter((x) => x !== t);
|
|
856
|
+
renderTargets();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
t.enabled = t.enabled === false;
|
|
860
|
+
renderTargets();
|
|
861
|
+
say("tg-save-msg", t.enabled === false
|
|
862
|
+
? `${providerLabel(t.provider)} will stop receiving new archives after you save. Its identity stays here so existing copies can still be restored or moved.`
|
|
863
|
+
: `${providerLabel(t.provider)} will receive eligible new archives again after you save.`);
|
|
864
|
+
});
|
|
865
|
+
act.append(rm);
|
|
866
|
+
tr.append(name, where, pr, cap, proj, act);
|
|
867
|
+
body.append(tr);
|
|
868
|
+
}
|
|
869
|
+
$("tg-wrap").style.display = "";
|
|
870
|
+
setTargetRoutingAvailable(targetBaselineLoaded);
|
|
871
|
+
}
|
|
872
|
+
async function loadTargets(token) {
|
|
873
|
+
if (!token || token !== targetAccountToken || token !== sessionStorage.getItem(TOKEN)) return false;
|
|
874
|
+
const accountGeneration = targetAccountGeneration;
|
|
875
|
+
const generation = ++targetLoadGeneration;
|
|
876
|
+
targetBaselineLoaded = false;
|
|
877
|
+
setTargetRoutingAvailable(false);
|
|
878
|
+
try {
|
|
879
|
+
const data = await api("/v1/cloud/storage-targets", { token });
|
|
880
|
+
if (generation !== targetLoadGeneration || !targetRoutingAccountIsCurrent(token, accountGeneration)) return false;
|
|
881
|
+
targetBaselineJson = typeof data.targets_json === "string" ? data.targets_json : null;
|
|
882
|
+
targetBaselineUpdatedAt = typeof data.updated_at === "string" ? data.updated_at : null;
|
|
883
|
+
try { targetRows = JSON.parse(targetBaselineJson || "[]"); } catch { targetRows = []; }
|
|
884
|
+
if (!Array.isArray(targetRows)) targetRows = [];
|
|
885
|
+
targetBaselineLoaded = true;
|
|
886
|
+
renderTargets();
|
|
887
|
+
return true;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (generation !== targetLoadGeneration || !targetRoutingAccountIsCurrent(token, accountGeneration)) return false;
|
|
890
|
+
targetBaselineLoaded = false;
|
|
891
|
+
renderTargets();
|
|
892
|
+
say("tg-msg", `${errText(error)} The last loaded rows are read-only until the account reload succeeds.`, "err");
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
$("tg-add-bucket").addEventListener("click", () => {
|
|
897
|
+
if (!targetBaselineLoaded) return void say("tg-save-msg", "Wait for the account routing to load before editing it.", "err");
|
|
898
|
+
targetRows.push({ id: "byo-" + Math.random().toString(36).slice(2, 8), provider: "r2", priority: 5, __editable: true });
|
|
899
|
+
renderTargets();
|
|
900
|
+
say("tg-save-msg", "Fill in the destination and save. Then use Sealkeep Settings on each machine to test and store that machine's credentials.");
|
|
901
|
+
});
|
|
902
|
+
$("tg-add-drive").addEventListener("click", () => {
|
|
903
|
+
if (!targetBaselineLoaded) return void say("tg-save-msg", "Wait for the account routing to load before editing it.", "err");
|
|
904
|
+
if (targetRows.some((t) => t.provider === "gdrive")) { say("tg-save-msg", "Drive is already in the rules."); return; }
|
|
905
|
+
targetRows.push({ id: "gdrive-" + Math.random().toString(36).slice(2, 8), provider: "gdrive", bucket: "Sealkeep", maxGb: 50, priority: 10, __new: true });
|
|
906
|
+
renderTargets();
|
|
907
|
+
say("tg-save-msg", "Added for personal vault memory — save, then open Sealkeep Settings on each machine for the one-time consent. Shared projects continue using managed project memory.");
|
|
908
|
+
});
|
|
909
|
+
async function saveTargets() {
|
|
910
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
911
|
+
if (!token) { say("tg-save-msg", "Sign in again to save.", "err"); return; }
|
|
912
|
+
if (!targetBaselineLoaded || token !== targetAccountToken) { say("tg-save-msg", "Reload this account's routing before saving.", "err"); return; }
|
|
913
|
+
const accountGeneration = targetAccountGeneration;
|
|
914
|
+
const saveGeneration = ++targetSaveGeneration;
|
|
915
|
+
const saveIsCurrent = () => saveGeneration === targetSaveGeneration
|
|
916
|
+
&& targetRoutingAccountIsCurrent(token, accountGeneration);
|
|
917
|
+
$("tg-save").disabled = true;
|
|
918
|
+
try {
|
|
919
|
+
for (const target of targetRows) {
|
|
920
|
+
if (!["r2", "s3", "b2", "gcs"].includes(target.provider)) continue;
|
|
921
|
+
if (!String(target.bucket || "").trim()) throw new Error(`${providerLabel(target.provider)} needs a bucket name.`);
|
|
922
|
+
target.prefix = String(target.prefix || "sealkeep").replace(/^\/+|\/+$/g, "");
|
|
923
|
+
if (target.provider === "s3" && !target.region) target.region = "us-east-1";
|
|
924
|
+
if (target.provider === "b2" && !target.region) throw new Error("Backblaze B2 needs the region from its S3 endpoint, for example us-west-004.");
|
|
925
|
+
if (target.provider === "r2" && !target.endpoint) throw new Error("Cloudflare R2 needs its account endpoint.");
|
|
926
|
+
if (target.endpoint) {
|
|
927
|
+
target.endpoint = canonicalStorageEndpoint(target.endpoint, providerLabel(target.provider));
|
|
928
|
+
const endpointHost = new URL(target.endpoint).hostname.toLowerCase().replace(/\.$/, "");
|
|
929
|
+
if (target.provider === "r2" && !endpointHost.endsWith(".r2.cloudflarestorage.com")) {
|
|
930
|
+
throw new Error("Cloudflare R2 needs its account endpoint ending in .r2.cloudflarestorage.com.");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const clean = targetRows.map(({ __editable, __new, usedBytes, usedAt, ...t }) => t);
|
|
935
|
+
const nextJson = JSON.stringify(clean);
|
|
936
|
+
let saved;
|
|
937
|
+
if (targetBaselineJson === null) {
|
|
938
|
+
saved = await api("/v1/cloud/storage-targets", { method: "PUT", token, body: { targets_json: nextJson } });
|
|
939
|
+
} else {
|
|
940
|
+
saved = await api("/v1/cloud/storage-targets", {
|
|
941
|
+
method: "PATCH", token,
|
|
942
|
+
body: {
|
|
943
|
+
targets_json: nextJson,
|
|
944
|
+
expected_targets_json: targetBaselineJson,
|
|
945
|
+
expected_updated_at: targetBaselineUpdatedAt,
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (!saveIsCurrent()) return;
|
|
950
|
+
targetBaselineJson = nextJson;
|
|
951
|
+
targetBaselineUpdatedAt = typeof saved?.updated_at === "string" ? saved.updated_at : null;
|
|
952
|
+
targetRows = clean;
|
|
953
|
+
renderTargets();
|
|
954
|
+
say("tg-save-msg", "Saved. Every machine follows these on its next seal.");
|
|
955
|
+
} catch (error) {
|
|
956
|
+
if (!saveIsCurrent()) return;
|
|
957
|
+
if (error?.status === 409) {
|
|
958
|
+
const reloaded = await loadTargets(token);
|
|
959
|
+
if (!saveIsCurrent()) return;
|
|
960
|
+
say("tg-save-msg", reloaded
|
|
961
|
+
? "Routing changed elsewhere. Your draft was not saved; the latest rules are loaded for review."
|
|
962
|
+
: "Routing changed elsewhere, and the latest rules could not be reloaded yet. Editing stays disabled until they load.", "err");
|
|
963
|
+
} else say("tg-save-msg", errText(error), "err");
|
|
964
|
+
}
|
|
965
|
+
if (!saveIsCurrent()) return;
|
|
966
|
+
$("tg-save").disabled = !targetBaselineLoaded;
|
|
967
|
+
}
|
|
968
|
+
$("tg-save").addEventListener("click", saveTargets);
|
|
969
|
+
async function loadStorage(token) {
|
|
970
|
+
try {
|
|
971
|
+
const rows = await api("/v1/cloud/archives", { token });
|
|
972
|
+
const body = $("s-body");
|
|
973
|
+
body.replaceChildren();
|
|
974
|
+
if (!rows.length) {
|
|
975
|
+
on("s-intro", false);
|
|
976
|
+
$("a-count").textContent = "0";
|
|
977
|
+
$("s-msg").className = "empty";
|
|
978
|
+
$("s-msg").textContent = "Nothing stored yet. Archives appear here on their own once a machine is connected.";
|
|
979
|
+
$("s-wrap").style.display = "none";
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
on("s-intro", true);
|
|
983
|
+
for (const r of foldFamilies(rows)) {
|
|
984
|
+
const tr = document.createElement("tr");
|
|
985
|
+
const ref = document.createElement("td");
|
|
986
|
+
const code = document.createElement("code"); code.textContent = String(r.vault_ref).slice(0, 8); ref.append(code);
|
|
987
|
+
const size = document.createElement("td");
|
|
988
|
+
const sc = document.createElement("code");
|
|
989
|
+
sc.textContent = bytes(r.bytes);
|
|
990
|
+
size.append(sc);
|
|
991
|
+
if (r.chunked) {
|
|
992
|
+
const sub = document.createElement("span");
|
|
993
|
+
sub.className = "cellsub";
|
|
994
|
+
sub.textContent = r.chunkCount + " sealed chunks";
|
|
995
|
+
size.append(sub);
|
|
996
|
+
}
|
|
997
|
+
const st = document.createElement("td");
|
|
998
|
+
const s = document.createElement("span");
|
|
999
|
+
s.className = "state " + (r.state === "durable" ? "durable" : "pending");
|
|
1000
|
+
s.textContent = r.state === "durable" ? "stored" : "incomplete";
|
|
1001
|
+
st.append(s);
|
|
1002
|
+
const when = document.createElement("td"); when.className = "mono"; when.style.fontSize = ".78rem"; when.style.color = "var(--soft)"; when.textContent = ago(r.durable_at || r.created_at);
|
|
1003
|
+
const act = document.createElement("td");
|
|
1004
|
+
const btn = document.createElement("button");
|
|
1005
|
+
btn.className = "del"; btn.type = "button"; btn.textContent = "Delete";
|
|
1006
|
+
btn.addEventListener("click", async () => {
|
|
1007
|
+
if (btn.dataset.armed !== "yes") {
|
|
1008
|
+
btn.dataset.armed = "yes"; btn.textContent = "Confirm";
|
|
1009
|
+
setTimeout(() => { if (btn.dataset.armed === "yes") { btn.dataset.armed = "no"; btn.textContent = "Delete"; } }, 4000);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
btn.disabled = true; btn.textContent = "Deleting";
|
|
1013
|
+
try {
|
|
1014
|
+
const refs = r.members ? [...r.members].sort((a, b) => a.endsWith(".envelope.skmeta") ? 1 : b.endsWith(".envelope.skmeta") ? -1 : 0) : [r.vault_ref];
|
|
1015
|
+
for (let i = 0; i < refs.length; i++) {
|
|
1016
|
+
if (refs.length > 1) btn.textContent = "Deleting " + (i + 1) + "/" + refs.length;
|
|
1017
|
+
await api("/v1/cloud/archives/" + encodeURIComponent(refs[i]), {
|
|
1018
|
+
method: "DELETE", token, body: { owner_key: sessionStorage.getItem(OWNER_KEY) || undefined }
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
await loadAccount(token);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const stale = !sessionStorage.getItem(OWNER_KEY);
|
|
1024
|
+
say("s-msg", stale
|
|
1025
|
+
? "Sign out and back in, then delete again — this session started before deletes were tied to a machine."
|
|
1026
|
+
: errText(error), "err");
|
|
1027
|
+
btn.disabled = false; btn.textContent = "Delete";
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
act.append(btn);
|
|
1031
|
+
tr.append(ref, size, st, when, act);
|
|
1032
|
+
body.append(tr);
|
|
1033
|
+
}
|
|
1034
|
+
const folded = foldFamilies(rows);
|
|
1035
|
+
const stored = folded.filter((r) => r.state === "durable").length;
|
|
1036
|
+
say("s-msg", "");
|
|
1037
|
+
$("a-count").textContent = String(folded.length);
|
|
1038
|
+
$("s-foot").textContent = `${folded.length} archive${folded.length === 1 ? "" : "s"} · ${stored} fully stored`
|
|
1039
|
+
+ (stored < folded.length ? ` · ${folded.length - stored} incomplete, not billed` : "");
|
|
1040
|
+
$("s-wrap").style.display = "";
|
|
1041
|
+
} catch (error) { say("s-msg", errText(error), "err"); }
|
|
1042
|
+
}
|
|
1043
|
+
const teamRows = (value, keys) => {
|
|
1044
|
+
if (Array.isArray(value)) return value;
|
|
1045
|
+
if (!value || typeof value !== "object") return [];
|
|
1046
|
+
for (const key of keys) {
|
|
1047
|
+
if (Array.isArray(value[key])) return value[key];
|
|
1048
|
+
if (value[key] && typeof value[key] === "object") return [value[key]];
|
|
1049
|
+
}
|
|
1050
|
+
return [];
|
|
1051
|
+
};
|
|
1052
|
+
const teamState = { token: "", selected: "", spaces: [], requests: [], incoming: [], outgoing: [], removals: [], members: new Map(), poll: 0, canCreate: false, draftOpened: false, loaded: false, machinesLoaded: false, machineCount: null, localDrafts: new Set() };
|
|
1053
|
+
const projectName = (item) => String(item?.project_name || "Untitled project");
|
|
1054
|
+
const normalizedProjectName = (value) => String(value || "").trim().toLocaleLowerCase();
|
|
1055
|
+
const matchingTeamProjectsByName = (name) => {
|
|
1056
|
+
const wanted = normalizedProjectName(name);
|
|
1057
|
+
if (!wanted) return [];
|
|
1058
|
+
return [
|
|
1059
|
+
...teamState.spaces.map((item) => ({ ...item, uiId: `space:${item.space_key}` })),
|
|
1060
|
+
...teamState.requests.filter((item) => item.state === "requested").map((item) => ({ ...item, uiId: `request:${item.id}` })),
|
|
1061
|
+
].filter((item) => normalizedProjectName(projectName(item)) === wanted);
|
|
1062
|
+
};
|
|
1063
|
+
const initials = (value) => String(value || "?").split(/[@\s._-]+/).filter(Boolean).slice(0, 2).map((word) => word[0]).join("");
|
|
1064
|
+
function beginTeamAccount(token) {
|
|
1065
|
+
if (teamState.token === token) return;
|
|
1066
|
+
clearTimeout(teamState.poll);
|
|
1067
|
+
teamState.token = token;
|
|
1068
|
+
teamState.selected = "";
|
|
1069
|
+
teamState.spaces = [];
|
|
1070
|
+
teamState.requests = [];
|
|
1071
|
+
teamState.incoming = [];
|
|
1072
|
+
teamState.outgoing = [];
|
|
1073
|
+
teamState.removals = [];
|
|
1074
|
+
teamState.members = new Map();
|
|
1075
|
+
teamState.loaded = false;
|
|
1076
|
+
teamState.machinesLoaded = false;
|
|
1077
|
+
teamState.machineCount = null;
|
|
1078
|
+
teamState.draftOpened = false;
|
|
1079
|
+
teamState.localDrafts = new Set();
|
|
1080
|
+
$("team-incoming").replaceChildren();
|
|
1081
|
+
$("team-project-list").replaceChildren();
|
|
1082
|
+
$("team-project-detail").replaceChildren();
|
|
1083
|
+
$("team-project-empty").style.display = "";
|
|
1084
|
+
$("team-project-empty").textContent = token ? "Loading your projects…" : "Sign in to see your projects.";
|
|
1085
|
+
closeTeamDrawer();
|
|
1086
|
+
}
|
|
1087
|
+
function avatar(value) {
|
|
1088
|
+
const el = document.createElement("span"); el.className = "member-avatar"; el.textContent = initials(value); return el;
|
|
1089
|
+
}
|
|
1090
|
+
function closeTeamDrawer() {
|
|
1091
|
+
$("team-invite-drawer").hidden = true;
|
|
1092
|
+
$("projects-layout").classList.remove("drawer-open");
|
|
1093
|
+
$("team-invite-email").value = "";
|
|
1094
|
+
say("team-invite-msg", "");
|
|
1095
|
+
}
|
|
1096
|
+
function currentTeamProject() {
|
|
1097
|
+
return teamState.spaces.find((space) => `space:${space.space_key}` === teamState.selected)
|
|
1098
|
+
|| teamState.requests.find((request) => `request:${request.id}` === teamState.selected) || null;
|
|
1099
|
+
}
|
|
1100
|
+
function makeAction(label, className, handler, disabled = false) {
|
|
1101
|
+
const button = document.createElement("button"); button.type = "button"; button.className = className; button.textContent = label; button.disabled = disabled; button.addEventListener("click", handler); return button;
|
|
1102
|
+
}
|
|
1103
|
+
function openProjectDialog(name = "") {
|
|
1104
|
+
$("team-project-name").value = name;
|
|
1105
|
+
say("team-project-msg", name ? "Your folder choice is saved on this machine. Create the project here and Sealkeep will connect it automatically." : "");
|
|
1106
|
+
$("team-project-dialog").showModal();
|
|
1107
|
+
setTimeout(() => { $("team-project-name").focus(); $("team-project-name").select(); }, 0);
|
|
1108
|
+
}
|
|
1109
|
+
function openLinkedProjectDraft() {
|
|
1110
|
+
if (teamState.draftOpened) return;
|
|
1111
|
+
const name = (sessionStorage.getItem(PROJECT_DRAFT_NAME) || "").trim();
|
|
1112
|
+
if (!name || !teamState.canCreate) return;
|
|
1113
|
+
teamState.draftOpened = true;
|
|
1114
|
+
const normalized = normalizedProjectName(name);
|
|
1115
|
+
teamState.localDrafts.add(normalized);
|
|
1116
|
+
const matches = matchingTeamProjectsByName(name);
|
|
1117
|
+
sessionStorage.removeItem(PROJECT_DRAFT_NAME);
|
|
1118
|
+
history.replaceState(null, "", `${location.pathname}?view=projects#account`);
|
|
1119
|
+
if (matches.length === 1) {
|
|
1120
|
+
teamState.selected = matches[0].uiId;
|
|
1121
|
+
renderTeamProjects();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
openProjectDialog(name);
|
|
1125
|
+
if (matches.length > 1) say("team-project-msg", "More than one older project uses this name. Choose the exact project from the list instead of creating another one.", "err");
|
|
1126
|
+
}
|
|
1127
|
+
function renderIncoming() {
|
|
1128
|
+
const box = $("team-incoming"); box.replaceChildren();
|
|
1129
|
+
for (const invitation of teamState.incoming) {
|
|
1130
|
+
const ready = invitation.state === "ready" || Boolean(invitation.wrapped_key);
|
|
1131
|
+
const row = document.createElement("div"); row.className = "invite-card";
|
|
1132
|
+
const copy = document.createElement("div"); copy.className = "invite-copy";
|
|
1133
|
+
const title = document.createElement("strong"); title.textContent = projectName(invitation);
|
|
1134
|
+
const detail = document.createElement("span"); detail.textContent = `${invitation.inviter_email || "A teammate"} invited you · ${ready ? "encrypted access is ready" : "their machine is preparing encrypted access"}`;
|
|
1135
|
+
copy.append(title, detail);
|
|
1136
|
+
const actions = document.createElement("div"); actions.className = "invite-actions";
|
|
1137
|
+
actions.append(
|
|
1138
|
+
makeAction(ready ? "Accept" : "Preparing…", "primary", async (event) => {
|
|
1139
|
+
const button = event.currentTarget; button.disabled = true;
|
|
1140
|
+
try { await api(`/v1/cloud/team/invitations/${encodeURIComponent(invitation.id)}/accept`, { method: "POST", token: teamState.token, body: {} }); await loadTeamAccess(teamState.token); }
|
|
1141
|
+
catch (error) { button.disabled = !ready; detail.textContent = errText(error); }
|
|
1142
|
+
}, !ready),
|
|
1143
|
+
makeAction("Decline", "ghost", async (event) => {
|
|
1144
|
+
event.currentTarget.disabled = true;
|
|
1145
|
+
try { await api(`/v1/cloud/team/invitations/${encodeURIComponent(invitation.id)}/decline`, { method: "POST", token: teamState.token, body: {} }); await loadTeamAccess(teamState.token); }
|
|
1146
|
+
catch (error) { event.currentTarget.disabled = false; detail.textContent = errText(error); }
|
|
1147
|
+
}),
|
|
1148
|
+
);
|
|
1149
|
+
row.append(avatar(invitation.inviter_email), copy, actions); box.append(row);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function renderTeamProjects() {
|
|
1153
|
+
const all = [
|
|
1154
|
+
...teamState.spaces.map((item) => ({ ...item, uiId: `space:${item.space_key}`, pending: false })),
|
|
1155
|
+
...teamState.requests.filter((item) => item.state === "requested").map((item) => ({ ...item, uiId: `request:${item.id}`, pending: true })),
|
|
1156
|
+
].sort((a, b) => projectName(a).localeCompare(projectName(b)));
|
|
1157
|
+
if (!all.some((item) => item.uiId === teamState.selected)) teamState.selected = all[0]?.uiId || "";
|
|
1158
|
+
const list = $("team-project-list"); list.replaceChildren();
|
|
1159
|
+
$("team-project-new").disabled = !teamState.canCreate;
|
|
1160
|
+
$("team-project-new").title = teamState.canCreate ? "Create project memory" : "Creating projects requires the Team plan";
|
|
1161
|
+
for (const item of all) {
|
|
1162
|
+
const memberResult = item.pending ? [] : teamState.members.get(item.space_key);
|
|
1163
|
+
const members = Array.isArray(memberResult) ? memberResult : [];
|
|
1164
|
+
const li = document.createElement("li"); const button = document.createElement("button"); button.type = "button"; button.setAttribute("aria-current", String(item.uiId === teamState.selected));
|
|
1165
|
+
const mark = document.createElement("span"); mark.className = `project-mark${item.pending ? " pending" : ""}`;
|
|
1166
|
+
const label = document.createElement("span"); label.className = "project-label"; label.textContent = projectName(item);
|
|
1167
|
+
const count = document.createElement("span"); count.className = "project-count"; count.textContent = item.pending ? "finishing…" : memberResult === null ? "?" : `${members.length || 1} people`;
|
|
1168
|
+
button.setAttribute("aria-label", `${projectName(item)}, ${item.pending ? "finishing automatically when an owner machine checks in" : count.textContent}`);
|
|
1169
|
+
button.append(mark, label, count); button.addEventListener("click", () => { teamState.selected = item.uiId; closeTeamDrawer(); renderTeamProjects(); }); li.append(button); list.append(li);
|
|
1170
|
+
}
|
|
1171
|
+
$("team-project-empty").style.display = all.length ? "none" : "";
|
|
1172
|
+
if (!all.length) $("team-project-empty").textContent = teamState.canCreate
|
|
1173
|
+
? "No project memory yet. Create one here—your machine handles the encryption automatically."
|
|
1174
|
+
: "No shared project memory yet. You can accept a Team invitation here; creating a project requires the Team plan.";
|
|
1175
|
+
renderTeamDetail();
|
|
1176
|
+
}
|
|
1177
|
+
function renderTeamDetail() {
|
|
1178
|
+
const detail = $("team-project-detail"); detail.replaceChildren();
|
|
1179
|
+
const selected = currentTeamProject();
|
|
1180
|
+
if (!selected) {
|
|
1181
|
+
const empty = document.createElement("div"); empty.className = "project-empty-state";
|
|
1182
|
+
const inner = document.createElement("div"); const glyph = document.createElement("div"); glyph.className = "cloud-glyph"; glyph.textContent = "⌁";
|
|
1183
|
+
const title = document.createElement("h3"); title.textContent = "Create your first project memory";
|
|
1184
|
+
const copy = document.createElement("p"); copy.textContent = "It starts private. Invite Ann, Bob, or anyone else only when this project should become shared.";
|
|
1185
|
+
const button = makeAction("Create project", "primary", () => openProjectDialog()); inner.append(glyph, title, copy, button); empty.append(inner); detail.append(empty); return;
|
|
1186
|
+
}
|
|
1187
|
+
const pending = Object.hasOwn(selected, "state") && selected.state === "requested" && !selected.space_key;
|
|
1188
|
+
const space = pending ? null : selected;
|
|
1189
|
+
const head = document.createElement("div"); head.className = "project-head";
|
|
1190
|
+
const copy = document.createElement("div"); const title = document.createElement("h3"); title.textContent = projectName(selected);
|
|
1191
|
+
const meta = document.createElement("div"); meta.className = "project-meta";
|
|
1192
|
+
const privacy = document.createElement("span"); privacy.textContent = space?.role === "owner" ? "Private by default" : "Shared with you";
|
|
1193
|
+
const key = document.createElement("span"); key.className = pending ? "" : "ready"; key.textContent = pending ? "Finishing automatically" : "● Project key ready"; meta.append(privacy, key); copy.append(title, meta); head.append(copy);
|
|
1194
|
+
if (space?.role === "owner" && teamState.canCreate) head.append(makeAction("Invite people", "primary", () => {
|
|
1195
|
+
const drawer = $("team-invite-drawer");
|
|
1196
|
+
$("team-drawer-project").textContent = `Access to ${projectName(space)}`; drawer.hidden = false; $("projects-layout").classList.add("drawer-open");
|
|
1197
|
+
setTimeout(() => {
|
|
1198
|
+
if (matchMedia("(max-width:70rem)").matches) drawer.scrollIntoView({ block: "start", behavior: motionOK() ? "smooth" : "instant" });
|
|
1199
|
+
$("team-invite-email").focus({ preventScroll: true });
|
|
1200
|
+
}, 0);
|
|
1201
|
+
}));
|
|
1202
|
+
detail.append(head);
|
|
1203
|
+
if (pending) {
|
|
1204
|
+
const wait = document.createElement("div"); wait.className = "project-wait"; const strong = document.createElement("strong"); strong.textContent = "Project name saved · finishing automatically";
|
|
1205
|
+
const note = document.createElement("span");
|
|
1206
|
+
const fromLocal = teamState.localDrafts.has(projectName(selected).trim().toLocaleLowerCase());
|
|
1207
|
+
const machineFact = !teamState.machinesLoaded
|
|
1208
|
+
? "Checking which machines are registered to this account."
|
|
1209
|
+
: teamState.machineCount === 0
|
|
1210
|
+
? "No machine is connected to this account yet."
|
|
1211
|
+
: `${teamState.machineCount} machine${teamState.machineCount === 1 ? " is" : "s are"} registered to the account. This cloud page does not receive live dashboard status, so it cannot claim this computer is online.`;
|
|
1212
|
+
note.textContent = `${fromLocal ? "The folder choice is already saved on the computer that sent you here. " : ""}A running owner machine creates and encrypts the project key; the cloud never receives it. ${machineFact}`;
|
|
1213
|
+
const actions = document.createElement("div"); actions.className = "project-wait-actions";
|
|
1214
|
+
const check = makeAction("Check again", "primary", async (event) => {
|
|
1215
|
+
const button = event.currentTarget; button.disabled = true; button.textContent = "Checking…";
|
|
1216
|
+
try { await loadTeamAccess(teamState.token); }
|
|
1217
|
+
finally { if (button.isConnected) { button.disabled = false; button.textContent = "Check again"; } }
|
|
1218
|
+
});
|
|
1219
|
+
const next = teamState.machinesLoaded && teamState.machineCount === 0
|
|
1220
|
+
? makeAction("Connect a machine", "ghost", () => showAccountView("machines"))
|
|
1221
|
+
: makeAction("Show local steps", "ghost", () => openLocalTask("project", projectName(selected)));
|
|
1222
|
+
const cancel = makeAction("Cancel request", "ghost", async (event) => { event.currentTarget.disabled = true; try { await api(`/v1/cloud/team/project-requests/${encodeURIComponent(selected.id)}/cancel`, { method: "POST", token: teamState.token, body: {} }); teamState.selected = ""; await loadTeamAccess(teamState.token); } catch (error) { note.textContent = errText(error); event.currentTarget.disabled = false; } });
|
|
1223
|
+
actions.append(check, next, cancel); wait.append(strong, note, actions); detail.append(wait); return;
|
|
1224
|
+
}
|
|
1225
|
+
const connected = document.createElement("div"); connected.className = "project-wait";
|
|
1226
|
+
const connectedTitle = document.createElement("strong"); connectedTitle.textContent = "Connect work on each machine";
|
|
1227
|
+
const connectedCopy = document.createElement("span"); connectedCopy.textContent = "Shared memory is stored in SealKeep Cloud for all project members, separately from personal backup destinations such as Google Drive. Local work folders are not storage destinations: SealKeep detects folders from Codex and Claude sessions, and each teammate confirms which ones belong here. Different paths can use the same project; source files are not copied between teammates.";
|
|
1228
|
+
const connect = makeAction("Connect detected work folders", "primary", () => openLocalTask("folders", projectName(space), space.space_key));
|
|
1229
|
+
connected.append(connectedTitle, connectedCopy, connect); detail.append(connected);
|
|
1230
|
+
const memberResult = teamState.members.get(space.space_key);
|
|
1231
|
+
const members = Array.isArray(memberResult) ? memberResult : [];
|
|
1232
|
+
const outgoing = teamState.outgoing.filter((item) => item.space_key === space.space_key);
|
|
1233
|
+
const heading = document.createElement("div"); heading.className = "members-head"; const h4 = document.createElement("h4"); h4.textContent = "People with access"; const total = document.createElement("span"); total.textContent = memberResult === null ? "Could not verify people · retrying" : `${members.length || 1} active · ${outgoing.length} pending`; heading.append(h4, total); detail.append(heading);
|
|
1234
|
+
const list = document.createElement("ul"); list.className = "member-list";
|
|
1235
|
+
for (const member of members) {
|
|
1236
|
+
const owner = member.role === "owner"; const removing = teamState.removals.some((item) => item.space_key === space.space_key && Number(item.target_account_id) === Number(member.account_id) && item.state === "requested");
|
|
1237
|
+
const row = document.createElement("li"); row.className = "member-row"; const email = member.display_email || (owner ? "Project owner" : "Project member");
|
|
1238
|
+
const memberCopy = document.createElement("div"); memberCopy.className = "member-copy"; const strong = document.createElement("strong"); strong.textContent = email; const sub = document.createElement("span"); sub.textContent = removing ? "Access removal is being sealed on your machine" : owner ? "Owns this project memory" : "Can read, write, and search this project"; memberCopy.append(strong, sub);
|
|
1239
|
+
const role = document.createElement("span"); role.className = `member-role${owner ? " owner" : ""}`; role.textContent = removing ? "Removing…" : owner ? "Owner" : "Member"; row.append(avatar(email), memberCopy, role);
|
|
1240
|
+
if (space.role === "owner" && !owner && !removing) row.append(makeAction("Remove", "ghost", async (event) => { if (!confirm(`Remove ${email} from ${projectName(space)}? Their future access ends after your machine rotates the project key.`)) return; event.currentTarget.disabled = true; try { await api(`/v1/cloud/team/spaces/${encodeURIComponent(space.space_key)}/members/${encodeURIComponent(member.account_id)}/removal-requests`, { method: "POST", token: teamState.token, body: {} }); await loadTeamAccess(teamState.token); } catch (error) { sub.textContent = errText(error); event.currentTarget.disabled = false; } }));
|
|
1241
|
+
list.append(row);
|
|
1242
|
+
}
|
|
1243
|
+
for (const invitation of outgoing) {
|
|
1244
|
+
const row = document.createElement("li"); row.className = "member-row"; const email = invitation.invitee_email || "Invited teammate"; const memberCopy = document.createElement("div"); memberCopy.className = "member-copy"; const strong = document.createElement("strong"); strong.textContent = email; const sub = document.createElement("span"); sub.textContent = invitation.state === "ready" || invitation.wrapped_key ? "Invitation sent · awaiting acceptance" : "Preparing encrypted access on your machine"; memberCopy.append(strong, sub); const role = document.createElement("span"); role.className = "member-role"; role.textContent = "Invited"; row.append(avatar(email), memberCopy, role, makeAction("Cancel", "ghost", async (event) => { event.currentTarget.disabled = true; try { await api(`/v1/cloud/team/invitations/${encodeURIComponent(invitation.id)}/cancel`, { method: "POST", token: teamState.token, body: {} }); await loadTeamAccess(teamState.token); } catch (error) { sub.textContent = errText(error); event.currentTarget.disabled = false; } })); list.append(row);
|
|
1245
|
+
}
|
|
1246
|
+
if (!members.length) { const row = document.createElement("li"); row.className = "member-row"; const copy = document.createElement("div"); copy.className = "member-copy"; const strong = document.createElement("strong"); strong.textContent = space.role === "owner" ? "You" : "Project membership"; const sub = document.createElement("span"); sub.textContent = space.role === "owner" ? "Owner · private until you invite someone" : "You have project-scoped access"; copy.append(strong, sub); row.append(avatar("You"), copy); list.append(row); }
|
|
1247
|
+
detail.append(list);
|
|
1248
|
+
}
|
|
1249
|
+
async function loadTeamAccess(token) {
|
|
1250
|
+
beginTeamAccount(token);
|
|
1251
|
+
try {
|
|
1252
|
+
const [spacesData, requestData, incomingData, outgoingData, removalData] = await Promise.all([
|
|
1253
|
+
api("/v1/cloud/team/spaces", { token }), api("/v1/cloud/team/project-requests", { token }), api("/v1/cloud/team/invitations", { token }), api("/v1/cloud/team/invitations/outgoing", { token }), api("/v1/cloud/team/access-requests/outgoing", { token }),
|
|
1254
|
+
]);
|
|
1255
|
+
if (teamState.token !== token) return;
|
|
1256
|
+
teamState.spaces = teamRows(spacesData, ["spaces", "memberships", "items", "data"]); teamState.requests = teamRows(requestData, ["requests", "items", "data"]); teamState.incoming = teamRows(incomingData, ["invitations", "items", "data"]); teamState.outgoing = teamRows(outgoingData, ["invitations", "items", "data"]); teamState.removals = teamRows(removalData, ["requests", "items", "data"]);
|
|
1257
|
+
const members = await Promise.all(teamState.spaces.map(async (space) => {
|
|
1258
|
+
try { const result = await api(`/v1/cloud/team/spaces/${encodeURIComponent(space.space_key)}/members`, { token }); return [space.space_key, teamRows(result, ["members", "memberships", "items", "data"])]; }
|
|
1259
|
+
catch { return [space.space_key, null]; }
|
|
1260
|
+
}));
|
|
1261
|
+
if (teamState.token !== token) return;
|
|
1262
|
+
teamState.members = new Map(members);
|
|
1263
|
+
teamState.loaded = true;
|
|
1264
|
+
renderIncoming(); renderTeamProjects();
|
|
1265
|
+
openLinkedProjectDraft();
|
|
1266
|
+
clearTimeout(teamState.poll); const unsettled = teamState.requests.some((item) => item.state === "requested") || teamState.incoming.some((item) => item.state !== "ready" && !item.wrapped_key) || teamState.outgoing.some((item) => item.state !== "ready" && !item.wrapped_key) || teamState.removals.some((item) => item.state === "requested");
|
|
1267
|
+
if (unsettled) teamState.poll = setTimeout(() => { if (sessionStorage.getItem(TOKEN) === token) loadTeamAccess(token); }, 5000);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
if (teamState.token !== token) return;
|
|
1270
|
+
$("team-project-empty").style.display = "";
|
|
1271
|
+
$("team-project-empty").textContent = errText(error);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
$("team-project-new").addEventListener("click", () => openProjectDialog());
|
|
1275
|
+
$("team-project-cancel").addEventListener("click", () => $("team-project-dialog").close());
|
|
1276
|
+
$("team-project-form").addEventListener("submit", async (event) => {
|
|
1277
|
+
event.preventDefault(); const name = $("team-project-name").value.trim(); if (!name) return void say("team-project-msg", "Enter a project name.", "err");
|
|
1278
|
+
const matches = matchingTeamProjectsByName(name);
|
|
1279
|
+
if (matches.length === 1) {
|
|
1280
|
+
teamState.selected = matches[0].uiId;
|
|
1281
|
+
$("team-project-dialog").close();
|
|
1282
|
+
renderTeamProjects();
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (matches.length > 1) return void say("team-project-msg", "More than one older project uses this name. Choose the exact project from the list instead of creating another one.", "err");
|
|
1286
|
+
const button = $("team-project-create"); button.disabled = true; button.textContent = "Creating…";
|
|
1287
|
+
try {
|
|
1288
|
+
const created = await api("/v1/cloud/team/project-requests", { method: "POST", token: teamState.token, body: { project_name: name } });
|
|
1289
|
+
const row = teamRows(created, ["request", "item", "data"])[0] || created;
|
|
1290
|
+
teamState.requests = [...teamState.requests.filter((item) => item.id !== row.id), { project_name: name, state: "requested", ...row }];
|
|
1291
|
+
teamState.selected = `request:${row.id}`;
|
|
1292
|
+
$("team-project-dialog").close();
|
|
1293
|
+
renderTeamProjects();
|
|
1294
|
+
clearTimeout(teamState.poll); teamState.poll = setTimeout(() => loadTeamAccess(teamState.token), 1000);
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) { say("team-project-msg", errText(error), "err"); }
|
|
1297
|
+
finally { button.disabled = false; button.textContent = "Create project"; }
|
|
1298
|
+
});
|
|
1299
|
+
$("team-drawer-close").addEventListener("click", closeTeamDrawer);
|
|
1300
|
+
$("team-invite-email").addEventListener("keydown", (event) => {
|
|
1301
|
+
if (event.key !== "Enter") return;
|
|
1302
|
+
event.preventDefault();
|
|
1303
|
+
$("team-invite-send").click();
|
|
1304
|
+
});
|
|
1305
|
+
$("team-invite-send").addEventListener("click", async () => {
|
|
1306
|
+
const space = currentTeamProject(); const email = $("team-invite-email").value.trim(); if (!space?.space_key || space.role !== "owner" || !email) return void say("team-invite-msg", "Enter the SealKeep account email to invite.", "err");
|
|
1307
|
+
const button = $("team-invite-send"); button.disabled = true; button.textContent = "Sending…";
|
|
1308
|
+
try { await api(`/v1/cloud/team/spaces/${encodeURIComponent(space.space_key)}/invitations`, { method: "POST", token: teamState.token, body: { email } }); $("team-invite-email").value = ""; say("team-invite-msg", "Invitation created. Your machine will seal access automatically.", "ok"); await loadTeamAccess(teamState.token); }
|
|
1309
|
+
catch (error) { say("team-invite-msg", errText(error), "err"); }
|
|
1310
|
+
finally { button.disabled = false; button.textContent = "Send invitation"; }
|
|
1311
|
+
});
|
|
1312
|
+
async function loadAccount(token) {
|
|
1313
|
+
const generation = ++accountLoadGeneration;
|
|
1314
|
+
const isCurrent = () => accountLoadIsCurrent(token, generation);
|
|
1315
|
+
if (!isCurrent()) return null;
|
|
1316
|
+
let user;
|
|
1317
|
+
try { user = await api("/me", { token }); }
|
|
1318
|
+
catch {
|
|
1319
|
+
if (!isCurrent()) return null;
|
|
1320
|
+
sessionStorage.removeItem(TOKEN);
|
|
1321
|
+
sessionStorage.removeItem(OWNER_KEY);
|
|
1322
|
+
accountLoadGeneration += 1;
|
|
1323
|
+
resetTargetRoutingState("");
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
if (!isCurrent()) return null;
|
|
1327
|
+
if (targetAccountToken !== token) resetTargetRoutingState(token);
|
|
1328
|
+
beginTeamAccount(token);
|
|
1329
|
+
planIsPaid = user.plan === "pro" || user.plan === "team";
|
|
1330
|
+
liveCloudBytes = Number(user.used_bytes) || 0;
|
|
1331
|
+
teamState.canCreate = user.plan === "team";
|
|
1332
|
+
renderAccount(user);
|
|
1333
|
+
await loadDeviceConnect(token);
|
|
1334
|
+
if (!isCurrent()) return null;
|
|
1335
|
+
if (user.plan === "pro" || user.plan === "team") { $("a-count").textContent = "—"; loadStorage(token); }
|
|
1336
|
+
await loadTargets(token);
|
|
1337
|
+
if (!isCurrent()) return null;
|
|
1338
|
+
loadAudit(token);
|
|
1339
|
+
loadTeamAccess(token);
|
|
1340
|
+
const devices = await loadDevices(token);
|
|
1341
|
+
if (!isCurrent()) return null;
|
|
1342
|
+
$("a-machines").textContent = String(devices);
|
|
1343
|
+
$("a-dest").textContent = String(Math.max(targetRows.length, 1));
|
|
1344
|
+
const destNames = targetRows.slice()
|
|
1345
|
+
.sort((a, b) => (a.priority ?? 10) - (b.priority ?? 10))
|
|
1346
|
+
.map((t) => providerLabel(t.provider) + (t.maxGb ? " ≤" + t.maxGb + " GB" : ""));
|
|
1347
|
+
const hasDrive = targetRows.some((t) => t.provider === "gdrive");
|
|
1348
|
+
const ownBucketTargets = targetRows.filter((t) => t.provider !== "gdrive" && t.provider !== "vaultline");
|
|
1349
|
+
const paidPlan = planIsPaid;
|
|
1350
|
+
const parts = [];
|
|
1351
|
+
if (paidPlan) parts.push(bytes(Number(user.used_bytes) || 0) + " in Sealkeep cloud");
|
|
1352
|
+
if (hasDrive) {
|
|
1353
|
+
const driveHealth = targetConnectionSummary(targetRows.find((t) => t.provider === "gdrive"));
|
|
1354
|
+
parts.push(driveHealth.kind === "connected"
|
|
1355
|
+
? "Google Drive personal backup ready"
|
|
1356
|
+
: driveHealth.kind === "partial"
|
|
1357
|
+
? "Google Drive personal backup needs sign-in on some machines"
|
|
1358
|
+
: driveHealth.kind === "disconnected"
|
|
1359
|
+
? "Google Drive personal backup sign-in missing"
|
|
1360
|
+
: "Google Drive personal backup configured — machine sign-in not reported");
|
|
1361
|
+
}
|
|
1362
|
+
if (ownBucketTargets.length) {
|
|
1363
|
+
const bucketHealth = ownBucketTargets.map(targetConnectionSummary);
|
|
1364
|
+
const bucketLabel = ownBucketTargets.length === 1 ? "your bucket" : "your buckets";
|
|
1365
|
+
if (bucketHealth.every((health) => health.kind === "connected")) {
|
|
1366
|
+
parts.push(`${bucketLabel}: credentials saved`);
|
|
1367
|
+
} else if (bucketHealth.some((health) => health.kind === "disconnected" || health.kind === "partial")) {
|
|
1368
|
+
parts.push(`${bucketLabel} need${ownBucketTargets.length === 1 ? "s" : ""} credentials on some machines`);
|
|
1369
|
+
} else {
|
|
1370
|
+
parts.push(`${bucketLabel} configured — machine credentials not reported`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
$("a-hero").textContent = parts.length ? parts.join(" · ")
|
|
1374
|
+
: "Your archives live in storage you run";
|
|
1375
|
+
$("a-summary").textContent =
|
|
1376
|
+
(devices ? devices + " machine" + (devices === 1 ? "" : "s") + " enrolled" : "No machine enrolled yet")
|
|
1377
|
+
+ " · new archives go to "
|
|
1378
|
+
+ (destNames.length ? destNames.join(", then ")
|
|
1379
|
+
: paidPlan ? "Sealkeep Cloud" : "the bucket each machine is configured with");
|
|
1380
|
+
const nothingYet = devices === 0 && Number(user.used_bytes) === 0;
|
|
1381
|
+
on("a-firstrun", nothingYet);
|
|
1382
|
+
(nothingYet ? $("pair-home-first") : $("pair-home-more")).append($("pair"));
|
|
1383
|
+
$("pair-side").textContent = nothingYet ? "Your pairing code" : "Add another machine";
|
|
1384
|
+
if (devices === 0)
|
|
1385
|
+
$("d-msg").textContent = nothingYet
|
|
1386
|
+
? "No machines connected yet. Use the pairing code at the top of this page."
|
|
1387
|
+
: "No machines connected yet.";
|
|
1388
|
+
if (nothingYet && !pairShown) mintCode(true);
|
|
1389
|
+
loadEscrow(token, Number(user.used_bytes) > 0);
|
|
1390
|
+
showAccountView(requestedAccountView(), { writeHistory: false });
|
|
1391
|
+
sessionStorage.removeItem("sealkeep.returnView");
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
$("a-rename").addEventListener("click", async () => {
|
|
1395
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
1396
|
+
const label = $("a-name").value.trim();
|
|
1397
|
+
if (!label) return void ($("a-rename-msg").textContent = "Give the account a name.");
|
|
1398
|
+
$("a-rename").disabled = true;
|
|
1399
|
+
$("a-rename-msg").textContent = "Saving";
|
|
1400
|
+
try {
|
|
1401
|
+
const saved = await api("/account", { method: "PATCH", token, body: { label } });
|
|
1402
|
+
$("a-label").textContent = saved.label;
|
|
1403
|
+
$("a-rename-msg").textContent = "Saved";
|
|
1404
|
+
} catch (error) { $("a-rename-msg").textContent = errText(error); }
|
|
1405
|
+
finally {
|
|
1406
|
+
$("a-rename").disabled = false;
|
|
1407
|
+
setTimeout(() => { $("a-rename-msg").textContent = ""; }, 5000);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
$("g-in").prepend($("g-up").querySelector("svg").cloneNode(true));
|
|
1411
|
+
let lastPaid = false;
|
|
1412
|
+
function applyStorageView() {
|
|
1413
|
+
on("a-store-free", !lastPaid);
|
|
1414
|
+
on("a-store-paid", lastPaid);
|
|
1415
|
+
}
|
|
1416
|
+
let pairShown = false;
|
|
1417
|
+
function showCode(code, email) {
|
|
1418
|
+
const field = $("pair-code");
|
|
1419
|
+
field.textContent = code;
|
|
1420
|
+
field.dataset.long = code.length > 16 ? "yes" : "no";
|
|
1421
|
+
$("pair-cli").textContent = "npm install -g sealkeep\nsealkeep enroll " + code;
|
|
1422
|
+
$("pair-note").textContent = "Single use. On the new machine, run the command below — that is the whole setup.";
|
|
1423
|
+
on("pair-shown", true);
|
|
1424
|
+
on("pair-alt", true);
|
|
1425
|
+
on("pair-ask", false);
|
|
1426
|
+
on("pair-side", true);
|
|
1427
|
+
pairShown = true;
|
|
1428
|
+
}
|
|
1429
|
+
async function mintCode(auto) {
|
|
1430
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
1431
|
+
$("pair-go").disabled = true;
|
|
1432
|
+
$("pair-msg").textContent = auto ? "" : "Making a code";
|
|
1433
|
+
try {
|
|
1434
|
+
const made = await api("/account/cli-token", { method: "POST", token });
|
|
1435
|
+
showCode(made.token, made.email);
|
|
1436
|
+
$("pair-msg").textContent = "";
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
$("pair-msg").textContent = errText(error);
|
|
1439
|
+
on("pair-ask", true);
|
|
1440
|
+
} finally { $("pair-go").disabled = false; }
|
|
1441
|
+
}
|
|
1442
|
+
$("pair-go").addEventListener("click", () => mintCode(false));
|
|
1443
|
+
$("a-verify-go").addEventListener("click", async () => {
|
|
1444
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
1445
|
+
$("a-verify-go").disabled = true;
|
|
1446
|
+
$("a-verify-msg").textContent = "Sending";
|
|
1447
|
+
try {
|
|
1448
|
+
const result = await api("/account/send-confirmation", { method: "POST", token });
|
|
1449
|
+
$("a-verify-msg").textContent = result.sent
|
|
1450
|
+
? "Sent. Check your inbox."
|
|
1451
|
+
: "Email delivery is not switched on yet — nothing was sent.";
|
|
1452
|
+
} catch (error) { $("a-verify-msg").textContent = errText(error); }
|
|
1453
|
+
finally { $("a-verify-go").disabled = false; }
|
|
1454
|
+
});
|
|
1455
|
+
$("p-signup").addEventListener("submit", async (event) => {
|
|
1456
|
+
event.preventDefault();
|
|
1457
|
+
const email = $("su-email").value.trim(), label = $("su-label").value.trim(), password = $("su-pass").value;
|
|
1458
|
+
if (password.length < 12) return say("su-msg", "Use at least 12 characters.", "err");
|
|
1459
|
+
$("su-go").disabled = true; say("su-msg", "Creating your account");
|
|
1460
|
+
try {
|
|
1461
|
+
await api("/signup", { method: "POST", body: { email, password, label } });
|
|
1462
|
+
const { token, owner_key } = await api("/login", { method: "POST", body: { email, password } });
|
|
1463
|
+
sessionStorage.setItem(TOKEN, token);
|
|
1464
|
+
keepOwnerKey(owner_key);
|
|
1465
|
+
say("su-msg", "");
|
|
1466
|
+
await loadAccount(token);
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
say("su-msg", /exists|unique|duplicate/i.test(error.message)
|
|
1469
|
+
? "That email is already registered. Sign in instead."
|
|
1470
|
+
: errText(error), "err");
|
|
1471
|
+
} finally { $("su-go").disabled = false; }
|
|
1472
|
+
});
|
|
1473
|
+
$("p-signin").addEventListener("submit", async (event) => {
|
|
1474
|
+
event.preventDefault();
|
|
1475
|
+
$("si-go").disabled = true; say("si-msg", "Signing in");
|
|
1476
|
+
try {
|
|
1477
|
+
const { token, owner_key } = await api("/login", { method: "POST", body: { email: $("si-email").value.trim(), password: $("si-pass").value } });
|
|
1478
|
+
sessionStorage.setItem(TOKEN, token);
|
|
1479
|
+
keepOwnerKey(owner_key);
|
|
1480
|
+
say("si-msg", "");
|
|
1481
|
+
await loadAccount(token);
|
|
1482
|
+
} catch {
|
|
1483
|
+
say("si-msg", "Email or password is not correct.", "err");
|
|
1484
|
+
} finally { $("si-go").disabled = false; }
|
|
1485
|
+
});
|
|
1486
|
+
$("a-out").addEventListener("click", () => {
|
|
1487
|
+
accountLoadGeneration += 1;
|
|
1488
|
+
sessionStorage.removeItem(TOKEN);
|
|
1489
|
+
sessionStorage.removeItem(OWNER_KEY);
|
|
1490
|
+
sessionStorage.removeItem(PROJECT_DRAFT_NAME);
|
|
1491
|
+
beginTeamAccount("");
|
|
1492
|
+
resetTargetRoutingState("");
|
|
1493
|
+
showPane("signin");
|
|
1494
|
+
say("si-msg", "Signed out. Archiving on your connected machines carries on.");
|
|
1495
|
+
goAccount();
|
|
1496
|
+
});
|
|
1497
|
+
for (const [b, key] of [["t-signup", "signup"], ["t-signin", "signin"]])
|
|
1498
|
+
$(b).addEventListener("click", () => showPane(key));
|
|
1499
|
+
function openAuthFromHash() {
|
|
1500
|
+
const wanted = { "#account": "signin", "#signin": "signin", "#signup": "signup" }[location.hash];
|
|
1501
|
+
if (!wanted && !pendingConnectRequest()) return;
|
|
1502
|
+
if (sessionStorage.getItem(TOKEN)) { goAccount(); return; }
|
|
1503
|
+
on("connect-auth-intro", Boolean(pendingConnectRequest()));
|
|
1504
|
+
showPane(wanted || "signin");
|
|
1505
|
+
goAccount();
|
|
1506
|
+
}
|
|
1507
|
+
window.addEventListener("hashchange", openAuthFromHash);
|
|
1508
|
+
function googleAuthorizeState(url, responseState, baseUrl = location.href) {
|
|
1509
|
+
const parsed = new URL(url, baseUrl);
|
|
1510
|
+
const responseHasState = responseState !== undefined && responseState !== null;
|
|
1511
|
+
const urlStates = parsed.searchParams.getAll("state");
|
|
1512
|
+
const urlHasState = urlStates.length > 0;
|
|
1513
|
+
if (!responseHasState && !urlHasState) return { ok: true, state: null };
|
|
1514
|
+
const ok = typeof responseState === "string" && responseState.length > 0
|
|
1515
|
+
&& urlStates.length === 1 && urlStates[0] === responseState;
|
|
1516
|
+
return { ok, state: ok ? responseState : null };
|
|
1517
|
+
}
|
|
1518
|
+
function googleCallbackState(expectedState, returnedState, legacyStarted) {
|
|
1519
|
+
const expectedHasState = expectedState !== undefined && expectedState !== null;
|
|
1520
|
+
const returnedHasState = returnedState !== undefined && returnedState !== null;
|
|
1521
|
+
if (!expectedHasState && !returnedHasState) return { ok: legacyStarted === true, state: null };
|
|
1522
|
+
const ok = typeof expectedState === "string" && expectedState.length > 0
|
|
1523
|
+
&& typeof returnedState === "string" && returnedState.length > 0
|
|
1524
|
+
&& returnedState === expectedState;
|
|
1525
|
+
return { ok, state: ok ? returnedState : null };
|
|
1526
|
+
}
|
|
1527
|
+
async function google(msgId) {
|
|
1528
|
+
say(msgId, "Opening Google");
|
|
1529
|
+
try {
|
|
1530
|
+
const { url, state } = await api("/google-auth/authorize");
|
|
1531
|
+
if (!url) throw new Error("Google sign-in could not start safely. Try again.");
|
|
1532
|
+
const verified = googleAuthorizeState(url, state);
|
|
1533
|
+
if (!verified.ok) throw new Error("Google sign-in could not start safely. Try again.");
|
|
1534
|
+
if (verified.state === null) sessionStorage.removeItem(GOOGLE_OAUTH_STATE);
|
|
1535
|
+
else sessionStorage.setItem(GOOGLE_OAUTH_STATE, verified.state);
|
|
1536
|
+
sessionStorage.setItem("sealkeep.returning", "1");
|
|
1537
|
+
sessionStorage.setItem("sealkeep.returnView", requestedAccountView());
|
|
1538
|
+
location.assign(url);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
sessionStorage.removeItem(GOOGLE_OAUTH_STATE);
|
|
1541
|
+
say(msgId, /404|not found/i.test(error.message)
|
|
1542
|
+
? "Google sign-in is not switched on yet. Use a password for now."
|
|
1543
|
+
: errText(error), "err");
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
$("g-up").addEventListener("click", () => google("su-msg"));
|
|
1547
|
+
$("g-in").addEventListener("click", () => google("si-msg"));
|
|
1548
|
+
async function backToSignIn(message, bad) {
|
|
1549
|
+
await bootDone(false);
|
|
1550
|
+
showPane("signin");
|
|
1551
|
+
say("si-msg", message, bad ? "err" : "");
|
|
1552
|
+
goAccount();
|
|
1553
|
+
}
|
|
1554
|
+
async function finishGoogle(code, returnedState) {
|
|
1555
|
+
const returnView = requestedAccountView();
|
|
1556
|
+
history.replaceState(null, "", `${location.pathname}?view=${encodeURIComponent(returnView)}#account`);
|
|
1557
|
+
const expectedState = sessionStorage.getItem(GOOGLE_OAUTH_STATE);
|
|
1558
|
+
const legacyStarted = sessionStorage.getItem("sealkeep.returning") === "1";
|
|
1559
|
+
sessionStorage.removeItem(GOOGLE_OAUTH_STATE);
|
|
1560
|
+
sessionStorage.removeItem("sealkeep.returning");
|
|
1561
|
+
bootSay("Signing you in");
|
|
1562
|
+
const verified = googleCallbackState(expectedState, returnedState, legacyStarted);
|
|
1563
|
+
if (!verified.ok) {
|
|
1564
|
+
return void await backToSignIn("That Google sign-in could not be verified. Start again with the button below.", true);
|
|
1565
|
+
}
|
|
1566
|
+
try {
|
|
1567
|
+
const body = verified.state === null ? { code } : { code, state: verified.state };
|
|
1568
|
+
const { token, owner_key } = await api("/google-auth/callback", { method: "POST", body });
|
|
1569
|
+
sessionStorage.setItem(TOKEN, token);
|
|
1570
|
+
keepOwnerKey(owner_key);
|
|
1571
|
+
say("si-msg", "");
|
|
1572
|
+
const opened = await loadAccount(token);
|
|
1573
|
+
if (opened === true) return void await bootDone(true);
|
|
1574
|
+
if (opened === null) return;
|
|
1575
|
+
await backToSignIn("That sign-in did not finish. Try it again below.", true);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
await backToSignIn(/execution failed/i.test(error.message)
|
|
1578
|
+
? "That Google sign-in did not complete. Start again with the button below."
|
|
1579
|
+
: errText(error), true);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const choices = [...document.querySelectorAll(".choice")];
|
|
1583
|
+
const paintChoices = () => {
|
|
1584
|
+
for (const c of choices) c.dataset.on = c.querySelector("input[type=radio]").checked ? "yes" : "no";
|
|
1585
|
+
};
|
|
1586
|
+
for (const c of choices) c.querySelector("input[type=radio]").addEventListener("change", paintChoices);
|
|
1587
|
+
paintChoices();
|
|
1588
|
+
const PACKAGES = [
|
|
1589
|
+
{ id: "free", name: "Your own bucket", gb: 0, monthly: 0, managed: false, seats: 0, blurb: "Your provider, your region, your bill. We never hold the bytes." },
|
|
1590
|
+
{ id: "pro_lite", name: "Pro Lite", gb: 0, monthly: 2, managed: false, seats: 3, blurb: "Your storage, our safety net: a sealed copy of your key in escrow, and every machine signed in. We still hold no bytes." },
|
|
1591
|
+
{ id: "gb100", name: "100 GB", gb: 100, monthly: 6, managed: true, seats: 10, blurb: "Years of transcripts from one machine." },
|
|
1592
|
+
{ id: "gb500", name: "500 GB", gb: 500, monthly: 12, managed: true, seats: 10, blurb: "A whole team's history." },
|
|
1593
|
+
{ id: "gb2000", name: "2 TB", gb: 2000, monthly: 30, managed: true, seats: 10, blurb: "Every agent on every machine, kept indefinitely." },
|
|
1594
|
+
{ id: "team", name: "Team", gb: 0, monthly: 9, perSeat: true, managed: false, blurb: "Per seat. Project-scoped members, live bridges relayed through our plane, revocation across every machine. Storage priced separately. Personal backups can use your own bucket; shared project memory uses Sealkeep Cloud." }
|
|
1595
|
+
];
|
|
1596
|
+
const packageIdFor = (user) => {
|
|
1597
|
+
if (user.plan === "team") return "team";
|
|
1598
|
+
if (user.plan === "pro_lite") return "pro_lite";
|
|
1599
|
+
if (user.plan !== "pro") return "free";
|
|
1600
|
+
const gb = Math.round((Number(user.quota_bytes) || 0) / 1073741824);
|
|
1601
|
+
const match = PACKAGES.find((p) => p.managed && p.gb === gb);
|
|
1602
|
+
return match ? match.id : "custom";
|
|
1603
|
+
};
|
|
1604
|
+
const packsFrom = (user) => {
|
|
1605
|
+
const current = packageIdFor(user);
|
|
1606
|
+
const cur = PACKAGES.find((p) => p.id === current);
|
|
1607
|
+
const seats = Number(user.seats) || 0;
|
|
1608
|
+
$("a-pack-current").textContent = cur
|
|
1609
|
+
? (cur.perSeat
|
|
1610
|
+
? `${cur.name} — $${cur.monthly} per seat${seats > 0 ? ` · ${seats} seat${seats === 1 ? "" : "s"}` : ""}`
|
|
1611
|
+
: cur.managed ? `${cur.name} — $${cur.monthly} a month` : `${cur.name} — free`)
|
|
1612
|
+
: `Custom allocation`;
|
|
1613
|
+
$("a-pack-context").textContent = cur ? cur.blurb : "";
|
|
1614
|
+
const host = $("a-packs");
|
|
1615
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
1616
|
+
for (const pack of PACKAGES) {
|
|
1617
|
+
const chip = document.createElement(pack.id === current ? "span" : "a");
|
|
1618
|
+
chip.className = "packchip" + (pack.id === current ? " now" : "");
|
|
1619
|
+
const rate = pack.perSeat ? `$${pack.monthly} a seat, monthly` : pack.monthly ? `$${pack.monthly} a month` : "free";
|
|
1620
|
+
if (pack.id !== current) {
|
|
1621
|
+
chip.href = "mailto:hello@spala.ai?subject=" + encodeURIComponent(`Move me to ${pack.name}`)
|
|
1622
|
+
+ "&body=" + encodeURIComponent(`Please move my account to ${pack.name} (${rate}).`);
|
|
1623
|
+
chip.title = `Ask us to move you to ${pack.name}`;
|
|
1624
|
+
}
|
|
1625
|
+
const name = document.createElement("b"); name.textContent = pack.name;
|
|
1626
|
+
const price = document.createElement("span");
|
|
1627
|
+
price.textContent = pack.perSeat ? `$${pack.monthly}/seat` : pack.monthly ? `$${pack.monthly}/mo` : "$0";
|
|
1628
|
+
chip.append(name, price);
|
|
1629
|
+
if (pack.id === current) {
|
|
1630
|
+
const tag = document.createElement("i"); tag.textContent = "your package"; chip.append(tag);
|
|
1631
|
+
}
|
|
1632
|
+
host.append(chip);
|
|
1633
|
+
}
|
|
1634
|
+
if (current === "custom") {
|
|
1635
|
+
const gb = Math.round((Number(user.quota_bytes) || 0) / 1073741824);
|
|
1636
|
+
$("a-pack-note").textContent = `You are on a custom allocation of ${gb} GB, not one of the standard packages.`;
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
const rulesFrom = (user) => {
|
|
1640
|
+
const mode = user.sync_mode || "all";
|
|
1641
|
+
const radio = document.querySelector(`input[name=sync][value="${mode}"]`) || choices[0].querySelector("input");
|
|
1642
|
+
radio.checked = true;
|
|
1643
|
+
if (user.sync_keep_last) $("r-last").value = user.sync_keep_last;
|
|
1644
|
+
if (user.sync_min_age_days) $("r-age").value = user.sync_min_age_days;
|
|
1645
|
+
paintChoices();
|
|
1646
|
+
};
|
|
1647
|
+
$("r-save").addEventListener("click", async () => {
|
|
1648
|
+
const token = sessionStorage.getItem(TOKEN);
|
|
1649
|
+
const mode = document.querySelector("input[name=sync]:checked").value;
|
|
1650
|
+
$("r-save").disabled = true;
|
|
1651
|
+
$("r-msg").textContent = "Saving";
|
|
1652
|
+
try {
|
|
1653
|
+
await api("/v1/cloud/sync-rules", {
|
|
1654
|
+
method: "POST", token,
|
|
1655
|
+
body: {
|
|
1656
|
+
sync_mode: mode,
|
|
1657
|
+
sync_keep_last: Number($("r-last").value) || 20,
|
|
1658
|
+
sync_min_age_days: Number($("r-age").value) || 7
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
$("r-msg").textContent = "Saved to your account.";
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
$("r-msg").textContent = /404|not found/i.test(error.message)
|
|
1664
|
+
? "Rules are not switched on yet — nothing was changed."
|
|
1665
|
+
: errText(error);
|
|
1666
|
+
} finally {
|
|
1667
|
+
$("r-save").disabled = false;
|
|
1668
|
+
setTimeout(() => { $("r-msg").textContent = ""; }, 6000);
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
async function finishConfirm(accountId, token) {
|
|
1672
|
+
history.replaceState(null, "", location.pathname + "#account");
|
|
1673
|
+
const signedIn = sessionStorage.getItem(TOKEN);
|
|
1674
|
+
try {
|
|
1675
|
+
await api("/confirm-email", { method: "POST", body: { account_id: accountId, token } });
|
|
1676
|
+
const opened = signedIn ? await loadAccount(signedIn) : false;
|
|
1677
|
+
if (opened === true) {
|
|
1678
|
+
await bootDone(returning);
|
|
1679
|
+
$("a-verify-msg").textContent = "Email confirmed.";
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (opened === null) return;
|
|
1683
|
+
await backToSignIn("Email confirmed. Sign in to continue.");
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
const opened = signedIn ? await loadAccount(signedIn) : false;
|
|
1686
|
+
if (opened === true) {
|
|
1687
|
+
await bootDone(returning);
|
|
1688
|
+
$("a-verify-msg").textContent = errText(error);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (opened === null) return;
|
|
1692
|
+
await backToSignIn(errText(error), true);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
(async function start() {
|
|
1696
|
+
if (authCode) return void finishGoogle(authCode, authState);
|
|
1697
|
+
if (confirmToken && confirmAccount) return void finishConfirm(confirmAccount, confirmToken);
|
|
1698
|
+
if (savedToken) {
|
|
1699
|
+
const opened = await loadAccount(savedToken);
|
|
1700
|
+
if (opened === null) return;
|
|
1701
|
+
await bootDone(returning && opened);
|
|
1702
|
+
sessionStorage.removeItem("sealkeep.returning");
|
|
1703
|
+
if (!opened) return void await backToSignIn("That session has expired. Sign in again.");
|
|
1704
|
+
if (location.hash === "#account") requestAnimationFrame(goAccount);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
await bootDone(false);
|
|
1708
|
+
if (location.hash === "#account") requestAnimationFrame(goAccount);
|
|
1709
|
+
requestAnimationFrame(openAuthFromHash);
|
|
1710
|
+
})();
|
|
1711
|
+
</script><script>
|
|
1712
|
+
// Product / Enterprise / Docs / Account share one document. #account keeps
|
|
1713
|
+
// its own long-standing flow; this only decides which surface is on screen,
|
|
1714
|
+
// fits the enterprise story, and lights the matching tab.
|
|
1715
|
+
(function () {
|
|
1716
|
+
const pills = { home: document.getElementById("pill-home"), enterprise: document.getElementById("pill-enterprise"), docs: document.getElementById("pill-docs"), account: document.getElementById("pill-account") };
|
|
1717
|
+
const enterpriseFrame = document.getElementById("enterprise-story");
|
|
1718
|
+
let enterpriseWasActive = false;
|
|
1719
|
+
const light = (which) => {
|
|
1720
|
+
for (const [key, pill] of Object.entries(pills)) pill.classList.toggle("on", key === which);
|
|
1721
|
+
};
|
|
1722
|
+
const controlEnterpriseStory = (active, reset = false) => {
|
|
1723
|
+
if (!enterpriseFrame?.contentWindow) return;
|
|
1724
|
+
enterpriseFrame.contentWindow.postMessage({ type: "sealkeep-story-control", active, reset }, location.origin);
|
|
1725
|
+
};
|
|
1726
|
+
const syncEnterpriseStory = () => {
|
|
1727
|
+
const active = document.body.classList.contains("enterprise-page");
|
|
1728
|
+
controlEnterpriseStory(active, false);
|
|
1729
|
+
};
|
|
1730
|
+
const enterpriseStoryReady = () => {
|
|
1731
|
+
enterpriseFrame.parentElement.classList.add("is-ready");
|
|
1732
|
+
syncEnterpriseStory();
|
|
1733
|
+
};
|
|
1734
|
+
if (enterpriseFrame) {
|
|
1735
|
+
enterpriseFrame.addEventListener("load", syncEnterpriseStory);
|
|
1736
|
+
addEventListener("message", (event) => {
|
|
1737
|
+
if (event.source !== enterpriseFrame.contentWindow || event.origin !== location.origin) return;
|
|
1738
|
+
if (event.data?.type === "sealkeep-story-ready") enterpriseStoryReady();
|
|
1739
|
+
if (event.data?.type === "sealkeep-story-size" && event.data.mode === "enterprise") {
|
|
1740
|
+
const height = Number(event.data.height);
|
|
1741
|
+
if (!Number.isFinite(height) || height < 200 || height > 10000) return;
|
|
1742
|
+
enterpriseFrame.style.height = `${Math.ceil(height)}px`;
|
|
1743
|
+
enterpriseFrame.parentElement.style.height = `${Math.ceil(height)}px`;
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
try {
|
|
1747
|
+
if (enterpriseFrame.contentDocument?.readyState === "complete") queueMicrotask(syncEnterpriseStory);
|
|
1748
|
+
} catch {}
|
|
1749
|
+
}
|
|
1750
|
+
const fitEnterpriseStory = () => {
|
|
1751
|
+
if (!enterpriseFrame || !document.body.classList.contains("enterprise-page")) return;
|
|
1752
|
+
const holder = enterpriseFrame.parentElement;
|
|
1753
|
+
const available = holder.clientWidth;
|
|
1754
|
+
if (!available) return;
|
|
1755
|
+
enterpriseFrame.style.width = "100%";
|
|
1756
|
+
enterpriseFrame.style.transform = "none";
|
|
1757
|
+
};
|
|
1758
|
+
const route = () => {
|
|
1759
|
+
const h = location.hash;
|
|
1760
|
+
const panel = document.body.classList.contains("panel");
|
|
1761
|
+
const docs = !panel && (h === "#docs" || h.startsWith("#docs-"));
|
|
1762
|
+
const enterprise = !panel && h === "#enterprise";
|
|
1763
|
+
const account = !panel && (h === "#account" || h === "#signin" || h === "#signup" || h.startsWith("#connect="));
|
|
1764
|
+
document.body.classList.toggle("docs", docs);
|
|
1765
|
+
document.body.classList.toggle("enterprise-page", enterprise);
|
|
1766
|
+
document.body.classList.toggle("account-page", account);
|
|
1767
|
+
document.title = enterprise ? "Sealkeep Enterprise — keep the work behind the code" : docs ? "Sealkeep Docs — setup and command reference" : account || panel ? "Your account — Sealkeep" : "Sealkeep — your agent history, sealed";
|
|
1768
|
+
if (!panel && ["#home", "#enterprise", "#docs", "#account", "#signin", "#signup"].includes(h)) {
|
|
1769
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.scrollTo({top:0,behavior:"instant"})));
|
|
1770
|
+
}
|
|
1771
|
+
const enteringEnterprise = enterprise && !enterpriseWasActive;
|
|
1772
|
+
enterpriseWasActive = enterprise;
|
|
1773
|
+
controlEnterpriseStory(enterprise, enteringEnterprise);
|
|
1774
|
+
if (panel) { light("account"); return; }
|
|
1775
|
+
if (enterprise) { window.scrollTo(0, 0); light("enterprise"); requestAnimationFrame(fitEnterpriseStory); return; }
|
|
1776
|
+
if (docs) {
|
|
1777
|
+
light("docs");
|
|
1778
|
+
requestAnimationFrame(() => {
|
|
1779
|
+
if (h === "#docs") window.scrollTo({top:0,behavior:"instant"});
|
|
1780
|
+
else document.getElementById(h.slice(1))?.scrollIntoView({behavior:"instant",block:"start"});
|
|
1781
|
+
});
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (account) { window.scrollTo(0, 0); light("account"); return; }
|
|
1785
|
+
light("home");
|
|
1786
|
+
};
|
|
1787
|
+
addEventListener("hashchange", route);
|
|
1788
|
+
addEventListener("load", route, {once:true});
|
|
1789
|
+
addEventListener("resize", fitEnterpriseStory);
|
|
1790
|
+
// The panel class flips outside any hash change (sign-in, sign-out), so
|
|
1791
|
+
// watch it rather than duplicating that logic here.
|
|
1792
|
+
new MutationObserver(route).observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
|
1793
|
+
for (const link of document.querySelectorAll('.navbar a[href="#home"],.navbar a[href="#how"],.navbar a[href="#get"]')) {
|
|
1794
|
+
link.addEventListener("click", (event) => {
|
|
1795
|
+
event.preventDefault();
|
|
1796
|
+
const hash = link.getAttribute("href");
|
|
1797
|
+
if (location.hash !== hash) history.pushState(null, "", hash);
|
|
1798
|
+
route();
|
|
1799
|
+
// Dedicated surfaces hide Product sections. Let routing expose the
|
|
1800
|
+
// target before measuring it, then perform the anchor movement.
|
|
1801
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1802
|
+
if (hash === "#home") window.scrollTo(0, 0);
|
|
1803
|
+
else document.querySelector(hash)?.scrollIntoView({ behavior: motionOK() ? "smooth" : "auto", block: "start" });
|
|
1804
|
+
}));
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
route();
|
|
1808
|
+
})();
|
|
1905
1809
|
</script>
|