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,495 @@
1
+ /**
2
+ * The segmented content index in use: the manifest and its segments become a
3
+ * `ContentIndex` a lookup can read, a seal appends one small segment instead
4
+ * of rewriting the whole vault, and a catch-up build streams archives into
5
+ * bounded batches instead of holding everything in memory at once.
6
+ *
7
+ * This is the integration layer over the two finished, independently tested
8
+ * pieces: `index-segments.ts` (one sealed segment) and `index-manifest.ts`
9
+ * (the list of them, and compaction). Nothing here seals or opens ciphertext
10
+ * directly — every crypto operation goes through those two modules.
11
+ *
12
+ * Segments mode is opt-in per vault: it is on once
13
+ * `<dataDir>/index/segments/manifest.json` exists with at least one segment
14
+ * (see `segmentsEnabled`). Until `sealkeep index migrate` runs, a vault stays
15
+ * on the legacy single-blob path untouched.
16
+ */
17
+ import { randomUUID } from "node:crypto";
18
+ import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
19
+ import { join } from "node:path";
20
+ import { canonicalPhrase } from "./mnemonic.js";
21
+ import { appendSegment, compactSegments, mergeSegmentRows, orphanSegmentIds, planCompaction, readManifest, retireArchives, updateManifest, } from "./index-manifest.js";
22
+ import { openSegment, removeSegment, writeSegment } from "./index-segments.js";
23
+ import { MAX_POSTINGS_PER_TOKEN, } from "./index-segment-types.js";
24
+ import { applyArchiveTokens, chronologicalPostings, INDEX_EXTRACTION_POLICY, loadContentIndex, normalizeContentIndex, persistContentIndex, } from "./search.js";
25
+ import { remoteIndexStoreFor } from "./index-sync.js";
26
+ import { publishSegments, writePublishedBlobState } from "./index-publish.js";
27
+ import { listArchives } from "./vault.js";
28
+ const segmentsDir = (dataDir) => join(dataDir, "index", "segments");
29
+ const manifestFilePath = (dataDir) => join(dataDir, "index", "segments", "manifest.json");
30
+ const coveragePath = (dataDir) => join(dataDir, "index", "coverage.json");
31
+ /** Atomic publication that never leaves this call's temp behind on error.
32
+ * Mirrors the private `writeIndexFile` in src/search.ts. */
33
+ async function writeStoreFile(target, contents) {
34
+ const temp = `${target}.${randomUUID()}.tmp`;
35
+ try {
36
+ await writeFile(temp, contents, { mode: 0o600 });
37
+ await rename(temp, target);
38
+ }
39
+ finally {
40
+ await rm(temp, { force: true }).catch(() => undefined);
41
+ }
42
+ }
43
+ function emptyBatchIndex(builtAt) {
44
+ return { version: 2, extractionPolicy: INDEX_EXTRACTION_POLICY, builtAt, archives: {}, tokens: {}, meta: {} };
45
+ }
46
+ /** Sorted "a", then "t", then "m" rows — the region order every segment
47
+ * writer requires. Declared synchronous (not `async`, despite the design
48
+ * doc's signature): an `async function` may only return a `Promise`, and
49
+ * every caller here — `writeSegment`'s `rows` parameter — accepts a plain
50
+ * `Iterable<SegmentRow>` directly, so there is nothing for a Promise to buy. */
51
+ export function segmentRowsFromIndex(index) {
52
+ function* rows() {
53
+ for (const id of Object.keys(index.archives).sort()) {
54
+ const line = index.archives[id];
55
+ yield ["a", id, ...line];
56
+ }
57
+ for (const token of Object.keys(index.tokens).sort()) {
58
+ yield ["t", token, index.tokens[token]];
59
+ }
60
+ const meta = index.meta ?? {};
61
+ for (const id of Object.keys(meta).sort()) {
62
+ yield ["m", id, meta[id]];
63
+ }
64
+ }
65
+ return rows();
66
+ }
67
+ const newestBuiltAt = (segments) => {
68
+ let newest = "";
69
+ for (const segment of segments)
70
+ if (segment.builtAt > newest)
71
+ newest = segment.builtAt;
72
+ return newest || new Date().toISOString();
73
+ };
74
+ /**
75
+ * Segments mode is a one-way door for a vault: once `sealkeep index migrate`
76
+ * has run, it must never silently fall back to the legacy whole-blob build
77
+ * (and its whole-index-in-memory cost) just because `dropContentIndex`
78
+ * happened to empty the manifest. So this is a FILE-existence check, not a
79
+ * segment-count one — `dropContentIndex` deliberately leaves behind an empty
80
+ * manifest (see `resetSegments`) rather than removing it, precisely so this
81
+ * keeps answering true.
82
+ */
83
+ export async function segmentsEnabled(dataDir) {
84
+ return stat(manifestFilePath(dataDir)).then(() => true, () => false);
85
+ }
86
+ /** The archive ids segments cover on this machine: the union of every
87
+ * segment's `archiveIds` minus tombstones — exactly what `coverage.json`
88
+ * holds, computed straight from the plaintext manifest. No segment body is
89
+ * opened and no phrase is needed. */
90
+ export async function manifestCoverage(dataDir) {
91
+ const manifest = await readManifest(dataDir);
92
+ const tombstones = new Set(manifest.tombstones);
93
+ const indexed = new Set();
94
+ for (const segment of manifest.segments) {
95
+ for (const id of segment.archiveIds ?? [])
96
+ if (!tombstones.has(id))
97
+ indexed.add(id);
98
+ }
99
+ return indexed;
100
+ }
101
+ /** Archives another machine sealed whose rows a segment here still carries — searchable, never this machine's to prune. */
102
+ export async function manifestRemoteCoverage(dataDir) {
103
+ const manifest = await readManifest(dataDir);
104
+ const tombstones = new Set(manifest.tombstones);
105
+ const remote = new Set();
106
+ for (const segment of manifest.segments) {
107
+ for (const id of segment.remoteArchiveIds ?? [])
108
+ if (!tombstones.has(id))
109
+ remote.add(id);
110
+ }
111
+ return remote;
112
+ }
113
+ /**
114
+ * `coverage.json` exactly as `saveLocal` writes it in search.ts: the unsealed
115
+ * sidecar `index status`, doctor, and the daemon read to answer "what is
116
+ * searchable" without the recovery phrase. Always re-derived from the
117
+ * manifest, so it can never drift from what the segments actually cover.
118
+ */
119
+ export async function writeCoverageFromManifest(dataDir) {
120
+ // Everything searchable here, as the blob path's saveLocal writes it: this
121
+ // machine's own seals and the rows it carries for another machine's. What
122
+ // this machine may prune is answered by the manifest (manifestCoverage),
123
+ // never by this file.
124
+ const indexed = new Set([...(await manifestCoverage(dataDir)), ...(await manifestRemoteCoverage(dataDir))]);
125
+ await mkdir(join(dataDir, "index"), { recursive: true, mode: 0o700 });
126
+ await writeStoreFile(coveragePath(dataDir), JSON.stringify({
127
+ extractionPolicy: INDEX_EXTRACTION_POLICY,
128
+ builtAt: new Date().toISOString(),
129
+ indexed: [...indexed].sort(),
130
+ }, null, 2) + "\n");
131
+ }
132
+ /**
133
+ * Empties a vault's segment set back to nothing while keeping it IN segments
134
+ * mode (the manifest file itself survives, freshly empty) — the counterpart
135
+ * `dropContentIndex` needs so a drop-then-rebuild never regresses to the
136
+ * legacy blob, and the extraction-policy bump needs so a stale segment set
137
+ * can be discarded wholesale rather than patched in place (segments are
138
+ * immutable; there is no way to re-extract one that already exists).
139
+ */
140
+ export async function resetSegments(dataDir) {
141
+ await mkdir(segmentsDir(dataDir), { recursive: true, mode: 0o700 });
142
+ return updateManifest(dataDir, async (current) => {
143
+ const onDisk = await listSksegIds(dataDir);
144
+ const ids = new Set([...current.segments.map((segment) => segment.id), ...onDisk]);
145
+ for (const id of ids)
146
+ await removeSegment(dataDir, id);
147
+ return { version: 1, generation: "", segments: [], tombstones: [] };
148
+ });
149
+ }
150
+ /** ids on disk under `index/segments/` with a `.skseg` body, regardless of
151
+ * whether the manifest still lists them. */
152
+ async function listSksegIds(dataDir) {
153
+ let entries;
154
+ try {
155
+ entries = await readdir(segmentsDir(dataDir));
156
+ }
157
+ catch {
158
+ return [];
159
+ }
160
+ const ids = [];
161
+ for (const name of entries) {
162
+ if (!name.endsWith(".skseg"))
163
+ continue;
164
+ const id = name.slice(0, -".skseg".length);
165
+ if (/^[0-9a-f-]{36}$/.test(id))
166
+ ids.push(id);
167
+ }
168
+ return ids;
169
+ }
170
+ async function removeOrphanSegments(dataDir) {
171
+ const orphans = await orphanSegmentIds(dataDir, listSksegIds);
172
+ for (const id of orphans)
173
+ await removeSegment(dataDir, id);
174
+ }
175
+ /**
176
+ * `planCompaction` over the current manifest; when it has work, stream the
177
+ * merge and replace the merged segments with the single result, then bring
178
+ * `coverage.json` and any stray `.skseg` files left by an interrupted prior
179
+ * attempt back in line with the manifest.
180
+ */
181
+ export async function maybeCompact(dataDir, phrase, options = {}) {
182
+ const manifest = await readManifest(dataDir);
183
+ // Seal time (appendArchiveSegment) asks for smallOnly: the "too many
184
+ // segments" rule can pull a hundreds-of-MB segment into a merge, which
185
+ // belongs to the background build lane, not a foreground seal. Disabling
186
+ // it here means only the cheap small-segment rule (< 8 MiB, >= 4 of them)
187
+ // can fire from that path.
188
+ const plan = planCompaction(manifest, options.smallOnly ? { maxSegments: Number.MAX_SAFE_INTEGER } : undefined);
189
+ if (plan.length === 0)
190
+ return null;
191
+ const planned = new Set(plan);
192
+ const tombstones = new Set(manifest.tombstones);
193
+ const mergedArchiveIds = new Set();
194
+ for (const segment of manifest.segments) {
195
+ if (!planned.has(segment.id))
196
+ continue;
197
+ for (const id of segment.archiveIds ?? [])
198
+ if (!tombstones.has(id))
199
+ mergedArchiveIds.add(id);
200
+ }
201
+ const archiveIds = [...mergedArchiveIds].sort();
202
+ const write = async (writeDataDir, writePhrase, input, writeOptions) => {
203
+ const entry = await writeSegment(writeDataDir, writePhrase, input, writeOptions);
204
+ return { ...entry, origin: "local", archiveIds };
205
+ };
206
+ // Two writers (a seal and the catch-up) can plan the same small segments;
207
+ // whichever merges second finds them gone and refuses — a benign race, not
208
+ // a failed seal. The next plan sees the merged set.
209
+ let entry;
210
+ try {
211
+ entry = await compactSegments(dataDir, phrase, plan, { open: openSegment, write, remove: removeSegment }, options);
212
+ }
213
+ catch (error) {
214
+ if (error?.code === "invalid_argument")
215
+ return null;
216
+ throw error;
217
+ }
218
+ await writeCoverageFromManifest(dataDir);
219
+ await removeOrphanSegments(dataDir);
220
+ return entry;
221
+ }
222
+ /** One archive, sealed at seal time: no whole-index rewrite, just a new
223
+ * one-archive segment. Also publishes it (step 2): a segment is exactly the
224
+ * unit a peer wants as soon as possible, not just at the next background
225
+ * build. A publish failure never fails the seal that produced the segment —
226
+ * it is already durable locally — so it is caught and named for the next
227
+ * successful publish (a build's, or this machine's next seal) to pick up. */
228
+ export async function appendArchiveSegment(dataDir, phrase, record, tokensByChunk, options = {}) {
229
+ const index = emptyBatchIndex(new Date().toISOString());
230
+ applyArchiveTokens(index, record, tokensByChunk);
231
+ const segmentId = randomUUID();
232
+ const builtAt = new Date().toISOString();
233
+ const entry = await writeSegment(dataDir, phrase, { segmentId, builtAt, rows: segmentRowsFromIndex(index) });
234
+ const withArchiveIds = { ...entry, origin: "local", archiveIds: [record.id] };
235
+ await appendSegment(dataDir, withArchiveIds);
236
+ await writeCoverageFromManifest(dataDir);
237
+ // Seal time only ever pays for the cheap small-segment compaction; the
238
+ // build lane pays for the full policy (see maybeCompact and
239
+ // buildContentIndex's segments branch).
240
+ await maybeCompact(dataDir, phrase, { smallOnly: true });
241
+ if (options.sync !== false) {
242
+ try {
243
+ const store = await remoteIndexStoreFor(dataDir, phrase, undefined, {
244
+ signal: options.signal,
245
+ remoteIdleMs: options.remoteIdleMs,
246
+ maxBytesPerSecond: options.maxBytesPerSecond,
247
+ });
248
+ if (store)
249
+ await publishSegments(dataDir, phrase, store, { signal: options.signal });
250
+ }
251
+ catch (error) {
252
+ if (options.signal?.aborted)
253
+ throw error;
254
+ const reason = error instanceof Error ? error.message.split("\n")[0] : String(error);
255
+ process.emitWarning(`segment publish deferred: ${reason}`, "SealkeepIndexDeferred");
256
+ }
257
+ }
258
+ return withArchiveIds;
259
+ }
260
+ /** The catch-up build's per-batch equivalent: one segment for however many
261
+ * archives a checkpoint's worth of indexing collected. */
262
+ export async function appendBatchSegment(dataDir, phrase, batch, options = {}) {
263
+ const archiveIds = Object.keys(batch.archives).sort();
264
+ if (archiveIds.length === 0)
265
+ return null;
266
+ const segmentId = randomUUID();
267
+ const builtAt = new Date().toISOString();
268
+ const entry = await writeSegment(dataDir, phrase, { segmentId, builtAt, rows: segmentRowsFromIndex(batch) }, options);
269
+ const withArchiveIds = { ...entry, origin: "local", archiveIds };
270
+ await appendSegment(dataDir, withArchiveIds);
271
+ await writeCoverageFromManifest(dataDir);
272
+ await maybeCompact(dataDir, phrase, options);
273
+ return withArchiveIds;
274
+ }
275
+ export async function retireArchiveIds(dataDir, archiveIds) {
276
+ await retireArchives(dataDir, archiveIds);
277
+ await writeCoverageFromManifest(dataDir);
278
+ }
279
+ /**
280
+ * Open every segment, answer `terms` from each concurrently, and merge into
281
+ * one `ContentIndex` a query-time caller reads exactly like the legacy blob:
282
+ * archives keep the richer (longer) row, meta merges field by field with the
283
+ * later (newest-last) segment winning except a real `summary` over scaffold
284
+ * and unioned `commits`/`files`, and token postings union, drop anything
285
+ * tombstoned, and rank chronologically before the usual per-token cap.
286
+ */
287
+ export async function lookupSegments(dataDir, phrase, terms, options = {}) {
288
+ const manifest = await readManifest(dataDir);
289
+ const settled = await Promise.allSettled(manifest.segments.map((segment) => openSegment(dataDir, phrase, segment.id)));
290
+ const readers = [];
291
+ for (const result of settled)
292
+ if (result.status === "fulfilled")
293
+ readers.push(result.value);
294
+ try {
295
+ const failed = settled.find((result) => result.status === "rejected");
296
+ if (failed)
297
+ throw failed.reason;
298
+ const lookups = await Promise.all(readers.map((reader) => reader.lookup(terms, { prefixKeep: options.prefixKeep, signal: options.signal })));
299
+ const tombstones = new Set(manifest.tombstones);
300
+ const merged = {
301
+ version: 2,
302
+ extractionPolicy: INDEX_EXTRACTION_POLICY,
303
+ builtAt: newestBuiltAt(manifest.segments),
304
+ archives: {},
305
+ tokens: {},
306
+ meta: {},
307
+ };
308
+ const tokenUnion = {};
309
+ for (const lookup of lookups) {
310
+ for (const [id, line] of Object.entries(lookup.archives)) {
311
+ if (tombstones.has(id))
312
+ continue;
313
+ const current = merged.archives[id];
314
+ if (!current || line.length > current.length)
315
+ merged.archives[id] = line;
316
+ }
317
+ for (const [id, metaObject] of Object.entries(lookup.meta)) {
318
+ if (tombstones.has(id))
319
+ continue;
320
+ const current = merged.meta[id];
321
+ merged.meta[id] = current ? mergeMetaFields(current, metaObject) : metaObject;
322
+ }
323
+ await options.onYield?.();
324
+ }
325
+ for (const lookup of lookups) {
326
+ for (const [token, postings] of Object.entries(lookup.tokens)) {
327
+ const bucket = (tokenUnion[token] ??= []);
328
+ const seen = new Set(bucket);
329
+ for (const posting of postings) {
330
+ const archiveId = posting.split("@")[0];
331
+ if (tombstones.has(archiveId) || seen.has(posting))
332
+ continue;
333
+ seen.add(posting);
334
+ bucket.push(posting);
335
+ }
336
+ }
337
+ }
338
+ for (const [token, postings] of Object.entries(tokenUnion)) {
339
+ if (postings.length === 0)
340
+ continue;
341
+ const ranked = chronologicalPostings(postings, merged.meta ?? {});
342
+ merged.tokens[token] = ranked.length > MAX_POSTINGS_PER_TOKEN ? ranked.slice(-MAX_POSTINGS_PER_TOKEN) : ranked;
343
+ }
344
+ return merged;
345
+ }
346
+ finally {
347
+ await Promise.all(readers.map((reader) => reader.close().catch(() => undefined)));
348
+ }
349
+ }
350
+ // A captured tool artifact, not a real session line; a real summary always
351
+ // beats it regardless of which segment it came from. Mirrors the same
352
+ // pattern in search.ts (SCAFFOLD_SUMMARY) and index-manifest.ts (JUNK_SUMMARY).
353
+ const JUNK_SUMMARY = /^\s*<(?:system-reminder|local-command-stdout|task-notification|command-name|command-message)/;
354
+ function isJunkSummary(value) {
355
+ return typeof value !== "string" || JUNK_SUMMARY.test(value);
356
+ }
357
+ function unionArrayField(existing, incoming) {
358
+ if (!Array.isArray(incoming))
359
+ return existing;
360
+ if (!Array.isArray(existing))
361
+ return incoming;
362
+ const seen = new Set(existing.map((item) => JSON.stringify(item)));
363
+ const merged = existing.slice();
364
+ for (const item of incoming) {
365
+ const key = JSON.stringify(item);
366
+ if (!seen.has(key)) {
367
+ seen.add(key);
368
+ merged.push(item);
369
+ }
370
+ }
371
+ return merged;
372
+ }
373
+ /** Later segment wins field by field, except `summary` (a real line beats
374
+ * captured scaffolding) and the unioned `commits`/`files` arrays. */
375
+ function mergeMetaFields(base, incoming) {
376
+ const merged = { ...base };
377
+ for (const [key, value] of Object.entries(incoming)) {
378
+ if (key === "summary") {
379
+ const existing = merged.summary;
380
+ merged.summary = existing !== undefined && !isJunkSummary(existing) && isJunkSummary(value) ? existing : value;
381
+ }
382
+ else if (key === "commits" || key === "files") {
383
+ merged[key] = unionArrayField(merged[key], value);
384
+ }
385
+ else {
386
+ merged[key] = value;
387
+ }
388
+ }
389
+ return merged;
390
+ }
391
+ /**
392
+ * The blob for cloud publication, produced from segments without ever
393
+ * holding more than the streaming merge needs: `mergeSegmentRows` folds every
394
+ * segment into archive/token/meta rows with tombstones already applied, and
395
+ * `persistContentIndex` seals and pushes exactly as the legacy build does.
396
+ * `maxTokens` is the background child's guard — a merge that would exceed it
397
+ * is abandoned (nothing written) rather than produced and then rejected.
398
+ *
399
+ * The blob-fallback publish path (step 2 addendum): a store with no generic
400
+ * keyed storage (managed today) cannot hold segments, so this is still how
401
+ * such a vault reaches the cloud at all. A successful push also records
402
+ * `published-blob.json`, so `publishPending` knows the manifest it merged is
403
+ * no longer waiting, and a peer without segments support (`pullSegments`'s
404
+ * own blob fallback) can tell its own publication apart from one worth
405
+ * converting.
406
+ */
407
+ export async function publishFromSegments(dataDir, phrase, options) {
408
+ const manifest = await readManifest(dataDir);
409
+ // A vault that has been dropped back to an empty (but still segments-mode)
410
+ // manifest has nothing to publish. persistContentIndex would happily seal
411
+ // and write an empty blob; that is not "nothing written".
412
+ if (manifest.segments.length === 0)
413
+ return false;
414
+ const tombstones = new Set(manifest.tombstones);
415
+ const readers = [];
416
+ try {
417
+ for (const segment of manifest.segments)
418
+ readers.push(await openSegment(dataDir, phrase, segment.id));
419
+ const index = {
420
+ version: 2,
421
+ extractionPolicy: INDEX_EXTRACTION_POLICY,
422
+ builtAt: newestBuiltAt(manifest.segments),
423
+ archives: {},
424
+ tokens: {},
425
+ meta: {},
426
+ };
427
+ let tokenRows = 0;
428
+ for await (const row of mergeSegmentRows(readers, tombstones, { signal: options.signal, onYield: options.onYield })) {
429
+ if (row[0] === "a") {
430
+ index.archives[row[1]] = row.slice(2);
431
+ }
432
+ else if (row[0] === "t") {
433
+ tokenRows += 1;
434
+ if (options.maxTokens !== undefined && tokenRows > options.maxTokens)
435
+ return false;
436
+ index.tokens[row[1]] = row[2];
437
+ }
438
+ else {
439
+ (index.meta ??= {})[row[1]] = row[2];
440
+ }
441
+ }
442
+ const pushed = await persistContentIndex(dataDir, phrase, index, options.store, { signal: options.signal, onYield: options.onYield });
443
+ if (pushed && options.store) {
444
+ // Not index.builtAt, and not the local content-index.json: sealing
445
+ // stamps its own envelope createdAt, and persistContentIndex's own BYO
446
+ // convergence retry can resave the LOCAL file with a later timestamp
447
+ // than the one it actually pushed (it reseals again after observing
448
+ // the remote). Read back what the store itself now reports, so this
449
+ // matches exactly what a peer's pullSegments blob fallback — or this
450
+ // vault's own next publish — will also see from store.meta().
451
+ const remoteEnvelope = await options.store.meta({ signal: options.signal }).catch(() => null);
452
+ const builtAt = remoteEnvelope?.manifest?.createdAt ?? index.builtAt;
453
+ await writePublishedBlobState(dataDir, { version: 1, generation: manifest.generation, builtAt });
454
+ }
455
+ return pushed;
456
+ }
457
+ finally {
458
+ await Promise.all(readers.map((reader) => reader.close().catch(() => undefined)));
459
+ }
460
+ }
461
+ /**
462
+ * One-time move of the existing whole-index blob into a single segment. The
463
+ * blob files are left exactly where they are — the cloud path still reads
464
+ * them until the next publish, and a vault with neither blob nor manifest is
465
+ * simply "build one" (unchanged). Accepts a raw or already-canonical phrase,
466
+ * like the other public entry points in search.ts (`buildContentIndex`,
467
+ * `loadContentIndex`): `sealkeep index migrate` hands it the CLI's raw
468
+ * `--recovery-phrase` value directly.
469
+ */
470
+ export async function migrateBlobToSegments(dataDir, rawPhrase) {
471
+ // Not `segmentsEnabled`: that is a file-existence check now (a vault stays
472
+ // in segments mode across a drop, even with an empty manifest). Migration
473
+ // itself must still be a no-op once there is real segment content, but is
474
+ // allowed — indeed necessary — to (re)run against an empty manifest, e.g.
475
+ // right after a drop.
476
+ if ((await readManifest(dataDir)).segments.length > 0)
477
+ return { migrated: false, archives: 0, tokens: 0 };
478
+ const phrase = canonicalPhrase(rawPhrase);
479
+ const index = normalizeContentIndex(await loadContentIndex(dataDir, phrase));
480
+ // The blob may carry rows another machine indexed (pulled from the account
481
+ // index). They stay searchable in the segment, but only this machine's own
482
+ // archives become coverage: the next build tombstones "covered but no
483
+ // longer here", and a teammate's session was never here to begin with.
484
+ const here = new Set((await listArchives(dataDir)).map((record) => record.id));
485
+ const allIds = Object.keys(index.archives).sort();
486
+ const archiveIds = allIds.filter((id) => here.has(id));
487
+ const remoteArchiveIds = allIds.filter((id) => !here.has(id));
488
+ const segmentId = randomUUID();
489
+ const builtAt = new Date().toISOString();
490
+ const entry = await writeSegment(dataDir, phrase, { segmentId, builtAt, rows: segmentRowsFromIndex(index) });
491
+ const withArchiveIds = { ...entry, origin: "local", archiveIds, ...(remoteArchiveIds.length ? { remoteArchiveIds } : {}) };
492
+ await appendSegment(dataDir, withArchiveIds);
493
+ await writeCoverageFromManifest(dataDir);
494
+ return { migrated: true, archives: allIds.length, tokens: Object.keys(index.tokens).length };
495
+ }
@@ -1,5 +1,7 @@
1
- import type { Envelope } from "../packages/vaultline-crypto/src/index.js";
1
+ import type { Envelope } from "../packages/sealkeep-crypto/src/index.js";
2
2
  import { type ChunkStoreClient } from "./chunk-store.js";
3
+ import type { StorageTargetConfig } from "./types.js";
4
+ import type { ManagedIndexCoverage, ManagedIndexHead, ManagedIndexReceipt, ManagedIndexV2PublishResult, ManagedIndexV2Snapshot, ManagedIndexV2Unsupported } from "./cloud.js";
3
5
  /**
4
6
  * The content index travels, sealed — Paul's question answered structurally.
5
7
  *
@@ -10,9 +12,9 @@ import { type ChunkStoreClient } from "./chunk-store.js";
10
12
  * whole history to rebuild its own. So the index now syncs like everything
11
13
  * else does: as ciphertext. Two small objects in the storage —
12
14
  *
13
- * …/index.vlmeta the index's ENVELOPE (tiny; carries builtAt, so
15
+ * …/index.skmeta the index's ENVELOPE (tiny; carries builtAt, so
14
16
  * "is theirs newer than mine" costs one small GET)
15
- * …/index.vlindex the sealed index itself
17
+ * …/index.skindex the sealed index itself
16
18
  *
17
19
  * — and the recovery phrase, which every machine of the vault holds, opens
18
20
  * them. The provider learns that an index exists and when it changed;
@@ -23,29 +25,103 @@ import { type ChunkStoreClient } from "./chunk-store.js";
23
25
  * hash it). Pro (managed): the control plane vends signed PUT/GET for a
24
26
  * per-account index slot — same signer, known lengths — via cloud.ts.
25
27
  */
28
+ export type RemoteIndexIoOptions = {
29
+ signal?: AbortSignal;
30
+ maxBytesPerSecond?: number;
31
+ onProgress?: (bytes: number) => void;
32
+ /** Optional caller memory ceiling for the combined immutable metadata and ciphertext read. */
33
+ maxBytes?: number;
34
+ };
35
+ /**
36
+ * The account's per-machine segment objects (step 2): a generic keyed
37
+ * put/get/head/delete over the same bucket the blob uses, addressed by a key
38
+ * RELATIVE to this machine's or a peer's `segments/` folder — the caller
39
+ * (`src/index-publish.ts`) never sees the prefix/hashing that turns it into
40
+ * the real remote key, exactly as `meta`/`pull`/`push` hide it for the blob.
41
+ */
42
+ export type RemoteIndexSegmentStore = {
43
+ put(key: string, body: Buffer | string, options?: RemoteIndexIoOptions): Promise<void>;
44
+ /** null when the object does not exist. */
45
+ get(key: string, options?: RemoteIndexIoOptions): Promise<Buffer | null>;
46
+ head(key: string, options?: RemoteIndexIoOptions): Promise<boolean>;
47
+ del(key: string, options?: RemoteIndexIoOptions): Promise<void>;
48
+ };
26
49
  export type RemoteIndexStore = {
27
50
  /** The remote envelope, or null when no index was ever pushed. */
28
- meta(): Promise<Envelope | null>;
29
- pull(): Promise<Buffer>;
30
- push(envelope: Envelope, ciphertext: Buffer): Promise<void>;
51
+ meta(options?: RemoteIndexIoOptions): Promise<Envelope | null>;
52
+ pull(options?: RemoteIndexIoOptions): Promise<Buffer>;
53
+ push(envelope: Envelope, ciphertext: Buffer, options?: RemoteIndexIoOptions): Promise<void>;
54
+ /** Large production snapshots use the committed local ciphertext directly. */
55
+ pushFile?(envelope: Envelope, ciphertextPath: string, options?: RemoteIndexIoOptions): Promise<void>;
56
+ /**
57
+ * Managed storage's immutable generation/CAS lane. Its absence means the
58
+ * provider has no protocol that can prove a cross-machine publication. An
59
+ * explicit `unsupported` result means the managed plane is too old; callers
60
+ * must keep source sessions rather than falling back to the legacy slot.
61
+ */
62
+ cas?: RemoteIndexCas;
63
+ /**
64
+ * Present only for a provider whose client exposes generic keyed objects —
65
+ * an own bucket. Managed and team stores vend a single fixed index slot,
66
+ * not a general key space, so they omit this; a caller with no `segments`
67
+ * skips publish/pull rather than guessing at a protocol that is not there.
68
+ */
69
+ segments?: RemoteIndexSegmentStore;
70
+ };
71
+ export type RemoteIndexCasPublishOptions = RemoteIndexIoOptions & {
72
+ /** Zero means there is no committed head. */
73
+ expectedRevision: number;
74
+ /** Canonical archiveRef -> entryDigest map for these exact sealed bytes. */
75
+ coverageJson: ManagedIndexCoverage;
76
+ /** Required for team stores and ignored/omitted for account stores. */
77
+ expectedMembershipVersion?: number;
31
78
  };
79
+ export type RemoteIndexCasHeadResult = ManagedIndexV2Unsupported | {
80
+ status: "ok";
81
+ head: ManagedIndexHead | null;
82
+ };
83
+ export type RemoteIndexCasSnapshotResult = ManagedIndexV2Unsupported | {
84
+ status: "ok";
85
+ snapshot: ManagedIndexV2Snapshot | null;
86
+ };
87
+ export type RemoteIndexCas = {
88
+ head(options?: RemoteIndexIoOptions): Promise<RemoteIndexCasHeadResult>;
89
+ pullSnapshot(options?: RemoteIndexIoOptions): Promise<RemoteIndexCasSnapshotResult>;
90
+ publish(envelope: Envelope, ciphertext: Buffer, options: RemoteIndexCasPublishOptions): Promise<ManagedIndexV2PublishResult>;
91
+ publishFile?(envelope: Envelope, ciphertextPath: string, options: RemoteIndexCasPublishOptions): Promise<ManagedIndexV2PublishResult>;
92
+ };
93
+ export type { ManagedIndexCoverage, ManagedIndexHead, ManagedIndexReceipt };
94
+ export declare const DEFAULT_INDEX_REMOTE_IDLE_MS: number;
32
95
  /** The store for an own-bucket vault, over the provider's plain object verbs. */
33
- export declare function byoIndexStore(dataDir: string, phrase: string, injected?: ChunkStoreClient, targetOverride?: {
34
- provider: "s3" | "r2" | "b2" | "gcs" | "gdrive" | "vaultline";
35
- bucket: string;
36
- prefix: string;
37
- region?: string;
38
- }): Promise<RemoteIndexStore>;
96
+ export declare function byoIndexStore(dataDir: string, phrase: string, injected?: ChunkStoreClient, targetOverride?: StorageTargetConfig, transfer?: Pick<RemoteIndexIoOptions, "maxBytesPerSecond" | "onProgress">): Promise<RemoteIndexStore>;
39
97
  /** The store for a managed vault, through the control plane's signed index slot. */
40
- export declare function managedIndexStore(dataDir: string): Promise<RemoteIndexStore>;
98
+ export declare function managedIndexStore(dataDir: string, options?: {
99
+ signal?: AbortSignal;
100
+ renewCloudToken?: boolean;
101
+ maxBytesPerSecond?: number;
102
+ onProgress?: (bytes: number) => void;
103
+ }): Promise<RemoteIndexStore>;
104
+ /** Managed team-space index slot. Membership authorization is enforced by the plane. */
105
+ export declare function managedTeamIndexStore(dataDir: string, spaceKey: string, transfer?: Pick<RemoteIndexIoOptions, "maxBytesPerSecond" | "onProgress">): Promise<RemoteIndexStore>;
41
106
  /** Picks the right store for this vault, or null when there is nowhere to sync. */
42
- export declare function remoteIndexStoreFor(dataDir: string, phrase: string, injected?: ChunkStoreClient): Promise<RemoteIndexStore | null>;
107
+ export declare function remoteIndexStoreFor(dataDir: string, phrase: string, injected?: ChunkStoreClient, options?: {
108
+ signal?: AbortSignal;
109
+ renewCloudToken?: boolean;
110
+ remoteIdleMs?: number;
111
+ maxBytesPerSecond?: number;
112
+ onProgress?: (bytes: number) => void;
113
+ }): Promise<RemoteIndexStore | null>;
43
114
  /**
44
115
  * Pulls the remote index when it is newer than what this machine holds.
45
116
  * Freshness is the envelope's own createdAt — one tiny GET decides, and the
46
117
  * blob only moves when it would change something.
47
118
  */
48
- export declare function pullIndexIfNewer(dataDir: string, store: RemoteIndexStore, localBuiltAt: string | null, save: (envelope: Envelope, ciphertext: Buffer) => Promise<void>): Promise<{
119
+ export declare function pullIndexIfNewer(dataDir: string, store: RemoteIndexStore, localBuiltAt: string | null, save: (envelope: Envelope, ciphertext: Buffer) => Promise<void>, options?: {
120
+ signal?: AbortSignal;
121
+ idleMs?: number;
122
+ maxBytesPerSecond?: number;
123
+ onProgress?: (bytes: number) => void;
124
+ }): Promise<{
49
125
  pulled: boolean;
50
126
  remoteBuiltAt: string | null;
51
127
  }>;