sealkeep 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,55 @@
1
+ /** A project identity already detected from a live, currently existing folder. */
2
+ export type DetectedProject = {
3
+ project: string;
4
+ projectKey: string;
5
+ projectPath: string;
6
+ };
7
+ export type HistoricalProjectRepairCandidate = {
8
+ /** Stable identity recorded by the archive, when one was available. */
9
+ historicalProjectKey: string | null;
10
+ /** Display-only label recorded by the archive. */
11
+ historicalProject: string | null;
12
+ /** The old identity is not among the supplied live folder identities. */
13
+ originalFolderAvailable: false;
14
+ reason: "folder-not-detected" | "no-stable-project-key" | "already-scoped";
15
+ archiveCount: number;
16
+ /** Distinct source transcript paths, not snapshots/delta links. */
17
+ sessionCount: number;
18
+ /** Exact choices; labels never select a replacement. Empty for legacy rows. */
19
+ replacementOptions: DetectedProject[];
20
+ repairable: boolean;
21
+ };
22
+ export type HistoricalProjectRepairRequest = {
23
+ historicalProjectKey: string;
24
+ replacementProjectKey: string;
25
+ };
26
+ export type HistoricalProjectRepairPreview = {
27
+ candidate: HistoricalProjectRepairCandidate;
28
+ replacement: DetectedProject;
29
+ /** True only when the replacement already belongs to a local team space. */
30
+ teamSpaceBound: true;
31
+ /** The exact opaque space id used by the alias; not archive key material. */
32
+ spaceKey: string;
33
+ };
34
+ export type HistoricalProjectRepairResult = HistoricalProjectRepairPreview & {
35
+ applied: true;
36
+ };
37
+ /**
38
+ * Finds archive identities that are no longer represented by a live folder.
39
+ *
40
+ * The caller supplies the same exact-key detections used by the dashboard's
41
+ * project picker. This function deliberately does not derive a replacement
42
+ * from a label, path basename, or nearest folder. Legacy archives without a
43
+ * stable key are surfaced for review but are never repairable automatically.
44
+ */
45
+ export declare function listHistoricalProjectRepairCandidates(dataDir: string, detectedProjects: readonly DetectedProject[]): Promise<HistoricalProjectRepairCandidate[]>;
46
+ /** Builds a no-write impact preview for one exact historical-to-live mapping. */
47
+ export declare function previewHistoricalProjectRepair(dataDir: string, request: HistoricalProjectRepairRequest, detectedProjects: readonly DetectedProject[]): Promise<HistoricalProjectRepairPreview>;
48
+ /**
49
+ * Adds the old stable key as a local alias of the replacement's existing team
50
+ * space. Confirmation is mandatory; no archive sidecar or ciphertext is
51
+ * opened or rewritten. The normal team backfill then handles encrypted copies.
52
+ */
53
+ export declare function applyHistoricalProjectRepair(dataDir: string, request: HistoricalProjectRepairRequest, detectedProjects: readonly DetectedProject[], options: {
54
+ confirm: boolean;
55
+ }): Promise<HistoricalProjectRepairResult>;
@@ -0,0 +1,131 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { listArchives, bindTeamSpace, readConfig, teamSpaceOf } from "./vault.js";
3
+ import { projectIdentityOfRecord } from "./adapters.js";
4
+ import { fail } from "./errors.js";
5
+ const STABLE_PROJECT_KEY = /^(?:g1|p1):[a-f0-9]{64}$/;
6
+ function assertStableProjectKey(value, label) {
7
+ if (!STABLE_PROJECT_KEY.test(value))
8
+ fail("invalid_argument", `${label} must be a stable project key`);
9
+ }
10
+ function projectOfRecord(record) {
11
+ const project = "project" in record.source && typeof record.source.project === "string"
12
+ ? record.source.project
13
+ : null;
14
+ const projectKey = "projectKey" in record.source && typeof record.source.projectKey === "string"
15
+ ? record.source.projectKey
16
+ : null;
17
+ return { project, projectKey };
18
+ }
19
+ /**
20
+ * Finds archive identities that are no longer represented by a live folder.
21
+ *
22
+ * The caller supplies the same exact-key detections used by the dashboard's
23
+ * project picker. This function deliberately does not derive a replacement
24
+ * from a label, path basename, or nearest folder. Legacy archives without a
25
+ * stable key are surfaced for review but are never repairable automatically.
26
+ */
27
+ export async function listHistoricalProjectRepairCandidates(dataDir, detectedProjects) {
28
+ const detected = (await Promise.all(detectedProjects
29
+ .filter((item) => STABLE_PROJECT_KEY.test(item.projectKey) && item.project.trim() && item.projectPath.trim())
30
+ .map(async (item) => await stat(item.projectPath).then((entry) => entry.isDirectory() ? item : null).catch(() => null))))
31
+ .filter((item) => item !== null);
32
+ const detectedKeys = new Set(detected.map((item) => item.projectKey));
33
+ const archives = await listArchives(dataDir);
34
+ const groups = new Map();
35
+ for (const record of archives) {
36
+ const recorded = projectOfRecord(record);
37
+ const identity = recorded.projectKey
38
+ ? recorded
39
+ : await projectIdentityOfRecord(record).catch(() => ({ project: recorded.project, projectKey: null, projectPath: null }));
40
+ const project = identity.project ?? recorded.project;
41
+ const projectKey = identity.projectKey && STABLE_PROJECT_KEY.test(identity.projectKey)
42
+ ? identity.projectKey
43
+ : null;
44
+ if (projectKey && detectedKeys.has(projectKey))
45
+ continue;
46
+ const groupKey = projectKey ?? `legacy:${project ?? ""}`;
47
+ const group = groups.get(groupKey) ?? {
48
+ project,
49
+ projectKey,
50
+ hasProjectScope: false,
51
+ archiveCount: 0,
52
+ sourcePaths: new Set(),
53
+ };
54
+ if ("projectScope" in record.source && typeof record.source.projectScope === "string" && record.source.projectScope) {
55
+ group.hasProjectScope = true;
56
+ }
57
+ group.archiveCount += 1;
58
+ group.sourcePaths.add(record.source.path);
59
+ groups.set(groupKey, group);
60
+ }
61
+ return [...groups.values()]
62
+ .map((group) => ({
63
+ historicalProjectKey: group.projectKey,
64
+ historicalProject: group.project,
65
+ originalFolderAvailable: false,
66
+ reason: !group.projectKey ? "no-stable-project-key" : group.hasProjectScope ? "already-scoped" : "folder-not-detected",
67
+ archiveCount: group.archiveCount,
68
+ sessionCount: group.sourcePaths.size,
69
+ replacementOptions: group.projectKey && !group.hasProjectScope ? detected : [],
70
+ repairable: Boolean(group.projectKey && !group.hasProjectScope && detected.length > 0),
71
+ }))
72
+ .sort((left, right) => (left.historicalProject ?? "").localeCompare(right.historicalProject ?? "")
73
+ || (left.historicalProjectKey ?? "").localeCompare(right.historicalProjectKey ?? ""));
74
+ }
75
+ async function resolvePreview(dataDir, request, detectedProjects) {
76
+ assertStableProjectKey(request.historicalProjectKey, "Historical project key");
77
+ assertStableProjectKey(request.replacementProjectKey, "Replacement project key");
78
+ if (request.historicalProjectKey === request.replacementProjectKey) {
79
+ fail("invalid_argument", "The replacement must be a different exact project key");
80
+ }
81
+ const candidates = await listHistoricalProjectRepairCandidates(dataDir, detectedProjects);
82
+ const candidate = candidates.find((item) => item.historicalProjectKey === request.historicalProjectKey);
83
+ if (!candidate) {
84
+ fail("invalid_argument", "That historical project is not awaiting repair, or is already represented by a detected folder");
85
+ }
86
+ if (!candidate.repairable) {
87
+ fail("invalid_argument", candidate.reason === "already-scoped"
88
+ ? "This history already carries a shared-project scope and cannot be reassigned locally"
89
+ : "This history has no stable project key, so it cannot be reassigned safely");
90
+ }
91
+ const replacement = candidate.replacementOptions.find((item) => item.projectKey === request.replacementProjectKey);
92
+ if (!replacement || !STABLE_PROJECT_KEY.test(replacement.projectKey)) {
93
+ fail("invalid_argument", "Choose an exact currently detected folder for the replacement");
94
+ }
95
+ const config = await readConfig(dataDir);
96
+ const binding = teamSpaceOf(config, replacement.project, replacement.projectKey);
97
+ if (!binding) {
98
+ fail("invalid_argument", "The exact replacement folder is not connected to a shared project yet; connect it first");
99
+ }
100
+ const existing = teamSpaceOf(config, candidate.historicalProject, candidate.historicalProjectKey);
101
+ if (existing && existing.spaceKey !== binding.spaceKey) {
102
+ fail("invalid_argument", "This historical identity is already bound to a different shared project");
103
+ }
104
+ return {
105
+ candidate,
106
+ replacement,
107
+ teamSpaceBound: true,
108
+ spaceKey: binding.spaceKey,
109
+ };
110
+ }
111
+ /** Builds a no-write impact preview for one exact historical-to-live mapping. */
112
+ export async function previewHistoricalProjectRepair(dataDir, request, detectedProjects) {
113
+ return resolvePreview(dataDir, request, detectedProjects);
114
+ }
115
+ /**
116
+ * Adds the old stable key as a local alias of the replacement's existing team
117
+ * space. Confirmation is mandatory; no archive sidecar or ciphertext is
118
+ * opened or rewritten. The normal team backfill then handles encrypted copies.
119
+ */
120
+ export async function applyHistoricalProjectRepair(dataDir, request, detectedProjects, options) {
121
+ if (options.confirm !== true)
122
+ fail("invalid_argument", "Confirm this exact historical-project repair after reviewing its archive and session counts");
123
+ const preview = await resolvePreview(dataDir, request, detectedProjects);
124
+ const config = await readConfig(dataDir);
125
+ const binding = teamSpaceOf(config, preview.replacement.project, preview.replacement.projectKey);
126
+ if (!binding)
127
+ fail("invalid_argument", "The replacement folder's shared-project binding changed; review again");
128
+ const aliasBinding = { ...binding, localConfirmed: true };
129
+ await bindTeamSpace(dataDir, preview.candidate.historicalProject ?? preview.replacement.project, aliasBinding, preview.candidate.historicalProjectKey);
130
+ return { ...preview, applied: true };
131
+ }
@@ -1,4 +1,6 @@
1
1
  import type { ProviderUploadClient, UploadLease } from "../control-plane.js";
2
+ import { type ByteStream } from "../byte-stream.js";
3
+ import type { StorageFetch } from "./safe-storage-fetch.js";
2
4
  /**
3
5
  * Google Cloud Storage V4 signing (`GOOG4-RSA-SHA256`) plus resumable uploads.
4
6
  *
@@ -55,6 +57,7 @@ export type GcsStreamOptions = {
55
57
  /** Awaited after every successful chunk PUT with the cumulative confirmed
56
58
  * bytes. This call is the durability point a resume journal writes at. */
57
59
  onState?: (state: GcsStreamState) => void | Promise<void>;
60
+ signal?: AbortSignal;
58
61
  };
59
62
  /**
60
63
  * GCS resumable upload client.
@@ -66,22 +69,34 @@ export declare class GcsUploadClient implements ProviderUploadClient {
66
69
  private readonly endpoint;
67
70
  private readonly serviceAccount;
68
71
  private readonly chunkBytes;
72
+ private readonly fetchImpl;
69
73
  readonly kind: "gcs";
70
- constructor(endpoint: GcsEndpoint, serviceAccount: ServiceAccount, chunkBytes?: number);
74
+ constructor(endpoint: GcsEndpoint, serviceAccount: ServiceAccount, chunkBytes?: number, fetchImpl?: StorageFetch);
71
75
  private sign;
72
76
  /** The chunk-folder layout's object verbs: plain signed single-object HTTP. */
73
- putObject(object: string, body: Buffer): Promise<{
77
+ putObject(object: string, body: Buffer, options?: {
78
+ signal?: AbortSignal;
79
+ }): Promise<{
74
80
  checksum: string;
75
81
  }>;
76
- headObject(object: string): Promise<{
82
+ headObject(object: string, options?: {
83
+ signal?: AbortSignal;
84
+ }): Promise<{
77
85
  exists: boolean;
78
86
  bytes: number;
79
87
  checksum?: string;
80
88
  }>;
81
- getObject(object: string): Promise<Buffer>;
89
+ getObject(object: string, options?: {
90
+ signal?: AbortSignal;
91
+ }): Promise<Buffer>;
92
+ /** Object-key streaming counterpart used by the background index store. */
93
+ getObjectStream(object: string, options?: {
94
+ signal?: AbortSignal;
95
+ }): Promise<ByteStream>;
96
+ private downloadObjectStream;
82
97
  deleteObject(object: string): Promise<void>;
83
98
  /** Starts a resumable session and returns its URI. */
84
- initiateResumable(object: string): Promise<string>;
99
+ initiateResumable(object: string, signal?: AbortSignal): Promise<string>;
85
100
  /**
86
101
  * Asks a session how many bytes the provider has durably confirmed, speaking
87
102
  * the protocol GCS actually implements: an empty PUT whose content-range is
@@ -89,7 +104,7 @@ export declare class GcsUploadClient implements ProviderUploadClient {
89
104
  * confirmed — or 308 with no Range at all when nothing is confirmed yet.
90
105
  * (Error messages here never carry the session URI: it is a bearer token.)
91
106
  */
92
- resumableStatus(session: string): Promise<{
107
+ resumableStatus(session: string, signal?: AbortSignal): Promise<{
93
108
  confirmedBytes: number;
94
109
  }>;
95
110
  upload(lease: UploadLease, ciphertext: Buffer): Promise<{
@@ -124,7 +139,13 @@ export declare class GcsUploadClient implements ProviderUploadClient {
124
139
  bytes: number;
125
140
  }>;
126
141
  /** Reads an object back for restore or offload-proof: a signed GET, whole. */
127
- download(lease: UploadLease): Promise<Buffer>;
142
+ download(lease: UploadLease, options?: {
143
+ signal?: AbortSignal;
144
+ }): Promise<Buffer>;
145
+ /** The production restore/transfer door: response chunks flow straight to disk. */
146
+ downloadStream(lease: UploadLease, options?: {
147
+ signal?: AbortSignal;
148
+ }): Promise<ByteStream>;
128
149
  head(lease: UploadLease): Promise<{
129
150
  exists: boolean;
130
151
  bytes: number;
@@ -1,6 +1,7 @@
1
1
  import { createHash, createPrivateKey, createSign } from "node:crypto";
2
2
  import { fail } from "../errors.js";
3
3
  import { amzDates, canonicalHeaders, canonicalQuery, canonicalUri } from "./sigv4.js";
4
+ import { collectChunks, responseBodyChunks } from "../byte-stream.js";
4
5
  /**
5
6
  * Google Cloud Storage V4 signing (`GOOG4-RSA-SHA256`) plus resumable uploads.
6
7
  *
@@ -51,26 +52,28 @@ export class GcsUploadClient {
51
52
  endpoint;
52
53
  serviceAccount;
53
54
  chunkBytes;
55
+ fetchImpl;
54
56
  kind = "gcs";
55
- constructor(endpoint, serviceAccount, chunkBytes = 8 * 1024 * 1024) {
57
+ constructor(endpoint, serviceAccount, chunkBytes = 8 * 1024 * 1024, fetchImpl = fetch) {
56
58
  this.endpoint = endpoint;
57
59
  this.serviceAccount = serviceAccount;
58
60
  this.chunkBytes = chunkBytes;
61
+ this.fetchImpl = fetchImpl;
59
62
  }
60
63
  sign(method, object, headers = {}, query = {}) {
61
64
  return signGcsUrl({ method, bucket: this.endpoint.bucket, object, serviceAccount: this.serviceAccount, headers, query, host: this.endpoint.host, port: this.endpoint.port, protocol: this.endpoint.protocol });
62
65
  }
63
66
  /** The chunk-folder layout's object verbs: plain signed single-object HTTP. */
64
- async putObject(object, body) {
67
+ async putObject(object, body, options = {}) {
65
68
  const { url } = this.sign("PUT", object);
66
- const response = await fetch(url, { method: "PUT", body: new Uint8Array(body) });
69
+ const response = await this.fetchImpl(url, { method: "PUT", body: new Uint8Array(body), signal: options.signal });
67
70
  if (!response.ok)
68
71
  fail("internal", `Upload failed for ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
69
72
  return { checksum: createHash("md5").update(body).digest("base64") };
70
73
  }
71
- async headObject(object) {
74
+ async headObject(object, options = {}) {
72
75
  const { url } = this.sign("HEAD", object);
73
- const response = await fetch(url, { method: "HEAD" });
76
+ const response = await this.fetchImpl(url, { method: "HEAD", signal: options.signal });
74
77
  if (response.status === 404)
75
78
  return { exists: false, bytes: 0 };
76
79
  if (!response.ok)
@@ -79,23 +82,30 @@ export class GcsUploadClient {
79
82
  const md5 = /(?:^|[ ,])md5=([A-Za-z0-9+/]+={0,2})/.exec(hash)?.[1];
80
83
  return { exists: true, bytes: Number(response.headers.get("content-length") ?? "0"), checksum: md5 };
81
84
  }
82
- async getObject(object) {
85
+ async getObject(object, options = {}) {
86
+ return collectChunks(await this.downloadObjectStream(object, options.signal), Number.MAX_SAFE_INTEGER);
87
+ }
88
+ /** Object-key streaming counterpart used by the background index store. */
89
+ async getObjectStream(object, options = {}) {
90
+ return this.downloadObjectStream(object, options.signal);
91
+ }
92
+ async downloadObjectStream(object, signal) {
83
93
  const { url } = this.sign("GET", object);
84
- const response = await fetch(url);
94
+ const response = await this.fetchImpl(url, { signal });
85
95
  if (!response.ok)
86
96
  fail("internal", `Could not download ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
87
- return Buffer.from(await response.arrayBuffer());
97
+ return responseBodyChunks(response);
88
98
  }
89
99
  async deleteObject(object) {
90
100
  const { url } = this.sign("DELETE", object);
91
- const response = await fetch(url, { method: "DELETE" });
101
+ const response = await this.fetchImpl(url, { method: "DELETE" });
92
102
  if (!response.ok && response.status !== 404)
93
103
  fail("internal", `Could not delete ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
94
104
  }
95
105
  /** Starts a resumable session and returns its URI. */
96
- async initiateResumable(object) {
106
+ async initiateResumable(object, signal) {
97
107
  const { url } = this.sign("POST", object, { "x-goog-resumable": "start" });
98
- const response = await fetch(url, { method: "POST", headers: { "x-goog-resumable": "start" } });
108
+ const response = await this.fetchImpl(url, { method: "POST", headers: { "x-goog-resumable": "start" }, signal });
99
109
  if (!response.ok)
100
110
  fail("internal", `Could not start a resumable upload for ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
101
111
  const session = response.headers.get("location");
@@ -108,8 +118,8 @@ export class GcsUploadClient {
108
118
  * confirmed — or 308 with no Range at all when nothing is confirmed yet.
109
119
  * (Error messages here never carry the session URI: it is a bearer token.)
110
120
  */
111
- async resumableStatus(session) {
112
- const response = await fetch(session, { method: "PUT", headers: { "content-range": "bytes */*" } });
121
+ async resumableStatus(session, signal) {
122
+ const response = await this.fetchImpl(session, { method: "PUT", headers: { "content-range": "bytes */*" }, signal });
113
123
  // 2xx means the session already finalized its object; resuming would try
114
124
  // to append to something finished, so the caller should verify instead.
115
125
  if (response.ok)
@@ -131,7 +141,7 @@ export class GcsUploadClient {
131
141
  const slice = ciphertext.subarray(offset, Math.min(offset + this.chunkBytes, ciphertext.length));
132
142
  const last = offset + slice.length >= ciphertext.length;
133
143
  const range = ciphertext.length === 0 ? "bytes */0" : `bytes ${offset}-${offset + slice.length - 1}/${ciphertext.length}`;
134
- const response = await fetch(session, { method: "PUT", body: new Uint8Array(slice), headers: { "content-range": range } });
144
+ const response = await this.fetchImpl(session, { method: "PUT", body: new Uint8Array(slice), headers: { "content-range": range } });
135
145
  // 308 means "resume incomplete" and is the expected status between chunks.
136
146
  if (!response.ok && !(response.status === 308 && !last)) {
137
147
  fail("internal", `Resumable upload for ${object} failed at ${range}: ${response.status} ${response.statusText}`, { object, status: response.status });
@@ -154,7 +164,7 @@ export class GcsUploadClient {
154
164
  let session;
155
165
  let sent = 0;
156
166
  if (resume) {
157
- const { confirmedBytes } = await this.resumableStatus(resume.session);
167
+ const { confirmedBytes } = await this.resumableStatus(resume.session, options.signal);
158
168
  // The caller re-sealed from the offset its journal recorded. If the
159
169
  // provider confirmed a different count — a PUT landed after the last
160
170
  // journal write, say — the very first flush would splice bytes at the
@@ -166,7 +176,7 @@ export class GcsUploadClient {
166
176
  sent = confirmedBytes;
167
177
  }
168
178
  else {
169
- session = await this.initiateResumable(objectKey);
179
+ session = await this.initiateResumable(objectKey, options.signal);
170
180
  }
171
181
  // A resumed run never saw the bytes the earlier run sent, so it cannot
172
182
  // roll an MD5 over the whole object — null says "no provider-language
@@ -183,7 +193,7 @@ export class GcsUploadClient {
183
193
  const range = final
184
194
  ? (piece.length === 0 ? `bytes */${sent}` : `bytes ${sent}-${sent + piece.length - 1}/${sent + piece.length}`)
185
195
  : `bytes ${sent}-${sent + piece.length - 1}/*`;
186
- const response = await fetch(session, { method: "PUT", body: new Uint8Array(piece), headers: { "content-range": range } });
196
+ const response = await this.fetchImpl(session, { method: "PUT", body: new Uint8Array(piece), headers: { "content-range": range }, signal: options.signal });
187
197
  if (!response.ok && !(response.status === 308 && !final)) {
188
198
  fail("internal", `Resumable upload for ${objectKey} failed at ${range}: ${response.status} ${response.statusText}`, { object: objectKey, status: response.status });
189
199
  }
@@ -194,6 +204,7 @@ export class GcsUploadClient {
194
204
  await options.onState({ session, confirmedBytes: sent });
195
205
  };
196
206
  for await (const chunk of chunks) {
207
+ options.signal?.throwIfAborted();
197
208
  md5?.update(chunk);
198
209
  held.push(chunk);
199
210
  heldBytes += chunk.length;
@@ -209,16 +220,16 @@ export class GcsUploadClient {
209
220
  return { remoteChecksum: md5 ? md5.digest("base64") : null, bytes: sent };
210
221
  }
211
222
  /** Reads an object back for restore or offload-proof: a signed GET, whole. */
212
- async download(lease) {
213
- const { url } = this.sign("GET", lease.objectKey);
214
- const response = await fetch(url);
215
- if (!response.ok)
216
- fail("internal", `Could not download ${lease.objectKey}: ${response.status} ${response.statusText}`, { key: lease.objectKey, status: response.status });
217
- return Buffer.from(await response.arrayBuffer());
223
+ async download(lease, options = {}) {
224
+ return collectChunks(await this.downloadStream(lease, options), Number.MAX_SAFE_INTEGER);
225
+ }
226
+ /** The production restore/transfer door: response chunks flow straight to disk. */
227
+ async downloadStream(lease, options = {}) {
228
+ return this.downloadObjectStream(lease.objectKey, options.signal);
218
229
  }
219
230
  async head(lease) {
220
231
  const { url } = this.sign("HEAD", lease.objectKey);
221
- const response = await fetch(url, { method: "HEAD" });
232
+ const response = await this.fetchImpl(url, { method: "HEAD" });
222
233
  if (response.status === 404)
223
234
  return { exists: false, bytes: 0 };
224
235
  if (!response.ok)
@@ -1,4 +1,6 @@
1
1
  import type { ProviderKind, ProviderUploadClient, UploadLease } from "../control-plane.js";
2
+ import { type ByteStream } from "../byte-stream.js";
3
+ import type { StorageFetch } from "./safe-storage-fetch.js";
2
4
  /**
3
5
  * Google Drive as a bring-your-own storage provider (Drive API v3).
4
6
  *
@@ -17,7 +19,7 @@ import type { ProviderKind, ProviderUploadClient, UploadLease } from "../control
17
19
  */
18
20
  export declare const GDRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
19
21
  /** Placeholder until a first-party client id ships. Point SEALKEEP_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
20
- export declare const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-vaultline-placeholder.apps.googleusercontent.com";
22
+ export declare const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-sealkeep-placeholder.apps.googleusercontent.com";
21
23
  export declare function gdriveClientId(env?: NodeJS.ProcessEnv): string;
22
24
  export declare const GDRIVE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
23
25
  export declare const GDRIVE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -27,6 +29,14 @@ export type GdriveCredentials = {
27
29
  kind: "gdrive";
28
30
  refreshToken: string;
29
31
  clientId: string;
32
+ /**
33
+ * Stable, non-secret Drive grantee id returned by `about.get` as
34
+ * `user.permissionId`. It binds a refresh token to the physical Drive
35
+ * account without persisting an email address or another bearer secret.
36
+ * Optional only so credentials written by older releases can be read and
37
+ * rejected safely until the person reconnects.
38
+ */
39
+ accountId?: string;
30
40
  /** "cloud" = the refresh needs the account's server (web-client secret lives there). */
31
41
  exchange?: "cloud";
32
42
  /** Runtime-only: attached at load time, never persisted. Refreshes through the plane. */
@@ -70,6 +80,18 @@ export declare function exchangeCodeForTokens(input: {
70
80
  accessToken: string;
71
81
  expiresIn: number;
72
82
  }>;
83
+ /**
84
+ * Resolve the stable Drive identity represented by an access token.
85
+ *
86
+ * `about.get(fields=user(permissionId))` is available under the existing
87
+ * `drive.file` grant. A permission id is an opaque grantee identifier, not a
88
+ * credential and not personally-readable account metadata, so it is safe to
89
+ * retain beside the refresh token and include in destination fingerprints.
90
+ */
91
+ export declare function gdriveAccountId(accessToken: string, options?: {
92
+ apiBase?: string;
93
+ fetchImpl?: typeof fetch;
94
+ }): Promise<string>;
73
95
  /**
74
96
  * Fire-and-forget platform opener. Failing to open a browser is never failing
75
97
  * to connect — the consent URL is reported to the caller either way — so this
@@ -88,6 +110,8 @@ export type ConnectOptions = {
88
110
  fetchImpl?: typeof fetch;
89
111
  authUrl?: string;
90
112
  tokenUrl?: string;
113
+ /** Test seam for the post-consent `about.get` identity lookup. */
114
+ accountIdResolver?: (accessToken: string) => Promise<string>;
91
115
  /** Loopback port; 0 (the default) asks the OS for a random one, which is what Google's loopback flow expects. */
92
116
  port?: number;
93
117
  timeoutMs?: number;
@@ -99,10 +123,19 @@ export type ConnectOptions = {
99
123
  * flow runs offline under test.
100
124
  */
101
125
  export declare function connectGdrive(options?: ConnectOptions): Promise<GdriveCredentials>;
126
+ export type GdriveStreamOptions = {
127
+ /**
128
+ * Drive requires the final object length when a resumable session starts.
129
+ * File-backed archive uploads measure this in a bounded first pass and pass
130
+ * it here; an unknown-length source must be staged before it can use Drive.
131
+ */
132
+ bytes?: number;
133
+ signal?: AbortSignal;
134
+ };
102
135
  /**
103
136
  * Drive upload client.
104
137
  *
105
- * A lease's objectKey (e.g. `vaultline/<id>.vlarchive`) has no path meaning in
138
+ * A lease's objectKey (e.g. `vaultline/<id>.skarchive`) has no path meaning in
106
139
  * Drive, so the mapping is: files live flat in one folder (or the hidden
107
140
  * appDataFolder space), named by the key's basename, and each carries
108
141
  * `appProperties.vaultlineKey = <full objectKey>`. head/download locate by that
@@ -119,7 +152,7 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
119
152
  private readonly now;
120
153
  constructor(endpoint: GdriveEndpoint, credentials: GdriveCredentials, options?: {
121
154
  chunkBytes?: number;
122
- fetchImpl?: typeof fetch;
155
+ fetchImpl?: StorageFetch;
123
156
  now?: () => number;
124
157
  });
125
158
  private base;
@@ -129,9 +162,14 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
129
162
  private token;
130
163
  private api;
131
164
  private list;
132
- /** Exact lookup by the appProperties key. Two files claiming one key is a state we refuse to guess about. */
165
+ /**
166
+ * Exact lookup inside this configured physical destination. Object keys are
167
+ * intentionally reusable across Drive folders, so the immutable parent id
168
+ * and Drive space are part of every lookup rather than merely creation
169
+ * metadata.
170
+ */
133
171
  private findByKey;
134
- /** Finds or creates the archive folder; the id is cached per instance. */
172
+ /** Resolves the archive folder to its immutable Drive id, creating it only for uploads. */
135
173
  private parentId;
136
174
  /**
137
175
  * Starts a resumable session (create, or in-place update when the key already
@@ -144,13 +182,38 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
144
182
  remoteChecksum: string;
145
183
  bytes: number;
146
184
  }>;
185
+ /**
186
+ * Uploads a known-length object through Drive's resumable protocol while
187
+ * retaining at most one configured resumable part. The archive pipeline
188
+ * measures a file in a bounded first pass, then this second pass computes
189
+ * Drive's whole-object MD5 as bytes are copied into fixed-size PUTs.
190
+ *
191
+ * Drive requires every non-final chunk to be a positive multiple of 256 KiB.
192
+ * The configured `chunkBytes` already enforces that rule; the last PUT may be
193
+ * short. A 308 response is accepted only for a non-final PUT, and the final
194
+ * PUT must be a success, so an early-finalized or half-accepted session can
195
+ * never be reported as complete.
196
+ */
197
+ uploadStream(objectKey: string, chunks: ByteStream, options?: GdriveStreamOptions): Promise<{
198
+ remoteChecksum: string;
199
+ bytes: number;
200
+ }>;
147
201
  head(lease: UploadLease): Promise<{
148
202
  exists: boolean;
149
203
  bytes: number;
150
204
  checksum?: string;
151
205
  }>;
152
206
  /** Reads an object back for restore or offload-proof: locate by key, then `alt=media`. */
153
- /** Removes the file holding this object key. Absent already means done. */
154
- deleteByKey(objectKey: string): Promise<void>;
155
- download(lease: UploadLease): Promise<Buffer>;
207
+ /**
208
+ * Removes the file holding this object key in this exact destination. An
209
+ * empty successful scoped listing proves absence. A DELETE 404 is rechecked
210
+ * in the same scope before callers may discard their ledger metadata.
211
+ */
212
+ deleteByKey(objectKey: string): Promise<"deleted" | "absent">;
213
+ download(lease: UploadLease, options?: {
214
+ signal?: AbortSignal;
215
+ }): Promise<Buffer>;
216
+ downloadStream(lease: UploadLease, options?: {
217
+ signal?: AbortSignal;
218
+ }): Promise<ByteStream>;
156
219
  }