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
@@ -1,4 +1,4 @@
1
- import type { ArchiveRecordV2 } from "./types.js";
1
+ import { type ArchiveRecordV2 } from "./types.js";
2
2
  /**
3
3
  * Seal straight into the bucket: read a slice, compress it, encrypt it, send
4
4
  * it, forget it. The full ciphertext exists nowhere — not on this disk, not in
@@ -72,5 +72,9 @@ export declare function sealArchiveToCloud(dataDir: string, sourcePath: string,
72
72
  /** Resume an interrupted upload of this source when a journal for it exists. Defaults on; `--fresh` turns it off. */
73
73
  resume?: boolean;
74
74
  now?: number;
75
+ /** Storage-route identity stamped on the verified copy. */
76
+ targetId?: string;
77
+ project?: string | null;
78
+ projectKey?: string | null;
75
79
  }): Promise<StreamSealOutcome>;
76
80
  export {};
@@ -2,20 +2,22 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { open, stat } from "node:fs/promises";
3
3
  import { gzipSync } from "node:zlib";
4
4
  import { join, resolve } from "node:path";
5
- import { fail, isVaultlineError } from "./errors.js";
5
+ import { fail, isSealkeepError } from "./errors.js";
6
6
  import { recordAudit } from "./audit.js";
7
7
  import { canonicalPhrase } from "./mnemonic.js";
8
- import { ADAPTER_VERSION, configuredRecipients, readConfig, writeRecord } from "./vault.js";
8
+ import { isV2 } from "./types.js";
9
+ import { ADAPTER_VERSION, configuredRecipients, mutateArchiveRecord, readConfig, teamSpaceOf } from "./vault.js";
10
+ import { appendArchiveCopy } from "./archive-copies.js";
9
11
  import { uploadClientFromStore } from "./providers/index.js";
10
12
  import { compositeChecksum } from "./providers/s3.js";
11
13
  import { acquireSpoolLock, createSpool, findSpoolForSource, shredSpool, unwrapSpoolKey, updateSpool } from "./spool.js";
12
- import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, StreamingSha256, TAG_BYTES, chunkAad, chunkNonce, hashFileRange, sealChunksToSink, wrapAll, zeroize } from "../packages/vaultline-crypto/src/index.js";
14
+ import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, StreamingSha256, TAG_BYTES, chunkAad, chunkNonce, hashFileRange, sealChunksToSink, wrapAll, zeroize } from "../packages/sealkeep-crypto/src/index.js";
13
15
  // The package's index deliberately exports formats and flows, not raw AEAD
14
16
  // primitives. Resuming needs exactly one primitive operation the flows do not
15
17
  // expose: re-producing a single chunk's ciphertext to prove the source still
16
18
  // seals to the bytes the provider already holds. Reaching into the package for
17
19
  // that beats re-implementing the cipher setup here and drifting from it.
18
- import { aeadCipher, aeadDecipher } from "../packages/vaultline-crypto/src/aead.js";
20
+ import { aeadCipher, aeadDecipher } from "../packages/sealkeep-crypto/src/aead.js";
19
21
  const STREAMABLE = new Set(["s3", "r2", "b2", "gcs"]);
20
22
  const SEAL_SUITE = "chacha20-poly1305";
21
23
  const sha256hex = (input) => createHash("sha256").update(input).digest("hex");
@@ -123,7 +125,7 @@ function interpretProviderState(state) {
123
125
  * Anything else (network drop, full disk, a provider 5xx) keeps the journal.
124
126
  */
125
127
  function resumeRefused(error) {
126
- if (!isVaultlineError(error))
128
+ if (!isSealkeepError(error))
127
129
  return false;
128
130
  return /cannot resume/i.test(error.message) || /resumable session/.test(error.message) || /NoSuchUpload/.test(error.message);
129
131
  }
@@ -182,7 +184,7 @@ export async function sealArchiveToCloud(dataDir, sourcePath, rawPhrase, agent,
182
184
  fail("invalid_argument", `The ${remoteStorage.provider} client cannot stream uploads`);
183
185
  }
184
186
  const client = rawClient;
185
- const shared = { dataDir, client, config, remoteStorage, absolute, totalBytes: source.size, phrase, agent, now: options.now };
187
+ const shared = { dataDir, client, config, remoteStorage, absolute, totalBytes: source.size, phrase, agent, project: options.project ?? null, projectKey: options.projectKey ?? null, targetId: options.targetId, now: options.now };
186
188
  // Journal triage. Each pass either resumes the newest journal for this
187
189
  // source or abandons it with the reason and looks again, so a stack of
188
190
  // crashed attempts drains to the one worth continuing — or to none.
@@ -395,20 +397,38 @@ async function verifyAndRecord(ctx, flight) {
395
397
  const now = new Date(ctx.now ?? Date.now()).toISOString();
396
398
  // `remote.checksum` stays OUR whole-object stored SHA-256 (base64), resumed
397
399
  // or not — the resumable hasher covered every stored byte across processes.
398
- const remote = { provider: ctx.remoteStorage.provider, bucket: ctx.remoteStorage.bucket, objectKey, bytes: flight.wholeStoredBytes, checksum: flight.hashers.stored.digestBase64(), verifiedAt: now };
399
- const record = {
400
+ const remote = {
401
+ provider: ctx.remoteStorage.provider,
402
+ bucket: ctx.remoteStorage.bucket,
403
+ objectKey,
404
+ bytes: flight.wholeStoredBytes,
405
+ checksum: flight.hashers.stored.digestBase64(),
406
+ verifiedAt: now,
407
+ ...(ctx.targetId ? { targetId: ctx.targetId } : {})
408
+ };
409
+ const completed = {
400
410
  version: 2, id: archiveId, createdAt: flight.envelope.manifest.createdAt,
401
- source: { path: ctx.absolute, agent: ctx.agent, bytes: ctx.totalBytes, sha256: flight.hashers.plaintext.digestHex() },
411
+ source: {
412
+ path: ctx.absolute, agent: ctx.agent, bytes: ctx.totalBytes, sha256: flight.hashers.plaintext.digestHex(),
413
+ ...(ctx.project ? { project: ctx.project } : {}),
414
+ ...(ctx.projectKey ? { projectKey: ctx.projectKey } : {}),
415
+ ...(teamSpaceOf(ctx.config, ctx.project, ctx.projectKey)?.spaceKey ? { projectScope: teamSpaceOf(ctx.config, ctx.project, ctx.projectKey).spaceKey } : {}),
416
+ },
402
417
  cipher: { algorithm: flight.envelope.suite, ciphertextSha256: flight.hashers.stored.digestHex(), storedBytes: flight.wholeStoredBytes, chunks: flight.envelope.chunks.length },
403
418
  envelope: flight.envelope,
404
419
  // Born offloaded: this path names where the blob would live, and the
405
420
  // offloaded marker says truthfully that no file is there — the same state
406
421
  // an uploaded-then-offloaded archive reaches, arrived at without the detour.
407
- objectPath: join(ctx.config.storage.root, `${archiveId}.vlarchive`),
422
+ objectPath: join(ctx.config.storage.root, `${archiveId}.skarchive`),
408
423
  remote,
409
424
  offloaded: { at: now, provider: ctx.remoteStorage.provider, bucket: ctx.remoteStorage.bucket, objectKey }
410
425
  };
411
- await writeRecord(ctx.config.storage.root, record);
426
+ const record = await mutateArchiveRecord(ctx.config.storage.root, archiveId, (current) => {
427
+ if (!isV2(current))
428
+ fail("invalid_argument", `Archive ${archiveId} uses format v1 and cannot receive a streamed copy`, { archiveId });
429
+ const { remote: _remote, copies: _copies, ...completedFacts } = completed;
430
+ return appendArchiveCopy({ ...current, ...completedFacts }, remote, { makePrimary: true });
431
+ }, { initial: completed });
412
432
  await recordAudit(ctx.dataDir, "archive.create", "allowed", { archiveId, agent: ctx.agent, bytes: ctx.totalBytes, recipients: flight.envelope.wrappedKeys.length, streamed: true, ...(flight.resumed ? { resumed: true } : {}) });
413
433
  await recordAudit(ctx.dataDir, "upload.verify", "allowed", { archiveId, objectKey, bytes: flight.wholeStoredBytes, provider: ctx.remoteStorage.provider, streamed: true, ...(flight.resumed ? { resumed: true } : {}) });
414
434
  await shredSpool(ctx.dataDir, archiveId);
@@ -419,10 +439,10 @@ async function verifyAndRecord(ctx, flight) {
419
439
  async function freshSeal(ctx) {
420
440
  const archiveId = randomUUID();
421
441
  const prefix = ctx.remoteStorage.prefix.replace(/^\/+|\/+$/g, "");
422
- const objectKey = `${prefix}/${archiveId}.vlarchive`;
442
+ const objectKey = `${prefix}/${archiveId}.skarchive`;
423
443
  const chunkBytes = DEFAULT_CHUNK_BYTES;
424
444
  const totalChunks = Math.max(1, Math.ceil(ctx.totalBytes / chunkBytes));
425
- const recipients = configuredRecipients(ctx.config, ctx.phrase);
445
+ const recipients = configuredRecipients(ctx.config, ctx.phrase, { project: ctx.project ?? undefined, projectKey: ctx.projectKey ?? undefined });
426
446
  // The key is minted here, not inside the seal, because the journal must
427
447
  // carry it (wrapped for the same recipients an envelope would name) BEFORE
428
448
  // the first byte goes out — a journal written any later could describe
@@ -520,7 +540,7 @@ async function resumeSeal(ctx, journal) {
520
540
  const archiveKey = unwrapSpoolKey(journal, { phrase: ctx.phrase });
521
541
  try {
522
542
  const suite = journal.suite;
523
- const recipients = configuredRecipients(ctx.config, ctx.phrase);
543
+ const recipients = configuredRecipients(ctx.config, ctx.phrase, { project: ctx.project ?? undefined, projectKey: ctx.projectKey ?? undefined });
524
544
  /**
525
545
  * Re-seals chunks [0, count) from the CURRENT source, verifying each
526
546
  * against the journaled header — and, when the S3 part map is known,
@@ -31,7 +31,7 @@ export declare const DEFAULT_SYNC_RULES: SyncRules;
31
31
  * response, one file, everything it said.
32
32
  */
33
33
  export type AccountSnapshot = {
34
- plan: "free" | "pro_lite" | "pro" | null;
34
+ plan: "free" | "pro_lite" | "pro" | "team" | null;
35
35
  email: string | null;
36
36
  quota: {
37
37
  usedBytes: number;
@@ -40,12 +40,39 @@ export type AccountSnapshot = {
40
40
  /** When the control plane last actually answered — this file's write time, in the data. */
41
41
  fetchedAt: string | null;
42
42
  };
43
- export declare function cacheSyncRules(dataDir: string, raw: unknown): Promise<SyncRules>;
43
+ export declare function cacheSyncRules(dataDir: string, raw: unknown, options?: {
44
+ authGeneration?: string | null;
45
+ }): Promise<SyncRules>;
44
46
  export declare function cachedSyncRules(dataDir: string): Promise<SyncRules | null>;
45
47
  /** The last account answer this machine ever received; null on a vault that never had one. */
46
48
  export declare function cachedAccount(dataDir: string): Promise<AccountSnapshot | null>;
47
49
  /** How long the last account answer is served as-is before asking again. */
48
50
  export declare const ACCOUNT_TTL_MS: number;
51
+ /** How long dashboard polling backs off after a best-effort Cloud refresh fails. */
52
+ export declare const ACCOUNT_REFRESH_RETRY_MS = 30000;
53
+ /** A display refresh may be stale, but it may never remain in flight forever. */
54
+ export declare const DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS = 10000;
55
+ export type AccountResolution = {
56
+ account: AccountSnapshot;
57
+ source: "account" | "cache" | "none";
58
+ };
59
+ type DashboardAccountDependencies = {
60
+ cloudAuthGeneration?: (dataDir: string) => Promise<string | null>;
61
+ accountStatus?: (dataDir: string, signal?: AbortSignal) => Promise<unknown>;
62
+ now?: () => number;
63
+ /** Deterministic test seam; production uses the fixed display deadline. */
64
+ requestTimeoutMs?: number;
65
+ };
66
+ /**
67
+ * Dashboard-only stale-while-refresh account resolution.
68
+ *
69
+ * A page render never waits for the control plane: a matching cached account
70
+ * is returned immediately (labelled `cache` when stale), while one detached
71
+ * refresh updates the next read. A never-linked vault returns `none`
72
+ * immediately. The blocking `resolveAccount` below intentionally keeps its old
73
+ * semantics for entitlement and action paths that require a live decision.
74
+ */
75
+ export declare function resolveDashboardAccount(dataDir: string, dependencies?: DashboardAccountDependencies): Promise<AccountResolution>;
49
76
  /**
50
77
  * The account, remembered if recent, live if stale, remembered again if the
51
78
  * control plane is unreachable.
@@ -57,10 +84,7 @@ export declare const ACCOUNT_TTL_MS: number;
57
84
  * put a ~4-second network round trip inside GET /v1/status, which every page's
58
85
  * identity card and plan bar sat waiting on.
59
86
  */
60
- export declare function resolveAccount(dataDir: string): Promise<{
61
- account: AccountSnapshot;
62
- source: "account" | "cache" | "none";
63
- }>;
87
+ export declare function resolveAccount(dataDir: string): Promise<AccountResolution>;
64
88
  /**
65
89
  * Reads the rules from the account, caching them; falls back to the last cached
66
90
  * copy when the control plane cannot be reached, and only to "everything" when
@@ -83,3 +107,4 @@ export declare function applySyncRules(found: readonly TranscriptCandidate[], ru
83
107
  heldBack: TranscriptCandidate[];
84
108
  };
85
109
  export declare function describeSyncRules(rules: SyncRules): string;
110
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { join, resolve as resolvePath } from "node:path";
3
3
  export const DEFAULT_SYNC_RULES = { mode: "all", keepLast: 20, minAgeDays: 7 };
4
4
  const CACHE = "sync-rules.json";
5
5
  function coerce(raw) {
@@ -14,7 +14,7 @@ function coerce(raw) {
14
14
  }
15
15
  /** Reads both shapes: the control plane's snake_case reply, and this file's own saved snapshot. */
16
16
  function coerceAccount(raw) {
17
- const plan = raw?.plan === "pro" || raw?.plan === "pro_lite" || raw?.plan === "free" ? raw.plan : null;
17
+ const plan = raw?.plan === "team" || raw?.plan === "pro" || raw?.plan === "pro_lite" || raw?.plan === "free" ? raw.plan : null;
18
18
  const used = Number(raw?.quota?.usedBytes ?? raw?.used_bytes);
19
19
  const limit = Number(raw?.quota?.quotaBytes ?? raw?.quota_bytes);
20
20
  return {
@@ -24,10 +24,18 @@ function coerceAccount(raw) {
24
24
  fetchedAt: typeof raw?.fetchedAt === "string" ? raw.fetchedAt : null
25
25
  };
26
26
  }
27
- export async function cacheSyncRules(dataDir, raw) {
27
+ export async function cacheSyncRules(dataDir, raw, options = {}) {
28
28
  const rules = coerce(raw);
29
29
  const account = coerceAccount(raw);
30
- await writeFile(join(dataDir, CACHE), JSON.stringify({ ...rules, ...account, fetchedAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
30
+ // Account data must be tied to the Cloud login that produced it. Without
31
+ // this marker a logout followed by another person's login could briefly show
32
+ // the first person's email, plan and quota while /me was still in flight.
33
+ // Callers that already sampled the generation pass it explicitly; ordinary
34
+ // action paths pay only one local-file read here.
35
+ const authGeneration = Object.hasOwn(options, "authGeneration")
36
+ ? options.authGeneration ?? null
37
+ : await import("./cloud.js").then(({ cloudAuthGeneration }) => cloudAuthGeneration(dataDir)).catch(() => null);
38
+ await writeFile(join(dataDir, CACHE), JSON.stringify({ ...rules, ...account, authGeneration, fetchedAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
31
39
  return rules;
32
40
  }
33
41
  export async function cachedSyncRules(dataDir) {
@@ -39,21 +47,140 @@ export async function cachedSyncRules(dataDir) {
39
47
  return null;
40
48
  }
41
49
  }
42
- /** The last account answer this machine ever received; null on a vault that never had one. */
43
- export async function cachedAccount(dataDir) {
50
+ async function cachedAccountRecord(dataDir) {
44
51
  try {
45
52
  const saved = JSON.parse(await readFile(join(dataDir, CACHE), "utf8"));
46
53
  const account = coerceAccount(saved);
47
54
  // A pre-snapshot cache file (rules only) has no account half; report null
48
55
  // rather than an all-null snapshot pretending the account said nothing.
49
- return account.plan === null && account.email === null && account.quota === null ? null : account;
56
+ if (account.plan === null && account.email === null && account.quota === null)
57
+ return null;
58
+ const authGeneration = typeof saved?.authGeneration === "string"
59
+ ? saved.authGeneration
60
+ : saved?.authGeneration === null
61
+ ? null
62
+ : undefined;
63
+ return { account, authGeneration };
50
64
  }
51
65
  catch {
52
66
  return null;
53
67
  }
54
68
  }
69
+ /** The last account answer this machine ever received; null on a vault that never had one. */
70
+ export async function cachedAccount(dataDir) {
71
+ return (await cachedAccountRecord(dataDir))?.account ?? null;
72
+ }
55
73
  /** How long the last account answer is served as-is before asking again. */
56
74
  export const ACCOUNT_TTL_MS = 5 * 60_000;
75
+ /** How long dashboard polling backs off after a best-effort Cloud refresh fails. */
76
+ export const ACCOUNT_REFRESH_RETRY_MS = 30_000;
77
+ /** A display refresh may be stale, but it may never remain in flight forever. */
78
+ export const DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS = 10_000;
79
+ const NO_ACCOUNT = { plan: null, email: null, quota: null, fetchedAt: null };
80
+ /** One live refresh per vault *and Cloud login*, shared by simultaneous UI reads. */
81
+ const dashboardAccountRefreshes = new Map();
82
+ /** A timestamp rather than a timer: this never keeps the daemon alive. */
83
+ const dashboardAccountRetryAt = new Map();
84
+ const accountIsFresh = (account, now) => {
85
+ if (!account.fetchedAt)
86
+ return false;
87
+ const fetchedAt = Date.parse(account.fetchedAt);
88
+ return Number.isFinite(fetchedAt) && now - fetchedAt < ACCOUNT_TTL_MS;
89
+ };
90
+ const cacheBelongsToGeneration = (record, generation) => (
91
+ // A legacy cache can only be used on a legacy machine which has never
92
+ // emitted an auth generation. Every login/logout/renewal now emits one.
93
+ record.authGeneration === undefined ? generation === null : record.authGeneration === generation);
94
+ async function dashboardCloudGeneration(dataDir, dependencies) {
95
+ if (dependencies.cloudAuthGeneration)
96
+ return dependencies.cloudAuthGeneration(dataDir);
97
+ const { cloudAuthGeneration } = await import("./cloud.js");
98
+ return cloudAuthGeneration(dataDir);
99
+ }
100
+ async function dashboardAccountStatus(dataDir, dependencies) {
101
+ const controller = new AbortController();
102
+ const timeoutMs = dependencies.requestTimeoutMs ?? DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS;
103
+ let timer;
104
+ const timedOut = new Promise((_, reject) => {
105
+ timer = setTimeout(() => {
106
+ controller.abort();
107
+ reject(new Error("Sealkeep Cloud account display refresh timed out"));
108
+ }, timeoutMs);
109
+ timer.unref?.();
110
+ });
111
+ const work = dependencies.accountStatus
112
+ ? dependencies.accountStatus(dataDir, controller.signal)
113
+ : import("./cloud.js").then(({ accountStatus }) => accountStatus(dataDir, undefined, {
114
+ signal: controller.signal,
115
+ // A passive poll must not start the process-wide token renewal which a
116
+ // later write may need to await. An expired snapshot simply leaves the
117
+ // last authenticated account cache visible.
118
+ renewToken: false,
119
+ }));
120
+ try {
121
+ return await Promise.race([work, timedOut]);
122
+ }
123
+ finally {
124
+ if (timer)
125
+ clearTimeout(timer);
126
+ }
127
+ }
128
+ function refreshDashboardAccount(dataDir, authGeneration, dependencies) {
129
+ const key = `${resolvePath(dataDir)}\0${authGeneration ?? "<no-auth-generation>"}`;
130
+ if (dashboardAccountRefreshes.has(key))
131
+ return;
132
+ const now = dependencies.now?.() ?? Date.now();
133
+ if ((dashboardAccountRetryAt.get(key) ?? 0) > now)
134
+ return;
135
+ const refresh = (async () => {
136
+ try {
137
+ const raw = await dashboardAccountStatus(dataDir, dependencies);
138
+ // A /me request can outlive logout, another login, or token renewal.
139
+ // Never let that old answer overwrite the identity now using the vault.
140
+ if (await dashboardCloudGeneration(dataDir, dependencies) !== authGeneration)
141
+ return;
142
+ await cacheSyncRules(dataDir, raw, { authGeneration });
143
+ dashboardAccountRetryAt.delete(key);
144
+ }
145
+ catch {
146
+ // Dashboard refresh is deliberately best-effort. The stale local answer
147
+ // remains useful, and this detached promise must never become unhandled.
148
+ // Only back off the login which actually failed; if auth changed while
149
+ // /me was pending, the new login gets its own immediate refresh.
150
+ const currentGeneration = await dashboardCloudGeneration(dataDir, dependencies).catch(() => null);
151
+ if (currentGeneration === authGeneration) {
152
+ dashboardAccountRetryAt.set(key, (dependencies.now?.() ?? Date.now()) + ACCOUNT_REFRESH_RETRY_MS);
153
+ }
154
+ }
155
+ })();
156
+ dashboardAccountRefreshes.set(key, refresh);
157
+ void refresh.then(() => { if (dashboardAccountRefreshes.get(key) === refresh)
158
+ dashboardAccountRefreshes.delete(key); }, () => { if (dashboardAccountRefreshes.get(key) === refresh)
159
+ dashboardAccountRefreshes.delete(key); });
160
+ }
161
+ /**
162
+ * Dashboard-only stale-while-refresh account resolution.
163
+ *
164
+ * A page render never waits for the control plane: a matching cached account
165
+ * is returned immediately (labelled `cache` when stale), while one detached
166
+ * refresh updates the next read. A never-linked vault returns `none`
167
+ * immediately. The blocking `resolveAccount` below intentionally keeps its old
168
+ * semantics for entitlement and action paths that require a live decision.
169
+ */
170
+ export async function resolveDashboardAccount(dataDir, dependencies = {}) {
171
+ const [authGeneration, cached] = await Promise.all([
172
+ dashboardCloudGeneration(dataDir, dependencies).catch(() => null),
173
+ cachedAccountRecord(dataDir),
174
+ ]);
175
+ const matching = cached && cacheBelongsToGeneration(cached, authGeneration) ? cached.account : null;
176
+ if (matching && accountIsFresh(matching, dependencies.now?.() ?? Date.now())) {
177
+ return { account: matching, source: "account" };
178
+ }
179
+ refreshDashboardAccount(dataDir, authGeneration, dependencies);
180
+ return matching
181
+ ? { account: matching, source: "cache" }
182
+ : { account: NO_ACCOUNT, source: "none" };
183
+ }
57
184
  /**
58
185
  * The account, remembered if recent, live if stale, remembered again if the
59
186
  * control plane is unreachable.
@@ -66,21 +193,33 @@ export const ACCOUNT_TTL_MS = 5 * 60_000;
66
193
  * identity card and plan bar sat waiting on.
67
194
  */
68
195
  export async function resolveAccount(dataDir) {
69
- const recent = await cachedAccount(dataDir);
70
- if (recent?.fetchedAt && Date.now() - Date.parse(recent.fetchedAt) < ACCOUNT_TTL_MS) {
196
+ const authGeneration = await dashboardCloudGeneration(dataDir, {}).catch(() => null);
197
+ const cachedRecord = await cachedAccountRecord(dataDir);
198
+ const recent = cachedRecord && cacheBelongsToGeneration(cachedRecord, authGeneration)
199
+ ? cachedRecord.account
200
+ : null;
201
+ if (recent && accountIsFresh(recent, Date.now())) {
71
202
  return { account: recent, source: "account" };
72
203
  }
73
204
  try {
74
205
  const { accountStatus } = await import("./cloud.js");
75
206
  const raw = await accountStatus(dataDir);
76
- await cacheSyncRules(dataDir, raw);
207
+ // The request authenticated before it left. If login/logout/renewal raced
208
+ // it, its answer belongs to the old session and must neither authorize an
209
+ // action nor overwrite the new session's cache.
210
+ if (await dashboardCloudGeneration(dataDir, {}).catch(() => null) !== authGeneration) {
211
+ return { account: NO_ACCOUNT, source: "none" };
212
+ }
213
+ await cacheSyncRules(dataDir, raw, { authGeneration });
77
214
  return { account: { ...coerceAccount(raw), fetchedAt: new Date().toISOString() }, source: "account" };
78
215
  }
79
216
  catch {
80
- const cached = await cachedAccount(dataDir);
81
- return cached
82
- ? { account: cached, source: "cache" }
83
- : { account: { plan: null, email: null, quota: null, fetchedAt: null }, source: "none" };
217
+ const currentGeneration = await dashboardCloudGeneration(dataDir, {}).catch(() => null);
218
+ const fallback = await cachedAccountRecord(dataDir);
219
+ const matching = fallback && cacheBelongsToGeneration(fallback, currentGeneration) ? fallback.account : null;
220
+ return matching
221
+ ? { account: matching, source: "cache" }
222
+ : { account: NO_ACCOUNT, source: "none" };
84
223
  }
85
224
  }
86
225
  /**
@@ -0,0 +1,8 @@
1
+ import type { TeamHistoryBackfillJob } from "./team-backfill.js";
2
+ /**
3
+ * Prefer recent work, promoting every base it still needs. Every fourth claim
4
+ * instead takes the longest-waiting eligible job, so new work and repeated
5
+ * failures cannot monopolize that lane. This only orders candidates; dependency/auth/claim gates stay with
6
+ * the durable ledger owner.
7
+ */
8
+ export declare function orderTeamBackfillJobs(jobs: TeamHistoryBackfillJob[], eligible: TeamHistoryBackfillJob[], sequence: number): TeamHistoryBackfillJob[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Prefer recent work, promoting every base it still needs. Every fourth claim
3
+ * instead takes the longest-waiting eligible job, so new work and repeated
4
+ * failures cannot monopolize that lane. This only orders candidates; dependency/auth/claim gates stay with
5
+ * the durable ledger owner.
6
+ */
7
+ export function orderTeamBackfillJobs(jobs, eligible, sequence) {
8
+ const stamp = (job) => {
9
+ const value = Date.parse(job.sourceCreatedAt ?? job.createdAt);
10
+ return Number.isFinite(value) ? value : 0;
11
+ };
12
+ const oldest = (a, b) => (a.lastScheduledAt ?? a.createdAt).localeCompare(b.lastScheduledAt ?? b.createdAt)
13
+ || a.createdAt.localeCompare(b.createdAt) || stamp(a) - stamp(b) || a.id.localeCompare(b.id);
14
+ if (sequence % 4 === 3)
15
+ return [...eligible].sort(oldest);
16
+ const byId = new Map(jobs.map((job) => [job.id, job]));
17
+ const priority = new Map();
18
+ for (const source of [...jobs].sort((a, b) => stamp(b) - stamp(a))) {
19
+ const value = stamp(source);
20
+ const pending = [source.id];
21
+ while (pending.length) {
22
+ const id = pending.pop();
23
+ if (priority.has(id) && priority.get(id) >= value)
24
+ continue;
25
+ const job = byId.get(id);
26
+ if (!job)
27
+ continue;
28
+ priority.set(id, value);
29
+ pending.push(...job.dependencyJobIds);
30
+ }
31
+ }
32
+ return [...eligible].sort((a, b) => (priority.get(b.id) ?? 0) - (priority.get(a.id) ?? 0) || oldest(a, b));
33
+ }
@@ -0,0 +1,116 @@
1
+ import { type Envelope } from "../packages/sealkeep-crypto/src/index.js";
2
+ import type { TeamRosterWire } from "./cloud.js";
3
+ import { type ArchiveRecordV2, type RemoteObject } from "./types.js";
4
+ export type TeamHistoryBackfillStage = "pending" | "prepared" | "uploaded" | "registered" | "indexed" | "blocked" | "superseded";
5
+ export type TeamHistoryBackfillJob = {
6
+ version: 1;
7
+ id: string;
8
+ shareRef: string;
9
+ sourceArchiveId: string;
10
+ /** Source archive creation time, not the time a backlog was discovered. */
11
+ sourceCreatedAt?: string;
12
+ /** Scheduling fairness, separate from renewal/progress timestamps. */
13
+ lastScheduledAt?: string;
14
+ spaceKey: string;
15
+ project: string;
16
+ projectKey?: string;
17
+ rosterHash: string;
18
+ membership_version?: number;
19
+ memberPublicKeys: string[];
20
+ dependencyJobIds: string[];
21
+ dependencyRefs: string[];
22
+ stage: TeamHistoryBackfillStage;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ attempts: number;
26
+ nextAttemptAt?: string;
27
+ lastError?: string;
28
+ blockedReason?: string;
29
+ envelope?: Envelope;
30
+ remote?: RemoteObject;
31
+ claim?: {
32
+ token: string;
33
+ pid: number;
34
+ processId?: string;
35
+ processStart?: string;
36
+ at: number;
37
+ };
38
+ cleanupNeeded?: boolean;
39
+ cleanupError?: string;
40
+ };
41
+ export type TeamHistoryBackfillSpaceStatus = {
42
+ state: "idle" | "working" | "attention";
43
+ total: number;
44
+ indexed: number;
45
+ registered: number;
46
+ pending: number;
47
+ failed: number;
48
+ blocked: number;
49
+ activeArchiveId?: string;
50
+ lastError?: string;
51
+ };
52
+ export type TeamHistoryBackfillStatus = {
53
+ state: "idle" | "working" | "attention";
54
+ total: number;
55
+ indexed: number;
56
+ registered: number;
57
+ pending: number;
58
+ failed: number;
59
+ blocked: number;
60
+ lastError?: string;
61
+ spaces: Record<string, TeamHistoryBackfillSpaceStatus>;
62
+ };
63
+ export type TeamHistoryBackfillOutcome = TeamHistoryBackfillStatus & {
64
+ scheduled: number;
65
+ processed: number;
66
+ uploadedNow: number;
67
+ registeredNow: number;
68
+ };
69
+ export type TeamHistoryBackfillPublication = {
70
+ jobId: string;
71
+ sourceArchiveId: string;
72
+ record: ArchiveRecordV2;
73
+ };
74
+ export type TeamHistoryBackfillInventoryDependencies = {
75
+ readArchiveNames?: (storageRoot: string) => Promise<string[]>;
76
+ statArchive?: (path: string) => Promise<{
77
+ size: number;
78
+ mtimeMs: number;
79
+ } | null>;
80
+ yieldAfterStat?: () => Promise<void>;
81
+ };
82
+ /**
83
+ * Discover exact old-history work and persist it before any bytes move.
84
+ * Network failures leave the previous durable plan alone; they never turn an
85
+ * unknown roster into a guessed recipient set.
86
+ */
87
+ export declare function scheduleTeamHistoryBackfill(dataDir: string, now?: number, inventory?: TeamHistoryBackfillInventoryDependencies, options?: {
88
+ signal?: AbortSignal;
89
+ idleMs?: number;
90
+ }): Promise<number>;
91
+ /** Drain a bounded number of durable jobs. Every stage is committed separately. */
92
+ export declare function drainTeamHistoryBackfill(dataDir: string, rawPhrase: string, options?: {
93
+ limit?: number;
94
+ now?: () => number;
95
+ signal?: AbortSignal;
96
+ idleMs?: number;
97
+ maxBatchMs?: number;
98
+ }): Promise<{
99
+ processed: number;
100
+ uploaded: number;
101
+ registered: number;
102
+ }>;
103
+ export declare function teamHistoryBackfillStatus(dataDir: string): Promise<TeamHistoryBackfillStatus>;
104
+ export declare function reconcileTeamHistoryBackfill(dataDir: string, phrase: string, options?: {
105
+ limit?: number;
106
+ now?: () => number;
107
+ inventory?: TeamHistoryBackfillInventoryDependencies;
108
+ signal?: AbortSignal;
109
+ idleMs?: number;
110
+ maxBatchMs?: number;
111
+ }): Promise<TeamHistoryBackfillOutcome>;
112
+ /** Current, verified, membership-wrapped jobs suitable for a team shard. */
113
+ export declare function teamHistoryBackfillPublications(dataDir: string, spaceKey: string, currentRosterSnapshot: TeamRosterWire): Promise<TeamHistoryBackfillPublication[]>;
114
+ /** All refs ever produced for this space, used only to remove stale roster aliases. */
115
+ export declare function allTeamHistoryBackfillRefs(dataDir: string, spaceKey: string): Promise<Set<string>>;
116
+ export declare function markTeamHistoryBackfillsIndexed(dataDir: string, jobIds: readonly string[]): Promise<void>;