sealkeep 0.9.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +252 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +1 -0
  16. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
  17. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  18. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  20. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  21. package/dist/site/index.html +1808 -1904
  22. package/dist/site/llms.txt +67 -0
  23. package/dist/site/trust/architecture-data-flow.html +53 -0
  24. package/dist/site/trust/audit-roadmap.html +37 -0
  25. package/dist/site/trust/deployment-responsibility.html +11 -0
  26. package/dist/site/trust/dpa-sample.html +30 -0
  27. package/dist/site/trust/release-provenance.html +21 -0
  28. package/dist/site/trust/subprocessors.html +15 -0
  29. package/dist/site/trust/threat-model.html +34 -0
  30. package/dist/site/trust/toms.html +41 -0
  31. package/dist/site/trust-document.css +32 -0
  32. package/dist/site/trust.html +73 -0
  33. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  34. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  35. package/dist/site/visual/index.html +18 -0
  36. package/dist/site.zip +0 -0
  37. package/dist/src/activity.d.ts +9 -0
  38. package/dist/src/activity.js +90 -1
  39. package/dist/src/adapters.d.ts +175 -5
  40. package/dist/src/adapters.js +961 -73
  41. package/dist/src/agent-context.d.ts +135 -0
  42. package/dist/src/agent-context.js +1059 -0
  43. package/dist/src/archive-copies.d.ts +47 -0
  44. package/dist/src/archive-copies.js +179 -0
  45. package/dist/src/audit.d.ts +1 -1
  46. package/dist/src/audit.js +29 -4
  47. package/dist/src/autopilot.d.ts +52 -7
  48. package/dist/src/autopilot.js +143 -25
  49. package/dist/src/background-bandwidth.d.ts +46 -0
  50. package/dist/src/background-bandwidth.js +301 -0
  51. package/dist/src/background-cpu.d.ts +82 -0
  52. package/dist/src/background-cpu.js +212 -0
  53. package/dist/src/background-worker-error.d.ts +12 -0
  54. package/dist/src/background-worker-error.js +18 -0
  55. package/dist/src/branding.d.ts +24 -1
  56. package/dist/src/branding.js +31 -1
  57. package/dist/src/bridge.d.ts +233 -0
  58. package/dist/src/bridge.js +604 -0
  59. package/dist/src/byte-stream.d.ts +91 -0
  60. package/dist/src/byte-stream.js +385 -0
  61. package/dist/src/chunk-store.d.ts +41 -8
  62. package/dist/src/chunk-store.js +161 -65
  63. package/dist/src/cli.js +1698 -163
  64. package/dist/src/cloud.d.ts +841 -31
  65. package/dist/src/cloud.js +3196 -277
  66. package/dist/src/context-background.d.ts +37 -0
  67. package/dist/src/context-background.js +309 -0
  68. package/dist/src/context-drain-child.d.ts +1 -0
  69. package/dist/src/context-drain-child.js +98 -0
  70. package/dist/src/context-reader.d.ts +118 -0
  71. package/dist/src/context-reader.js +447 -0
  72. package/dist/src/control-plane/auth.d.ts +32 -4
  73. package/dist/src/control-plane/auth.js +85 -24
  74. package/dist/src/control-plane/server.js +19 -6
  75. package/dist/src/control-plane.d.ts +17 -1
  76. package/dist/src/control-plane.js +32 -6
  77. package/dist/src/crypto.d.ts +1 -1
  78. package/dist/src/crypto.js +5 -5
  79. package/dist/src/daemon-lease.d.ts +70 -0
  80. package/dist/src/daemon-lease.js +420 -0
  81. package/dist/src/daemon.d.ts +94 -1
  82. package/dist/src/daemon.js +1082 -105
  83. package/dist/src/darwin-service-policy.d.ts +41 -0
  84. package/dist/src/darwin-service-policy.js +60 -0
  85. package/dist/src/dashboard-cli.js +15 -15
  86. package/dist/src/device-authorization.d.ts +37 -0
  87. package/dist/src/device-authorization.js +199 -0
  88. package/dist/src/device-enrollment.d.ts +91 -0
  89. package/dist/src/device-enrollment.js +349 -0
  90. package/dist/src/disk.d.ts +17 -12
  91. package/dist/src/disk.js +43 -17
  92. package/dist/src/doctor.d.ts +35 -1
  93. package/dist/src/doctor.js +332 -41
  94. package/dist/src/durable-ticket-lock.d.ts +24 -0
  95. package/dist/src/durable-ticket-lock.js +232 -0
  96. package/dist/src/enroll.d.ts +1 -1
  97. package/dist/src/enroll.js +13 -7
  98. package/dist/src/env.d.ts +10 -1
  99. package/dist/src/env.js +11 -3
  100. package/dist/src/errors.d.ts +8 -8
  101. package/dist/src/errors.js +6 -6
  102. package/dist/src/flush.d.ts +12 -0
  103. package/dist/src/flush.js +37 -0
  104. package/dist/src/heartbeat.d.ts +86 -12
  105. package/dist/src/heartbeat.js +415 -29
  106. package/dist/src/index-background-watchdog.d.ts +1 -0
  107. package/dist/src/index-background-watchdog.js +94 -0
  108. package/dist/src/index-background-work.d.ts +21 -0
  109. package/dist/src/index-background-work.js +25 -0
  110. package/dist/src/index-background.d.ts +64 -0
  111. package/dist/src/index-background.js +394 -0
  112. package/dist/src/index-build-child.d.ts +1 -0
  113. package/dist/src/index-build-child.js +109 -0
  114. package/dist/src/index-manifest.d.ts +52 -0
  115. package/dist/src/index-manifest.js +444 -0
  116. package/dist/src/index-publication-proof.d.ts +84 -0
  117. package/dist/src/index-publication-proof.js +380 -0
  118. package/dist/src/index-publication-state.d.ts +149 -0
  119. package/dist/src/index-publication-state.js +696 -0
  120. package/dist/src/index-publication-verifier.d.ts +89 -0
  121. package/dist/src/index-publication-verifier.js +341 -0
  122. package/dist/src/index-publish.d.ts +62 -0
  123. package/dist/src/index-publish.js +540 -0
  124. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  125. package/dist/src/index-scratch-cleanup.js +166 -0
  126. package/dist/src/index-segment-types.d.ts +132 -0
  127. package/dist/src/index-segment-types.js +21 -0
  128. package/dist/src/index-segments.d.ts +9 -0
  129. package/dist/src/index-segments.js +516 -0
  130. package/dist/src/index-store.d.ts +123 -0
  131. package/dist/src/index-store.js +495 -0
  132. package/dist/src/index-sync.d.ts +91 -15
  133. package/dist/src/index-sync.js +286 -53
  134. package/dist/src/index-upgrade-publication.d.ts +30 -0
  135. package/dist/src/index-upgrade-publication.js +179 -0
  136. package/dist/src/integration-manager.d.ts +32 -0
  137. package/dist/src/integration-manager.js +394 -0
  138. package/dist/src/leakscan.js +1 -1
  139. package/dist/src/local-api.d.ts +56 -15
  140. package/dist/src/local-api.js +4979 -541
  141. package/dist/src/machine-settings.d.ts +51 -0
  142. package/dist/src/machine-settings.js +166 -0
  143. package/dist/src/managed-chunks.d.ts +5 -2
  144. package/dist/src/managed-chunks.js +14 -14
  145. package/dist/src/mcp-install.d.ts +11 -9
  146. package/dist/src/mcp-install.js +73 -28
  147. package/dist/src/mcp-workspace.d.ts +18 -0
  148. package/dist/src/mcp-workspace.js +50 -0
  149. package/dist/src/mcp.js +294 -25
  150. package/dist/src/migrate.js +27 -21
  151. package/dist/src/notify.d.ts +1 -1
  152. package/dist/src/notify.js +5 -5
  153. package/dist/src/offload.d.ts +201 -14
  154. package/dist/src/offload.js +1848 -140
  155. package/dist/src/onboarding.d.ts +8 -1
  156. package/dist/src/onboarding.js +4 -4
  157. package/dist/src/packages.d.ts +2 -2
  158. package/dist/src/packages.js +10 -2
  159. package/dist/src/passkey.d.ts +0 -1
  160. package/dist/src/passkey.js +2 -7
  161. package/dist/src/password-lock.d.ts +2 -2
  162. package/dist/src/password-lock.js +6 -6
  163. package/dist/src/paths.d.ts +2 -0
  164. package/dist/src/paths.js +2 -0
  165. package/dist/src/presence.d.ts +86 -0
  166. package/dist/src/presence.js +240 -0
  167. package/dist/src/progress-deadline.d.ts +21 -0
  168. package/dist/src/progress-deadline.js +91 -0
  169. package/dist/src/project-repair.d.ts +55 -0
  170. package/dist/src/project-repair.js +131 -0
  171. package/dist/src/providers/gcs.d.ts +28 -7
  172. package/dist/src/providers/gcs.js +35 -24
  173. package/dist/src/providers/gdrive.d.ts +71 -8
  174. package/dist/src/providers/gdrive.js +223 -50
  175. package/dist/src/providers/index.d.ts +11 -3
  176. package/dist/src/providers/index.js +30 -10
  177. package/dist/src/providers/s3.d.ts +30 -8
  178. package/dist/src/providers/s3.js +41 -30
  179. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  180. package/dist/src/providers/safe-storage-fetch.js +72 -0
  181. package/dist/src/queue.d.ts +136 -19
  182. package/dist/src/queue.js +862 -96
  183. package/dist/src/reclaim-transaction.d.ts +156 -0
  184. package/dist/src/reclaim-transaction.js +1027 -0
  185. package/dist/src/recovery-codes.d.ts +32 -0
  186. package/dist/src/recovery-codes.js +338 -0
  187. package/dist/src/recovery.js +12 -9
  188. package/dist/src/rehydrate.d.ts +25 -22
  189. package/dist/src/rehydrate.js +319 -23
  190. package/dist/src/restore.d.ts +57 -4
  191. package/dist/src/restore.js +272 -36
  192. package/dist/src/resume-stub.d.ts +92 -0
  193. package/dist/src/resume-stub.js +417 -0
  194. package/dist/src/retention.d.ts +98 -7
  195. package/dist/src/retention.js +1066 -62
  196. package/dist/src/rotate.js +3 -3
  197. package/dist/src/search.d.ts +566 -8
  198. package/dist/src/search.js +5940 -297
  199. package/dist/src/secrets.d.ts +51 -7
  200. package/dist/src/secrets.js +316 -24
  201. package/dist/src/service.d.ts +49 -11
  202. package/dist/src/service.js +776 -35
  203. package/dist/src/share.js +3 -3
  204. package/dist/src/shared-spaces.d.ts +98 -0
  205. package/dist/src/shared-spaces.js +214 -0
  206. package/dist/src/source-reader.d.ts +73 -0
  207. package/dist/src/source-reader.js +715 -0
  208. package/dist/src/spool.d.ts +1 -1
  209. package/dist/src/spool.js +1 -1
  210. package/dist/src/start-tui.js +2 -1
  211. package/dist/src/start.js +2 -2
  212. package/dist/src/storage-endpoint.d.ts +21 -0
  213. package/dist/src/storage-endpoint.js +122 -0
  214. package/dist/src/storage-setup.js +12 -12
  215. package/dist/src/storage-targets.d.ts +109 -6
  216. package/dist/src/storage-targets.js +975 -67
  217. package/dist/src/stream-to-cloud.d.ts +5 -1
  218. package/dist/src/stream-to-cloud.js +34 -14
  219. package/dist/src/sync-rules.d.ts +31 -6
  220. package/dist/src/sync-rules.js +153 -14
  221. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  222. package/dist/src/team-backfill-scheduling.js +33 -0
  223. package/dist/src/team-backfill.d.ts +116 -0
  224. package/dist/src/team-backfill.js +1429 -0
  225. package/dist/src/team-index-cache.d.ts +16 -0
  226. package/dist/src/team-index-cache.js +152 -0
  227. package/dist/src/team-offboarding.d.ts +38 -0
  228. package/dist/src/team-offboarding.js +1043 -0
  229. package/dist/src/team-presence.d.ts +127 -0
  230. package/dist/src/team-presence.js +904 -0
  231. package/dist/src/team-publication-policy.d.ts +20 -0
  232. package/dist/src/team-publication-policy.js +140 -0
  233. package/dist/src/team-realtime.d.ts +68 -0
  234. package/dist/src/team-realtime.js +816 -0
  235. package/dist/src/team-source-facts-cache.d.ts +23 -0
  236. package/dist/src/team-source-facts-cache.js +255 -0
  237. package/dist/src/trash.d.ts +1 -1
  238. package/dist/src/trash.js +2 -2
  239. package/dist/src/tui.js +11 -12
  240. package/dist/src/types.d.ts +173 -7
  241. package/dist/src/types.js +20 -0
  242. package/dist/src/ui-server.d.ts +163 -35
  243. package/dist/src/ui-server.js +712 -72
  244. package/dist/src/ui.d.ts +1 -2
  245. package/dist/src/ui.js +1 -2
  246. package/dist/src/upload.d.ts +27 -0
  247. package/dist/src/upload.js +383 -43
  248. package/dist/src/vault.d.ts +226 -30
  249. package/dist/src/vault.js +1776 -192
  250. package/dist/src/watcher.d.ts +7 -1
  251. package/dist/src/watcher.js +198 -55
  252. package/dist/src/worker.d.ts +27 -3
  253. package/dist/src/worker.js +274 -55
  254. package/package.json +33 -12
  255. package/scripts/native-reboot-rehearsal.mjs +90 -0
  256. package/web/app.js +6032 -343
  257. package/web/bootstrap.js +17 -0
  258. package/web/index.html +255 -57
  259. package/web/rail.js +317 -40
  260. package/web/retention.html +2 -2
  261. package/web/rules-view.js +188 -16
  262. package/web/sessions-view.js +485 -62
  263. package/web/sessions.html +2 -2
  264. package/web/setup-api.js +152 -29
  265. package/web/setup-logic.js +68 -9
  266. package/web/setup.html +113 -44
  267. package/web/setup.js +604 -71
  268. package/web/style.css +513 -98
  269. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,23 @@
1
+ import type { ContentIndex } from "./search.js";
2
+ import type { TeamIndexCacheIdentity } from "./team-index-cache.js";
3
+ /** Derived facts, never authority: callers still fetch the current native head and grants. */
4
+ export type TeamSourceFactsCacheIdentity = TeamIndexCacheIdentity & {
5
+ project: string;
6
+ projectKey?: string;
7
+ };
8
+ export declare const TEAM_SOURCE_FACTS_MAX_BYTES: number;
9
+ export declare const TEAM_SOURCE_FACTS_CACHE_BYTES: number;
10
+ export declare const TEAM_SOURCE_FACTS_MAX_ENTRIES = 32;
11
+ export declare function readTeamSourceFactsCache(dataDir: string, identity: TeamSourceFactsCacheIdentity, privateRaw: Buffer, options?: {
12
+ maxDecodedBytes?: number;
13
+ signal?: AbortSignal;
14
+ onDecoded?: (bytes: number) => void;
15
+ }): Promise<{
16
+ index: ContentIndex;
17
+ decodedBytes: number;
18
+ } | null>;
19
+ /** Call only after the checkpoint's AEAD, original hash and coverage verifier succeeds,
20
+ * and before filtering grants. Grant permissions are neither cached nor inferred. */
21
+ export declare function writeTeamSourceFactsCache(dataDir: string, identity: TeamSourceFactsCacheIdentity, privateRaw: Buffer, verifiedIndex: ContentIndex, options?: {
22
+ signal?: AbortSignal;
23
+ }): Promise<void>;
@@ -0,0 +1,255 @@
1
+ import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { mkdir, open, readdir, rename, stat, unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ export const TEAM_SOURCE_FACTS_MAX_BYTES = 2 * 1024 * 1024;
6
+ export const TEAM_SOURCE_FACTS_CACHE_BYTES = 64 * 1024 * 1024;
7
+ export const TEAM_SOURCE_FACTS_MAX_ENTRIES = 32;
8
+ const MAX_ARCHIVES = 10_000;
9
+ const HEADER_BYTES = 32; // magic, random 96-bit nonce, 128-bit GCM tag
10
+ const MAGIC = Buffer.from("SKF1");
11
+ const NAME = /^[a-f0-9]{64}\.facts$/;
12
+ const PARTIAL = /^\.(\d+)\.[a-f0-9-]{36}\.partial$/;
13
+ const activePartials = new Set();
14
+ const directory = (dataDir) => join(dataDir, "index", "team-source-facts-cache");
15
+ const hash = (value) => createHash("sha256").update(value).digest();
16
+ const text = (value, max = 8192) => typeof value === "string" && value.length <= max;
17
+ const sha = (value) => typeof value === "string" && /^[a-f0-9]{64}$/i.test(value);
18
+ const integer = (value, min = 0) => Number.isSafeInteger(value) && Number(value) >= min;
19
+ function scope(identity) {
20
+ const r = identity.receipt;
21
+ const strings = [identity.origin, identity.vaultId, identity.spaceKey, identity.project, identity.projectKey ?? "",
22
+ r.generationId, r.committedAt, r.meta.objectKey, r.blob.objectKey];
23
+ if (strings.some((value) => !text(value)) || !integer(identity.accountId, 1) || !integer(r.revision, 1)
24
+ || !integer(r.membershipVersion, 1) || !integer(r.meta.bytes, 1) || !integer(r.blob.bytes, 1)
25
+ || !sha(r.meta.sha256) || !sha(r.blob.sha256))
26
+ return null;
27
+ const coverage = Object.entries(r.coverageJson);
28
+ if (coverage.length > MAX_ARCHIVES || coverage.some(([id, digest]) => !text(id, 512) || !sha(digest)))
29
+ return null;
30
+ let coverageBytes = 0;
31
+ for (const entry of coverage) {
32
+ coverageBytes += Buffer.byteLength(JSON.stringify(entry)) + 1;
33
+ if (coverageBytes > TEAM_SOURCE_FACTS_MAX_BYTES)
34
+ return null;
35
+ }
36
+ coverage.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
37
+ return hash(JSON.stringify(["sealkeep-team-source-facts-v1", ...strings, identity.accountId,
38
+ r.revision, r.membershipVersion, r.meta.bytes, r.meta.sha256, r.blob.bytes, r.blob.sha256, coverage]));
39
+ }
40
+ function copyFacts(value) {
41
+ if (!value || typeof value !== "object")
42
+ return null;
43
+ const f = value;
44
+ if (f.v !== 1 || !text(f.createdAt, 128) || !text(f.algorithm, 128) || !integer(f.sourceBytes)
45
+ || !integer(f.storedBytes, 1) || !integer(f.chunks, 1) || !sha(f.sourceSha256) || !sha(f.ciphertextSha256)
46
+ || (f.envelopeArchiveId !== undefined && !text(f.envelopeArchiveId, 512))
47
+ || (f.envelopeSha256 !== undefined && !sha(f.envelopeSha256))
48
+ || (f.teamCustody !== undefined && f.teamCustody !== true))
49
+ return null;
50
+ if (f.delta && (!text(f.delta.baseArchiveId, 512) || !integer(f.delta.baseBytes, 1) || f.delta.baseBytes >= f.sourceBytes))
51
+ return null;
52
+ if (f.supersedes && (!text(f.supersedes.archiveId, 512) || !integer(f.supersedes.bytes)))
53
+ return null;
54
+ return { v: 1, createdAt: f.createdAt, algorithm: f.algorithm, sourceBytes: f.sourceBytes,
55
+ storedBytes: f.storedBytes, chunks: f.chunks, sourceSha256: f.sourceSha256, ciphertextSha256: f.ciphertextSha256,
56
+ ...(f.envelopeArchiveId !== undefined ? { envelopeArchiveId: f.envelopeArchiveId } : {}),
57
+ ...(f.envelopeSha256 !== undefined ? { envelopeSha256: f.envelopeSha256 } : {}),
58
+ ...(f.teamCustody ? { teamCustody: true } : {}),
59
+ ...(f.delta ? { delta: { baseArchiveId: f.delta.baseArchiveId, baseBytes: f.delta.baseBytes } } : {}),
60
+ ...(f.supersedes ? { supersedes: { archiveId: f.supersedes.archiveId, bytes: f.supersedes.bytes } } : {}) };
61
+ }
62
+ /** Whitelist facts only, preserving every archive/dependency, never query tokens or summaries. */
63
+ function factsIndex(value) {
64
+ if (!value || typeof value !== "object")
65
+ return null;
66
+ const raw = value;
67
+ if (!text(raw.builtAt, 128) || !raw.archives || !raw.meta)
68
+ return null;
69
+ const rows = Object.entries(raw.archives);
70
+ if (rows.length > MAX_ARCHIVES)
71
+ return null;
72
+ const index = { version: 2, builtAt: raw.builtAt, archives: Object.create(null), tokens: {}, meta: Object.create(null) };
73
+ let serializedBytes = 128;
74
+ for (const [id, row] of rows) {
75
+ const facts = copyFacts(raw.meta[id]?.archiveRecord);
76
+ if (!text(id, 512) || !Array.isArray(row) || row.length > 8 || !row.every((item) => text(item)) || !facts)
77
+ return null;
78
+ const line = [...row];
79
+ serializedBytes += Buffer.byteLength(JSON.stringify([id, line, facts])) + Buffer.byteLength(JSON.stringify(id)) + 40;
80
+ if (serializedBytes > TEAM_SOURCE_FACTS_MAX_BYTES)
81
+ return null;
82
+ index.archives[id] = line;
83
+ index.meta[id] = { archiveRecord: facts };
84
+ }
85
+ for (const [id, metadata] of Object.entries(index.meta)) {
86
+ const delta = metadata.archiveRecord.delta;
87
+ if (delta && (!index.archives[delta.baseArchiveId]
88
+ || index.meta[delta.baseArchiveId]?.archiveRecord?.sourceBytes !== delta.baseBytes
89
+ || delta.baseArchiveId === id))
90
+ return null;
91
+ }
92
+ return index;
93
+ }
94
+ async function cleanup(dataDir, keep) {
95
+ const dir = directory(dataDir);
96
+ const names = await readdir(dir).catch(() => []);
97
+ for (const name of names) {
98
+ const match = PARTIAL.exec(name);
99
+ if (!match)
100
+ continue;
101
+ const path = join(dir, name), pid = Number(match[1]);
102
+ if (!integer(pid, 1) || activePartials.has(path))
103
+ continue;
104
+ let alive = pid !== process.pid;
105
+ if (alive) {
106
+ try {
107
+ process.kill(pid, 0);
108
+ }
109
+ catch (error) {
110
+ alive = error.code !== "ESRCH";
111
+ }
112
+ }
113
+ if (!alive)
114
+ await unlink(path).catch(() => undefined);
115
+ }
116
+ const rows = await Promise.all(names.filter((name) => NAME.test(name)).map(async (name) => {
117
+ const info = await stat(join(dir, name)).catch(() => null);
118
+ return info?.isFile() ? { name, bytes: info.size, at: info.mtimeMs } : null;
119
+ }));
120
+ const ordered = rows.filter((row) => row !== null)
121
+ .sort((a, b) => Number(b.name === keep) - Number(a.name === keep) || b.at - a.at || a.name.localeCompare(b.name));
122
+ let bytes = 0, count = 0;
123
+ for (const row of ordered) {
124
+ bytes += row.bytes;
125
+ count += 1;
126
+ if (row.bytes > TEAM_SOURCE_FACTS_MAX_BYTES + HEADER_BYTES || bytes > TEAM_SOURCE_FACTS_CACHE_BYTES
127
+ || count > TEAM_SOURCE_FACTS_MAX_ENTRIES)
128
+ await unlink(join(dir, row.name)).catch(() => undefined);
129
+ }
130
+ }
131
+ export async function readTeamSourceFactsCache(dataDir, identity, privateRaw, options = {}) {
132
+ options.signal?.throwIfAborted();
133
+ const aad = scope(identity);
134
+ const limit = options.maxDecodedBytes ?? TEAM_SOURCE_FACTS_MAX_BYTES;
135
+ if (!aad || privateRaw.length !== 32 || !integer(limit))
136
+ return null;
137
+ let key, plaintext;
138
+ let callbackFailure;
139
+ try {
140
+ await cleanup(dataDir);
141
+ const handle = await open(join(directory(dataDir), `${aad.toString("hex")}.facts`), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
142
+ let bytes;
143
+ try {
144
+ const before = await handle.stat();
145
+ if (!before.isFile() || before.size <= HEADER_BYTES || before.size > HEADER_BYTES + Math.min(limit, TEAM_SOURCE_FACTS_MAX_BYTES))
146
+ return null;
147
+ bytes = Buffer.alloc(before.size);
148
+ let at = 0;
149
+ while (at < bytes.length) {
150
+ options.signal?.throwIfAborted();
151
+ const read = await handle.read(bytes, at, bytes.length - at, at);
152
+ if (!read.bytesRead)
153
+ return null;
154
+ at += read.bytesRead;
155
+ }
156
+ const after = await handle.stat();
157
+ if (after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
158
+ return null;
159
+ }
160
+ finally {
161
+ await handle.close();
162
+ }
163
+ if (!bytes.subarray(0, 4).equals(MAGIC))
164
+ return null;
165
+ key = Buffer.from(hkdfSync("sha256", privateRaw, aad, "sealkeep-source-facts-cache-v1", 32));
166
+ const decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(4, 16));
167
+ decipher.setAAD(aad);
168
+ decipher.setAuthTag(bytes.subarray(16, HEADER_BYTES));
169
+ plaintext = decipher.update(bytes.subarray(HEADER_BYTES));
170
+ // Even rejected ciphertext consumed bounded decoded memory. Account for
171
+ // that attempt before a caller decides whether a full-verifier fallback fits.
172
+ try {
173
+ options.onDecoded?.(plaintext.length);
174
+ }
175
+ catch (error) {
176
+ callbackFailure = { error };
177
+ throw error;
178
+ }
179
+ const final = decipher.final(); // GCM authentication before JSON parsing or publication.
180
+ if (final.length) {
181
+ final.fill(0);
182
+ return null;
183
+ }
184
+ options.signal?.throwIfAborted();
185
+ const index = factsIndex(JSON.parse(plaintext.toString("utf8")));
186
+ if (!index)
187
+ return null;
188
+ return { index, decodedBytes: plaintext.length };
189
+ }
190
+ catch (error) {
191
+ if (callbackFailure)
192
+ throw callbackFailure.error;
193
+ if (options.signal?.aborted)
194
+ throw error;
195
+ return null;
196
+ }
197
+ finally {
198
+ key?.fill(0);
199
+ plaintext?.fill(0);
200
+ }
201
+ }
202
+ /** Call only after the checkpoint's AEAD, original hash and coverage verifier succeeds,
203
+ * and before filtering grants. Grant permissions are neither cached nor inferred. */
204
+ export async function writeTeamSourceFactsCache(dataDir, identity, privateRaw, verifiedIndex, options = {}) {
205
+ options.signal?.throwIfAborted();
206
+ const aad = scope(identity), index = factsIndex(verifiedIndex);
207
+ if (!aad || privateRaw.length !== 32 || !index)
208
+ return;
209
+ const plaintext = Buffer.from(JSON.stringify(index));
210
+ if (plaintext.length > TEAM_SOURCE_FACTS_MAX_BYTES) {
211
+ plaintext.fill(0);
212
+ return;
213
+ }
214
+ const dir = directory(dataDir), name = `${aad.toString("hex")}.facts`;
215
+ const temporary = join(dir, `.${process.pid}.${randomUUID()}.partial`);
216
+ let key;
217
+ try {
218
+ key = Buffer.from(hkdfSync("sha256", privateRaw, aad, "sealkeep-source-facts-cache-v1", 32));
219
+ const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
220
+ cipher.setAAD(aad);
221
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
222
+ await mkdir(dir, { recursive: true, mode: 0o700 });
223
+ await cleanup(dataDir);
224
+ activePartials.add(temporary);
225
+ const handle = await open(temporary, "wx", 0o600);
226
+ try {
227
+ await handle.writeFile(Buffer.concat([MAGIC, nonce, cipher.getAuthTag(), ciphertext]));
228
+ await handle.sync();
229
+ }
230
+ finally {
231
+ await handle.close();
232
+ }
233
+ options.signal?.throwIfAborted();
234
+ await rename(temporary, join(dir, name));
235
+ const parent = await open(dir, "r").catch(() => null);
236
+ try {
237
+ await parent?.sync().catch(() => undefined);
238
+ }
239
+ finally {
240
+ await parent?.close();
241
+ }
242
+ await cleanup(dataDir, name);
243
+ }
244
+ catch (error) {
245
+ if (options.signal?.aborted)
246
+ throw error;
247
+ // Derived cache failure never prevents an otherwise verified source read.
248
+ }
249
+ finally {
250
+ key?.fill(0);
251
+ plaintext.fill(0);
252
+ activePartials.delete(temporary);
253
+ await unlink(temporary).catch(() => undefined);
254
+ }
255
+ }
@@ -1,6 +1,6 @@
1
1
  export type TrashResult = {
2
2
  movedTo: string;
3
- strategy: "macos-trash" | "xdg-trash" | "windows-recycle-bin" | "vaultline-trash";
3
+ strategy: "macos-trash" | "xdg-trash" | "windows-recycle-bin" | "sealkeep-trash";
4
4
  };
5
5
  /**
6
6
  * Moves a reclaimed source to the operating system's trash, so a mistake is
package/dist/src/trash.js CHANGED
@@ -50,7 +50,7 @@ export async function moveToTrash(path, options) {
50
50
  await mkdir(root, { recursive: true, mode: 0o700 });
51
51
  const target = await uniquePath(root, name);
52
52
  await rename(path, target);
53
- return { movedTo: target, strategy: "vaultline-trash" };
53
+ return { movedTo: target, strategy: "sealkeep-trash" };
54
54
  }
55
55
  export function defaultStrategy() {
56
56
  if (platform() === "darwin")
@@ -59,5 +59,5 @@ export function defaultStrategy() {
59
59
  return "xdg-trash";
60
60
  if (platform() === "win32")
61
61
  return "windows-recycle-bin";
62
- return "vaultline-trash";
62
+ return "sealkeep-trash";
63
63
  }
package/dist/src/tui.js CHANGED
@@ -1,13 +1,13 @@
1
- import { PRODUCT_MARK, PRODUCT_NAME } from "./branding.js";
1
+ import { PRODUCT_MARK, PRODUCT_NAME, providerLabel } from "./branding.js";
2
2
  import { emitKeypressEvents } from "node:readline";
3
- import { createLocalApiServer, localApiToken } from "./local-api.js";
4
- import { freePort } from "./net.js";
3
+ import { localApiToken } from "./local-api.js";
4
+ import { startUi } from "./ui-server.js";
5
5
  /**
6
6
  * A keyboard-first terminal view over the local API.
7
7
  *
8
8
  * It renders with plain ANSI and reads only from `/v1`, so it shows exactly what
9
- * the dashboard shows and works unchanged over SSH. If no agent is listening it
10
- * starts a private one on a loopback port for the lifetime of the session.
9
+ * the dashboard shows and works unchanged over SSH. If no address was supplied
10
+ * it borrows the vault's singleton loopback API, or owns it for this session.
11
11
  */
12
12
  const ESC = "[";
13
13
  const RESET = `${ESC}0m`;
@@ -67,7 +67,7 @@ export function renderFrame(view, state, width = 100) {
67
67
  const retention = state.retention ?? {};
68
68
  lines.push(`Archives ${BOLD}${status.archiveCount ?? 0}${RESET} (${formatBytes(status.archivedBytes ?? 0)} of source preserved)`);
69
69
  lines.push(`Queue ${status.queue?.ready ?? 0} ready · ${status.queue?.leased ?? 0} in flight · ${status.queue?.done ?? 0} archived · ${status.queue?.failed ?? 0} failed`);
70
- lines.push(`Remote ${status.remoteStorage ? `${status.remoteStorage.provider}://${status.remoteStorage.bucket}/${status.remoteStorage.prefix}` : `${DIM}none configured${RESET}`}`);
70
+ lines.push(`Remote ${status.remoteStorage ? `${providerLabel(status.remoteStorage.provider)} · ${status.remoteStorage.bucket}/${status.remoteStorage.prefix}` : `${DIM}none configured${RESET}`}`);
71
71
  lines.push(`Retention policy ${retention.policy ?? "?"} · ${formatBytes(retention.reclaimableBytes ?? 0)} reclaimable`);
72
72
  lines.push("");
73
73
  for (const agent of state.agents ?? []) {
@@ -96,7 +96,7 @@ export function renderFrame(view, state, width = 100) {
96
96
  if (view === "retention") {
97
97
  const retention = state.retention ?? {};
98
98
  lines.push(`Policy ${BOLD}${retention.policy ?? "?"}${RESET} · older than ${retention.olderThanDays ?? "?"} days · ${retention.graceDays ?? "?"}-day grace`);
99
- lines.push(`${DIM}Reclaimed sources move to the OS trash. Preview never touches the disk.${RESET}`);
99
+ lines.push(`${DIM}Confirmed reclaim permanently frees only verified, searchable, unchanged, idle originals. Preview never touches the disk.${RESET}`);
100
100
  lines.push("");
101
101
  for (const candidate of (retention.candidates ?? []).slice(0, 15)) {
102
102
  lines.push(`${candidate.eligible ? `${GREEN}eligible${RESET}` : `${DIM}held ${RESET}`} ${truncate(candidate.path, 40)} ${DIM}${candidate.blockers?.[0] ?? ""}${RESET}`);
@@ -130,10 +130,8 @@ export async function runTui(dataDir, options = {}) {
130
130
  let base = options.base;
131
131
  const token = options.token ?? await localApiToken(dataDir);
132
132
  if (!base) {
133
- owned = createLocalApiServer(dataDir, token);
134
- const port = await freePort(4190);
135
- await new Promise((ready) => owned.listen(port, "127.0.0.1", ready));
136
- base = `http://127.0.0.1:${port}`;
133
+ owned = await startUi(dataDir, { port: 4190, openBrowser: false, owner: "interactive" });
134
+ base = owned.origin;
137
135
  }
138
136
  const client = apiClient(base, token);
139
137
  let view = "overview";
@@ -175,5 +173,6 @@ export async function runTui(dataDir, options = {}) {
175
173
  };
176
174
  input.on("keypress", onKey);
177
175
  });
178
- await new Promise((closed) => (owned ? owned.close(() => closed()) : closed()));
176
+ if (owned)
177
+ await owned.close();
179
178
  }
@@ -1,4 +1,25 @@
1
- import type { Envelope, Suite } from "../packages/vaultline-crypto/src/index.js";
1
+ import type { Envelope, Suite } from "../packages/sealkeep-crypto/src/index.js";
2
+ /**
3
+ * Immutable, non-secret identity of an object-store destination.
4
+ *
5
+ * A routing target id is deliberately mutable, while bucket names are not
6
+ * globally unique for R2, B2, MinIO, or other S3-compatible accounts. Copies
7
+ * and locally-held credentials therefore bind to this canonical description,
8
+ * not to the routing label alone. `endpoint` is a canonical origin. `prefix`
9
+ * is included for credentials because changing it changes the namespace that
10
+ * credential was explicitly tested against; an individual copy's objectKey
11
+ * still names the exact object within it.
12
+ */
13
+ export type StorageDestinationIdentity = {
14
+ version: 1;
15
+ provider: "s3" | "r2" | "b2" | "gcs" | "gdrive";
16
+ bucket: string;
17
+ prefix: string;
18
+ region: string | null;
19
+ endpoint: string | null;
20
+ /** Provider account identity when routing metadata exposes one separately. */
21
+ account: string | null;
22
+ };
2
23
  /** The original prototype envelope. Still readable; never written by this build. */
3
24
  export type ArchiveRecordV1 = {
4
25
  version: 1;
@@ -26,8 +47,20 @@ export type ArchiveRecordV1 = {
26
47
  };
27
48
  objectPath: string;
28
49
  };
50
+ export type TeamCustody = {
51
+ version: 1;
52
+ spaceKey: string;
53
+ envelopeArchiveId: string;
54
+ };
55
+ /** Custody is a narrowly scoped exception to locator/envelope identity equality. */
56
+ export declare function teamCustodyRecordIsValid(value: unknown): boolean;
29
57
  /** A remote copy the client uploaded and then verified. Written only after verification. */
30
58
  export type RemoteObject = {
59
+ /**
60
+ * Stable identity for this physical copy. Older records do not carry it;
61
+ * archive-copies derives the same id from provider, bucket and object key.
62
+ */
63
+ copyId?: string;
31
64
  provider: string;
32
65
  bucket: string;
33
66
  /** Single-object layout: the object's key. Chunk layout: the FOLDER prefix (no trailing slash). */
@@ -38,7 +71,7 @@ export type RemoteObject = {
38
71
  verifiedAt: string;
39
72
  /**
40
73
  * Present when the remote is a FOLDER of chunk objects (`chunk-000000`…)
41
- * plus an `envelope.vlmeta` sidecar, instead of one blob. Every chunk is
74
+ * plus an `envelope.skmeta` sidecar, instead of one blob. Every chunk is
42
75
  * independently sealed and independently fetchable; the sidecar makes the
43
76
  * folder self-recovering with nothing but the recovery phrase. Absent means
44
77
  * the original single-object layout, which stays readable forever.
@@ -49,8 +82,15 @@ export type RemoteObject = {
49
82
  };
50
83
  /** Which configured storage target this copy went to, for usage accounting. */
51
84
  targetId?: string;
85
+ /**
86
+ * Immutable physical destination recorded when this copy verified. Older
87
+ * records omit both fields and are handled by an explicit legacy policy;
88
+ * they must never be silently rebound to an arbitrary custom endpoint.
89
+ */
90
+ destination?: StorageDestinationIdentity;
91
+ destinationFingerprint?: string;
52
92
  };
53
- /** The current record: a `vaultline-crypto` v2 envelope plus local bookkeeping. */
93
+ /** The current record: a `sealkeep-crypto` v2 envelope plus local bookkeeping. */
54
94
  export type ArchiveRecordV2 = {
55
95
  version: 2;
56
96
  id: string;
@@ -60,6 +100,12 @@ export type ArchiveRecordV2 = {
60
100
  agent: string;
61
101
  bytes: number;
62
102
  sha256: string;
103
+ /** The project this session belonged to, recorded at seal time — the only place that knows it for certain. */
104
+ project?: string;
105
+ /** Collision-safe local identity used to select a team binding. `project` is display-only. */
106
+ projectKey?: string;
107
+ /** Stable shared-project scope. Local folder/project names may differ on every machine; this opaque id does not. */
108
+ projectScope?: string;
63
109
  };
64
110
  /** Summary of the stored object, shared with v1 so callers can treat both alike. */
65
111
  cipher: {
@@ -69,13 +115,42 @@ export type ArchiveRecordV2 = {
69
115
  chunks: number;
70
116
  };
71
117
  envelope: Envelope;
118
+ /** Explicit owner custody alias: the storage locator differs from the sealed
119
+ * AEAD identity. This is authorized only for the named shared project. */
120
+ teamCustody?: TeamCustody;
72
121
  objectPath: string;
122
+ /**
123
+ * Additional verified remote copies. `remote` remains the primary copy so
124
+ * older Sealkeep builds keep reading v2 records; this array never needs to
125
+ * repeat the primary and is normalised by archive-copies on mutation.
126
+ */
127
+ copies?: RemoteObject[];
73
128
  remote?: RemoteObject;
74
- /** Set once the local source has been moved to the trash. The archive itself stays. */
129
+ /**
130
+ * Set once the verified/searchable local source has been reclaimed. Current
131
+ * builds permanently purge the transaction-staged original only after the
132
+ * record and audit decision are durable. Older records may still name a
133
+ * recoverable Trash path.
134
+ */
75
135
  reclaimed?: {
76
136
  at: string;
77
- trashPath: string;
137
+ /** Legacy recoverable-trash location. Absent for permanent disk reclaim. */
138
+ trashPath?: string;
139
+ /** Internal crash-recovery staging path; normally absent by the time callers read the record. */
140
+ stagingPath?: string;
78
141
  strategy: string;
142
+ transactionId?: string;
143
+ originalBytes?: number;
144
+ /** Allocated filesystem blocks released, net of a tiny native resume pointer. */
145
+ freedBytes?: number;
146
+ purgedAt?: string;
147
+ /** Exact tiny rollout left behind so Codex can still resolve `resume <sessionId>`. */
148
+ resumeStub?: {
149
+ version: 1;
150
+ sessionId: string;
151
+ bytes: number;
152
+ sha256: string;
153
+ };
79
154
  };
80
155
  /**
81
156
  * Set once the local *archive* has been removed because a verified remote
@@ -92,6 +167,12 @@ export type ArchiveRecordV2 = {
92
167
  provider: string;
93
168
  bucket: string;
94
169
  objectKey: string;
170
+ /** Present only while a crash-recoverable local-cache removal is in flight. */
171
+ transactionId?: string;
172
+ /** Logical ciphertext length displaced from this machine. */
173
+ localBytes?: number;
174
+ /** Filesystem allocation actually released (st_blocks * 512 where available). */
175
+ freedBytes?: number;
95
176
  };
96
177
  /**
97
178
  * The archive whose bytes this one re-archived, and how many of them.
@@ -127,15 +208,34 @@ export type ArchiveRecord = ArchiveRecordV1 | ArchiveRecordV2;
127
208
  export type StorageTargetConfig = {
128
209
  id: string;
129
210
  provider: "vaultline" | "s3" | "r2" | "b2" | "gcs" | "gdrive";
211
+ /** False keeps the destination available for restore/move but excludes it from all new writes. */
212
+ enabled?: boolean;
130
213
  bucket?: string;
131
214
  prefix?: string;
132
215
  region?: string;
216
+ /** Non-secret provider endpoint shared with every machine (required by R2/custom S3). */
217
+ endpoint?: string;
133
218
  maxGb?: number;
134
219
  projects?: string[];
135
220
  priority?: number;
136
221
  /** Stamped by machines when they push: bytes this target holds, per their records. */
137
222
  usedBytes?: number;
138
223
  usedAt?: string;
224
+ /**
225
+ * Non-secret local credential evidence reported by each enrolled machine.
226
+ * Provider credentials never leave that machine. This does not claim the
227
+ * provider answered now; `verifiedAt` names the last explicit full probe.
228
+ */
229
+ connectionHealth?: Record<string, {
230
+ /** Backward-compatible availability bit: local credentials/session exist. */
231
+ connected: boolean;
232
+ /** What was actually observed; this is never presented as a live provider ping. */
233
+ status?: "credentials_verified" | "credentials_saved" | "credentials_missing" | "account_session";
234
+ checkedAt: string;
235
+ /** Time of the last successful provider write/read/delete/absence probe. */
236
+ verifiedAt?: string | null;
237
+ label?: string;
238
+ }>;
139
239
  };
140
240
  export declare function isV2(record: ArchiveRecord): record is ArchiveRecordV2;
141
241
  export type VaultConfig = {
@@ -162,6 +262,20 @@ export type VaultConfig = {
162
262
  };
163
263
  /** Multiple simultaneous storage targets with routing rules; wins over remoteStorage when present. */
164
264
  storageTargets?: StorageTargetConfig[];
265
+ /**
266
+ * Provenance for the local routing copy. Account-derived rows are usable only
267
+ * while this machine is still in the exact Cloud auth generation which read
268
+ * or edited them; otherwise an account switch could route the next archive
269
+ * through the previous account's Drive or bucket credentials while offline.
270
+ * `local` is reserved for an explicit machine-only configuration. Absence is
271
+ * a legacy, unknown origin and is treated conservatively while signed in.
272
+ */
273
+ storageTargetsScope?: {
274
+ kind: "local";
275
+ } | {
276
+ kind: "account";
277
+ accountScope: string | null;
278
+ };
165
279
  recovery: {
166
280
  phraseCheck: string;
167
281
  };
@@ -175,6 +289,8 @@ export type VaultConfig = {
175
289
  group?: string;
176
290
  /** Sharing scope: set, this key is a member of one project and is wrapped into that project's archives only. */
177
291
  project?: string;
292
+ /** Stable project identity for new scoped recipients. Legacy label-only recipients remain readable. */
293
+ projectKey?: string;
178
294
  }[];
179
295
  /** `deleteAfterDays` is optional so vaults written before archive lifecycle existed still load; absent means never. */
180
296
  retention?: {
@@ -185,8 +301,58 @@ export type VaultConfig = {
185
301
  };
186
302
  /** Ed25519 public keys, by key id, that may sign a release this client will accept. */
187
303
  updateKeys?: Record<string, string>;
188
- /** How many seals may run at once (1–4). Absent means one at a time. */
189
- sealConcurrency?: number;
304
+ /**
305
+ * Per-project sharing tempo. "active": the moment a session of this project
306
+ * seals, the content index syncs and a `finished` presence line is published
307
+ * — teammates' agents can find the work in minutes. Absent or "passive":
308
+ * archive-at-rest behaviour, nothing extra moves.
309
+ */
310
+ projectSharing?: Record<string, "active" | "passive">;
311
+ /**
312
+ * Visible local bindings between a checkout/project and an opaque hosted
313
+ * collaboration space. The space key is an identifier, not an encryption
314
+ * key; the actual feed key lives only in the OS keystore. A binding is made
315
+ * once from the dashboard and every supported agent hook uses it thereafter.
316
+ */
317
+ teamSpaces?: Record<string, {
318
+ /** Stable logical space id used by REST, encryption AAD, and local inboxes. */
319
+ spaceKey: string;
320
+ role?: "owner" | "member";
321
+ keyVersion?: number;
322
+ /** Rotatable opaque Socket.IO route. Absent on pre-rotation/v1 bindings. */
323
+ realtimeKey?: string;
324
+ /** True only after this machine's user chose the local project mapping. */
325
+ localConfirmed?: true;
326
+ /** Human-facing label for bindings stored under an opaque project key. */
327
+ localProject?: string;
328
+ }>;
329
+ /**
330
+ * Hosted spaces this particular machine deliberately disconnected from.
331
+ * Membership remains owned by SealKeep Cloud, but automatic reconciliation
332
+ * must not silently reconnect a checkout the user removed here.
333
+ */
334
+ teamSpaceOptOuts?: string[];
335
+ /**
336
+ * A local, privacy-preserving handoff from folder discovery to the account
337
+ * panel. The browser records only the logical project name in the cloud;
338
+ * these machine-specific folder/project names never leave this vault.
339
+ *
340
+ * When the matching cloud project appears, the daemon binds every listed
341
+ * local project to its one stable space id and removes the draft. Keeping
342
+ * this in config (rather than browser storage) makes the handoff survive a
343
+ * closed tab, service restart, or package upgrade.
344
+ */
345
+ teamProjectDrafts?: Array<{
346
+ projectName: string;
347
+ /** Legacy display labels, retained so older clients can still render the draft. */
348
+ projects: string[];
349
+ /** Exact selections used by new clients; labels never decide access when these are present. */
350
+ projectSelections?: Array<{
351
+ projectKey: string;
352
+ project: string;
353
+ }>;
354
+ createdAt: string;
355
+ }>;
190
356
  /**
191
357
  * How chunk-folder remotes are named. "readable" (default): project/date/
192
358
  * session as words — browsable in your own bucket's console. "hashed":
package/dist/src/types.js CHANGED
@@ -1,3 +1,23 @@
1
+ /** Custody is a narrowly scoped exception to locator/envelope identity equality. */
2
+ export function teamCustodyRecordIsValid(value) {
3
+ const object = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
4
+ const uuid = (entry) => typeof entry === "string"
5
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entry);
6
+ const record = object(value);
7
+ const custody = object(record?.teamCustody);
8
+ const cipher = object(record?.cipher);
9
+ const envelope = object(record?.envelope);
10
+ const source = object(record?.source);
11
+ return record?.version === 2 && uuid(record.id)
12
+ && custody?.version === 1 && uuid(custody.envelopeArchiveId)
13
+ && typeof custody.spaceKey === "string" && /^[A-Za-z0-9_-]{43}$/.test(custody.spaceKey)
14
+ && source?.projectScope === custody.spaceKey
15
+ && (cipher?.algorithm === "aes-256-gcm" || cipher?.algorithm === "chacha20-poly1305")
16
+ && envelope?.version === 2 && envelope.archiveId === custody.envelopeArchiveId
17
+ && envelope.suite === cipher.algorithm
18
+ && Array.isArray(envelope.wrappedKeys) && envelope.wrappedKeys.length === 1
19
+ && object(envelope.wrappedKeys[0])?.type === "phrase";
20
+ }
1
21
  export function isV2(record) {
2
22
  return record.version === 2;
3
23
  }