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,119 +1,343 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, stat } from "node:fs/promises";
2
2
  import { fail } from "./errors.js";
3
3
  import { recordAudit } from "./audit.js";
4
4
  import { readConfig } from "./vault.js";
5
5
  import { uploadClientFromStore } from "./providers/index.js";
6
6
  import { folderNamingKey, storageScopeOf } from "./chunk-store.js";
7
7
  import { createHmac } from "node:crypto";
8
- const INDEX_SEGMENT = "vaultline-index";
8
+ import { STORAGE_PREFIX_DEFAULT } from "./branding.js";
9
+ import { withProgressDeadline } from "./progress-deadline.js";
10
+ import { bufferChunks, collectChunks, fileChunks, LEGACY_BUFFER_LIMIT_BYTES } from "./byte-stream.js";
11
+ import { paceBackgroundByteStream } from "./background-bandwidth.js";
12
+ export const DEFAULT_INDEX_REMOTE_IDLE_MS = 2 * 60_000;
13
+ const INDEX_SEGMENT = "sealkeep-index";
9
14
  function indexKeys(prefix, naming, namingKey) {
10
15
  const clean = prefix.replace(/^\/+|\/+$/g, "");
11
16
  const segment = naming === "hashed" && namingKey
12
17
  ? createHmac("sha256", namingKey).update("segment:vaultline-index").digest("hex").slice(0, 16)
13
18
  : INDEX_SEGMENT;
14
- return { meta: `${clean}/${segment}/index.vlmeta`, blob: `${clean}/${segment}/index.vlindex` };
19
+ return { meta: `${clean}/${segment}/index.skmeta`, blob: `${clean}/${segment}/index.skindex` };
20
+ }
21
+ /**
22
+ * The folder the per-machine segment objects live under — a `segments/`
23
+ * sibling of the blob's own keys, in the exact same (hashed or readable)
24
+ * folder `indexKeys` derives, so the naming rule is defined in exactly one
25
+ * place. `src/index-publish.ts` joins a machine/segment-relative key onto it.
26
+ */
27
+ function segmentKeys(prefix, naming, namingKey) {
28
+ const { blob } = indexKeys(prefix, naming, namingKey);
29
+ return { base: `${blob.slice(0, blob.lastIndexOf("/"))}/segments` };
15
30
  }
16
31
  /** The store for an own-bucket vault, over the provider's plain object verbs. */
17
- export async function byoIndexStore(dataDir, phrase, injected, targetOverride) {
32
+ export async function byoIndexStore(dataDir, phrase, injected, targetOverride, transfer = {}) {
18
33
  const config = await readConfig(dataDir);
19
34
  const remoteStorage = targetOverride ?? config.remoteStorage ?? fail("storage_not_configured", "No remote target configured, so the index has nowhere to sync");
20
35
  if (remoteStorage.provider === "vaultline")
21
36
  fail("invalid_argument", "Managed vaults sync their index through the control plane, not the object store");
22
- const raw = injected ?? (await uploadClientFromStore(dataDir, config.vaultId, remoteStorage));
37
+ if (!remoteStorage.bucket)
38
+ fail("storage_not_configured", "The index target has no bucket configured");
39
+ const providerConfig = {
40
+ provider: remoteStorage.provider,
41
+ bucket: remoteStorage.bucket,
42
+ prefix: remoteStorage.prefix ?? STORAGE_PREFIX_DEFAULT,
43
+ ...(remoteStorage.region ? { region: remoteStorage.region } : {}),
44
+ };
45
+ let raw = injected;
46
+ if (!raw) {
47
+ const identity = targetOverride ?? { id: "primary", ...providerConfig };
48
+ const { credentialConfigIdForTarget, endpointOverridesForResolvedTarget } = await import("./storage-targets.js");
49
+ const credentialId = await credentialConfigIdForTarget(dataDir, identity);
50
+ if (!credentialId)
51
+ fail("signer_not_configured", `Storage target "${identity.id}" is not connected on this machine, so its index cannot sync.`);
52
+ raw = await uploadClientFromStore(dataDir, credentialId, providerConfig, await endpointOverridesForResolvedTarget(dataDir, identity));
53
+ }
23
54
  if (typeof raw.putObject !== "function" || typeof raw.getObject !== "function" || typeof raw.headObject !== "function") {
24
55
  fail("invalid_argument", `The ${remoteStorage.provider} client cannot sync the index`);
25
56
  }
26
57
  const naming = config.remoteNaming ?? "hashed";
27
- const keys = indexKeys(remoteStorage.prefix, naming, naming === "hashed" ? folderNamingKey(phrase, storageScopeOf(remoteStorage)) : null);
58
+ const namingKey = naming === "hashed" ? folderNamingKey(phrase, storageScopeOf(providerConfig)) : null;
59
+ const keys = indexKeys(providerConfig.prefix, naming, namingKey);
60
+ const segBase = segmentKeys(providerConfig.prefix, naming, namingKey).base;
61
+ const configuredRate = transfer.maxBytesPerSecond;
62
+ const configuredProgress = transfer.onProgress;
63
+ const get = async (key, options) => {
64
+ if (typeof raw.getObjectStream !== "function") {
65
+ const body = await raw.getObject(key, { signal: options.signal });
66
+ (options.onProgress ?? configuredProgress)?.(body.length);
67
+ return body;
68
+ }
69
+ const source = await raw.getObjectStream(key, { signal: options.signal });
70
+ const rate = options.maxBytesPerSecond ?? configuredRate;
71
+ return collectChunks(rate ? paceBackgroundByteStream(dataDir, source, rate, {
72
+ signal: options.signal,
73
+ onProgress: options.onProgress ?? configuredProgress,
74
+ }) : source, Number.MAX_SAFE_INTEGER);
75
+ };
76
+ const put = async (key, body, options) => {
77
+ const bytes = typeof body === "string" ? (await stat(body)).size : body.length;
78
+ const source = typeof body === "string" ? fileChunks(body, { signal: options.signal }) : bufferChunks(body);
79
+ if (typeof raw.uploadStream === "function") {
80
+ const rate = options.maxBytesPerSecond ?? configuredRate;
81
+ await raw.uploadStream(key, rate ? paceBackgroundByteStream(dataDir, source, rate, { signal: options.signal }) : source, { signal: options.signal, bytes });
82
+ return;
83
+ }
84
+ if (typeof body === "string" && bytes > LEGACY_BUFFER_LIMIT_BYTES) {
85
+ fail("provider_unsupported", `The ${remoteStorage.provider} client cannot stream this ${bytes}-byte index. Update the storage client before retrying.`);
86
+ }
87
+ const buffered = typeof body === "string" ? await readFile(body) : body;
88
+ await raw.putObject(key, buffered, { signal: options.signal });
89
+ };
90
+ const pushPair = async (envelope, ciphertext, options) => {
91
+ await put(keys.blob, ciphertext, options);
92
+ await put(keys.meta, Buffer.from(JSON.stringify(envelope), "utf8"), options);
93
+ };
28
94
  return {
29
- meta: async () => {
30
- const head = await raw.headObject(keys.meta);
95
+ meta: async (options = {}) => {
96
+ const head = await raw.headObject(keys.meta, { signal: options.signal });
31
97
  if (!head.exists)
32
98
  return null;
33
- return JSON.parse((await raw.getObject(keys.meta)).toString("utf8"));
99
+ return JSON.parse((await get(keys.meta, options)).toString("utf8"));
100
+ },
101
+ pull: (options = {}) => get(keys.blob, options),
102
+ push: (envelope, ciphertext, options = {}) => pushPair(envelope, ciphertext, options),
103
+ pushFile: (envelope, ciphertextPath, options = {}) => pushPair(envelope, ciphertextPath, options),
104
+ // Reuses the exact `put`/`get` closures above: same rate limiting, same
105
+ // uploadStream-when-available path for a segment's `.skseg` body, same
106
+ // legacy-buffer-size guard. Only the key namespace (relative to `segBase`
107
+ // instead of `keys.*`) differs from the blob's single slot.
108
+ segments: {
109
+ put: (key, body, options = {}) => put(`${segBase}/${key}`, body, options),
110
+ get: async (key, options = {}) => {
111
+ const fullKey = `${segBase}/${key}`;
112
+ const head = await raw.headObject(fullKey, { signal: options.signal });
113
+ if (!head.exists)
114
+ return null;
115
+ return get(fullKey, options);
116
+ },
117
+ head: async (key, options = {}) => (await raw.headObject(`${segBase}/${key}`, { signal: options.signal })).exists,
118
+ del: async (key) => { await raw.deleteObject(`${segBase}/${key}`); },
34
119
  },
35
- pull: () => raw.getObject(keys.blob),
36
- push: async (envelope, ciphertext) => {
37
- // Blob first, envelope second: a reader that sees the new envelope can
38
- // always fetch the blob it describes; the reverse order has a window
39
- // where the meta promises an index the bucket does not hold yet.
40
- await raw.putObject(keys.blob, ciphertext);
41
- await raw.putObject(keys.meta, Buffer.from(JSON.stringify(envelope), "utf8"));
42
- }
43
120
  };
44
121
  }
45
122
  /** The store for a managed vault, through the control plane's signed index slot. */
46
- export async function managedIndexStore(dataDir) {
123
+ export async function managedIndexStore(dataDir, options = {}) {
124
+ const configuredRate = options.maxBytesPerSecond;
47
125
  const cloud = await import("./cloud.js");
48
126
  if (typeof cloud.pullIndexObject !== "function" || typeof cloud.pushIndexObject !== "function") {
49
127
  fail("invalid_argument", "This build's control plane client cannot sync the index yet");
50
128
  }
51
129
  return {
52
- meta: async () => {
53
- const body = await cloud.pullIndexObject(dataDir, "meta");
130
+ meta: async (readOptions = {}) => {
131
+ const body = await cloud.pullIndexObject(dataDir, "meta", undefined, {
132
+ signal: readOptions.signal ?? options.signal,
133
+ renewToken: options.renewCloudToken !== false,
134
+ maxBytesPerSecond: readOptions.maxBytesPerSecond ?? configuredRate,
135
+ onProgress: readOptions.onProgress ?? options.onProgress,
136
+ });
54
137
  return body ? JSON.parse(body.toString("utf8")) : null;
55
138
  },
56
- pull: async () => {
57
- const body = await cloud.pullIndexObject(dataDir, "blob");
139
+ pull: async (readOptions = {}) => {
140
+ const body = await cloud.pullIndexObject(dataDir, "blob", undefined, {
141
+ signal: readOptions.signal ?? options.signal,
142
+ renewToken: options.renewCloudToken !== false,
143
+ maxBytesPerSecond: readOptions.maxBytesPerSecond ?? configuredRate,
144
+ onProgress: readOptions.onProgress ?? options.onProgress,
145
+ });
58
146
  if (!body)
59
147
  fail("archive_not_found", "No index has been pushed to managed storage yet");
60
148
  return body;
61
149
  },
62
- push: async (envelope, ciphertext) => {
63
- await cloud.pushIndexObject(dataDir, "blob", ciphertext);
64
- await cloud.pushIndexObject(dataDir, "meta", Buffer.from(JSON.stringify(envelope), "utf8"));
65
- }
150
+ push: async (envelope, ciphertext, writeOptions = {}) => {
151
+ const io = { signal: writeOptions.signal ?? options.signal, maxBytesPerSecond: writeOptions.maxBytesPerSecond ?? configuredRate };
152
+ await cloud.pushIndexObject(dataDir, "blob", ciphertext, undefined, io);
153
+ await cloud.pushIndexObject(dataDir, "meta", Buffer.from(JSON.stringify(envelope), "utf8"), undefined, io);
154
+ },
155
+ pushFile: async (envelope, ciphertextPath, writeOptions = {}) => {
156
+ if (typeof cloud.pushIndexFile !== "function")
157
+ fail("provider_unsupported", "This build cannot stream a large managed search index");
158
+ const io = { signal: writeOptions.signal ?? options.signal, maxBytesPerSecond: writeOptions.maxBytesPerSecond ?? configuredRate };
159
+ await cloud.pushIndexFile(dataDir, "blob", ciphertextPath, undefined, io);
160
+ await cloud.pushIndexObject(dataDir, "meta", Buffer.from(JSON.stringify(envelope), "utf8"), undefined, io);
161
+ },
162
+ cas: {
163
+ head: (readOptions = {}) => cloud.headIndexGenerationV2(dataDir, undefined, {
164
+ signal: readOptions.signal ?? options.signal,
165
+ renewToken: options.renewCloudToken !== false,
166
+ }),
167
+ pullSnapshot: (readOptions = {}) => cloud.pullIndexGenerationV2(dataDir, undefined, {
168
+ signal: readOptions.signal ?? options.signal,
169
+ renewToken: options.renewCloudToken !== false,
170
+ maxBytesPerSecond: readOptions.maxBytesPerSecond ?? configuredRate,
171
+ onProgress: readOptions.onProgress ?? options.onProgress,
172
+ maxBytes: readOptions.maxBytes,
173
+ }),
174
+ publish: (envelope, ciphertext, writeOptions) => cloud.publishIndexGenerationV2(dataDir, {
175
+ envelope,
176
+ ciphertext,
177
+ expectedRevision: writeOptions.expectedRevision,
178
+ coverageJson: writeOptions.coverageJson,
179
+ }, undefined, {
180
+ signal: writeOptions.signal ?? options.signal,
181
+ maxBytesPerSecond: writeOptions.maxBytesPerSecond ?? configuredRate,
182
+ onProgress: writeOptions.onProgress ?? options.onProgress,
183
+ }),
184
+ publishFile: (envelope, ciphertextPath, writeOptions) => cloud.publishIndexGenerationV2(dataDir, {
185
+ envelope,
186
+ ciphertext: { path: ciphertextPath },
187
+ expectedRevision: writeOptions.expectedRevision,
188
+ coverageJson: writeOptions.coverageJson,
189
+ }, undefined, {
190
+ signal: writeOptions.signal ?? options.signal,
191
+ maxBytesPerSecond: writeOptions.maxBytesPerSecond ?? configuredRate,
192
+ onProgress: writeOptions.onProgress ?? options.onProgress,
193
+ }),
194
+ },
195
+ };
196
+ }
197
+ /** Managed team-space index slot. Membership authorization is enforced by the plane. */
198
+ export async function managedTeamIndexStore(dataDir, spaceKey, transfer = {}) {
199
+ const cloud = await import("./cloud.js");
200
+ const configuredRate = transfer.maxBytesPerSecond;
201
+ return {
202
+ meta: async (options = {}) => {
203
+ const body = await cloud.pullTeamIndexObject(dataDir, spaceKey, "meta", undefined, {
204
+ ...options,
205
+ maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate,
206
+ onProgress: options.onProgress ?? transfer.onProgress,
207
+ });
208
+ return body ? JSON.parse(body.toString("utf8")) : null;
209
+ },
210
+ pull: async (options = {}) => {
211
+ const body = await cloud.pullTeamIndexObject(dataDir, spaceKey, "blob", undefined, {
212
+ ...options,
213
+ maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate,
214
+ onProgress: options.onProgress ?? transfer.onProgress,
215
+ });
216
+ if (!body)
217
+ fail("archive_not_found", "No team index has been pushed to managed storage yet");
218
+ return body;
219
+ },
220
+ push: async (envelope, ciphertext, options = {}) => {
221
+ const io = { ...options, maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate };
222
+ await cloud.pushTeamIndexObject(dataDir, spaceKey, "blob", ciphertext, undefined, io);
223
+ await cloud.pushTeamIndexObject(dataDir, spaceKey, "meta", Buffer.from(JSON.stringify(envelope), "utf8"), undefined, io);
224
+ },
225
+ cas: {
226
+ head: (options = {}) => cloud.headTeamIndexGenerationV2(dataDir, spaceKey, undefined, {
227
+ signal: options.signal,
228
+ }),
229
+ pullSnapshot: (options = {}) => cloud.pullTeamIndexGenerationV2(dataDir, spaceKey, undefined, {
230
+ signal: options.signal,
231
+ maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate,
232
+ onProgress: options.onProgress ?? transfer.onProgress,
233
+ maxBytes: options.maxBytes,
234
+ }),
235
+ publish: (envelope, ciphertext, options) => cloud.publishTeamIndexGenerationV2(dataDir, spaceKey, {
236
+ envelope,
237
+ ciphertext,
238
+ expectedRevision: options.expectedRevision,
239
+ coverageJson: options.coverageJson,
240
+ expectedMembershipVersion: options.expectedMembershipVersion
241
+ ?? fail("invalid_argument", "A team index publication requires a membership version"),
242
+ }, undefined, {
243
+ signal: options.signal,
244
+ maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate,
245
+ onProgress: options.onProgress ?? transfer.onProgress,
246
+ }),
247
+ publishFile: (envelope, ciphertextPath, options) => cloud.publishTeamIndexGenerationV2(dataDir, spaceKey, {
248
+ envelope,
249
+ ciphertext: { path: ciphertextPath },
250
+ expectedRevision: options.expectedRevision,
251
+ coverageJson: options.coverageJson,
252
+ expectedMembershipVersion: options.expectedMembershipVersion
253
+ ?? fail("invalid_argument", "A team index publication requires a membership version"),
254
+ }, undefined, {
255
+ signal: options.signal,
256
+ maxBytesPerSecond: options.maxBytesPerSecond ?? configuredRate,
257
+ onProgress: options.onProgress ?? transfer.onProgress,
258
+ }),
259
+ },
66
260
  };
67
261
  }
68
262
  /** Picks the right store for this vault, or null when there is nowhere to sync. */
69
- export async function remoteIndexStoreFor(dataDir, phrase, injected) {
263
+ export async function remoteIndexStoreFor(dataDir, phrase, injected, options = {}) {
70
264
  const config = await readConfig(dataDir);
71
- // Multi-target vaults keep ONE phonebook, on the most durable target:
72
- // managed when present, else the first bucket that can hold objects.
73
- if (config.storageTargets && config.storageTargets.length > 0) {
74
- const managed = config.storageTargets.find((target) => target.provider === "vaultline");
75
- if (managed) {
76
- try {
77
- return await managedIndexStore(dataDir);
78
- }
79
- catch {
80
- return null;
81
- }
265
+ const idleMs = options.remoteIdleMs ?? DEFAULT_INDEX_REMOTE_IDLE_MS;
266
+ // Keep ONE phonebook on the most durable account target: managed when
267
+ // present, otherwise the first connected object-store target. Resolve the
268
+ // cloud-backed list rather than looking only at config.json so a newly
269
+ // enrolled machine does not silently miss account routing metadata.
270
+ const { resolveTargets, targetConnectedOnThisMachine } = await import("./storage-targets.js");
271
+ const targets = await withProgressDeadline("Resolving search-index storage", idleMs, options.signal, (signal) => resolveTargets(dataDir, { ...options, signal }));
272
+ const managed = targets.find((target) => target.provider === "vaultline");
273
+ if (managed) {
274
+ try {
275
+ return await managedIndexStore(dataDir, options);
82
276
  }
83
- const byo = config.storageTargets.find((target) => target.provider !== "gdrive");
84
- if (!byo)
277
+ catch (error) {
278
+ if (options.signal?.aborted)
279
+ throw options.signal.reason ?? error;
85
280
  return null;
281
+ }
282
+ }
283
+ const byoTargets = targets
284
+ .filter((target) => target.provider !== "gdrive")
285
+ .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
286
+ for (const byo of byoTargets) {
287
+ if (!injected && !await targetConnectedOnThisMachine(dataDir, byo, {
288
+ renewCloudToken: options.renewCloudToken,
289
+ signal: options.signal,
290
+ }))
291
+ continue;
86
292
  try {
87
- return await byoIndexStore(dataDir, phrase, injected, { provider: byo.provider, bucket: byo.bucket ?? "", prefix: byo.prefix ?? "vaultline", ...(byo.region ? { region: byo.region } : {}) });
293
+ return await byoIndexStore(dataDir, phrase, injected, byo, {
294
+ maxBytesPerSecond: options.maxBytesPerSecond,
295
+ onProgress: options.onProgress,
296
+ });
88
297
  }
89
- catch {
90
- return null;
298
+ catch (error) {
299
+ if (options.signal?.aborted)
300
+ throw options.signal.reason ?? error; /* try the next connected bucket */
91
301
  }
92
302
  }
303
+ if (targets.length > 0)
304
+ return null;
93
305
  if (!config.remoteStorage) {
94
306
  // An enrolled managed vault has a cloud token and no remoteStorage entry —
95
307
  // same rule as storedCopiesMode. Without the token there is nowhere to sync.
96
308
  try {
97
309
  const { cloudToken } = await import("./cloud.js");
98
- await cloudToken(dataDir);
99
- return await managedIndexStore(dataDir);
310
+ await withProgressDeadline("Checking the Sealkeep account for index storage", idleMs, options.signal, (signal) => cloudToken(dataDir, undefined, {
311
+ renew: options.renewCloudToken !== false,
312
+ signal,
313
+ }));
314
+ return await managedIndexStore(dataDir, options);
100
315
  }
101
- catch {
316
+ catch (error) {
317
+ if (options.signal?.aborted)
318
+ throw options.signal.reason ?? error;
102
319
  return null;
103
320
  }
104
321
  }
105
322
  if (config.remoteStorage.provider === "vaultline") {
106
323
  try {
107
- return await managedIndexStore(dataDir);
324
+ return await managedIndexStore(dataDir, options);
108
325
  }
109
- catch {
326
+ catch (error) {
327
+ if (options.signal?.aborted)
328
+ throw options.signal.reason ?? error;
110
329
  return null;
111
330
  }
112
331
  }
113
332
  try {
114
- return await byoIndexStore(dataDir, phrase, injected);
333
+ return await byoIndexStore(dataDir, phrase, injected, undefined, {
334
+ maxBytesPerSecond: options.maxBytesPerSecond,
335
+ onProgress: options.onProgress,
336
+ });
115
337
  }
116
- catch {
338
+ catch (error) {
339
+ if (options.signal?.aborted)
340
+ throw options.signal.reason ?? error;
117
341
  return null;
118
342
  }
119
343
  }
@@ -122,15 +346,24 @@ export async function remoteIndexStoreFor(dataDir, phrase, injected) {
122
346
  * Freshness is the envelope's own createdAt — one tiny GET decides, and the
123
347
  * blob only moves when it would change something.
124
348
  */
125
- export async function pullIndexIfNewer(dataDir, store, localBuiltAt, save) {
126
- const envelope = await store.meta();
349
+ export async function pullIndexIfNewer(dataDir, store, localBuiltAt, save, options = {}) {
350
+ const idleMs = options.idleMs ?? DEFAULT_INDEX_REMOTE_IDLE_MS;
351
+ const envelope = await withProgressDeadline("Reading remote index metadata", idleMs, options.signal, (signal) => store.meta({
352
+ signal,
353
+ maxBytesPerSecond: options.maxBytesPerSecond,
354
+ onProgress: options.onProgress,
355
+ }));
127
356
  if (!envelope)
128
357
  return { pulled: false, remoteBuiltAt: null };
129
358
  const remoteBuiltAt = envelope.manifest?.createdAt ?? null;
130
359
  if (!remoteBuiltAt || (localBuiltAt && Date.parse(remoteBuiltAt) <= Date.parse(localBuiltAt))) {
131
360
  return { pulled: false, remoteBuiltAt };
132
361
  }
133
- const ciphertext = await store.pull();
362
+ const ciphertext = await withProgressDeadline("Pulling the remote search index", idleMs, options.signal, (signal) => store.pull({
363
+ signal,
364
+ maxBytesPerSecond: options.maxBytesPerSecond,
365
+ onProgress: options.onProgress,
366
+ }));
134
367
  await save(envelope, ciphertext);
135
368
  await recordAudit(dataDir, "archive.restore", "allowed", { indexPulled: true, remoteBuiltAt }).catch(() => undefined);
136
369
  return { pulled: true, remoteBuiltAt };
@@ -0,0 +1,30 @@
1
+ import { readIndexPublicationState } from "./index-publication-state.js";
2
+ import { type RemoteIndexStore } from "./index-sync.js";
3
+ import { publishLocalContentIndexCheckpointToManagedCas, type PublishLocalContentIndexCheckpointOptions, type PublishedLocalContentIndexCheckpoint } from "./search.js";
4
+ export declare const legacyIndexPublicationMarkerPath: (dataDir: string) => string;
5
+ export type LegacyIndexPublicationOutcome = {
6
+ status: "not-needed";
7
+ } | {
8
+ status: "published";
9
+ publication: PublishedLocalContentIndexCheckpoint;
10
+ };
11
+ type UpgradePublicationDependencies = {
12
+ readPublicationState: typeof readIndexPublicationState;
13
+ resolveInitialStore(dataDir: string, phrase: string, options: PublishLocalContentIndexCheckpointOptions): Promise<RemoteIndexStore | null>;
14
+ resolveRetryStore(dataDir: string, options: PublishLocalContentIndexCheckpointOptions): Promise<RemoteIndexStore>;
15
+ publish: typeof publishLocalContentIndexCheckpointToManagedCas;
16
+ };
17
+ /**
18
+ * One-time upgrade bridge for a complete local v3 checkpoint which predates
19
+ * immutable managed index receipts.
20
+ *
21
+ * It runs only in the daemon-owned index child. A first attempt starts only
22
+ * when the receipt ledger is absent and the selected account index store has
23
+ * managed CAS. Once started, its marker makes every later daemon attempt retry
24
+ * this exact fail-closed bridge. The bridge never overwrites a different
25
+ * remote head; that one case exits to the ordinary authenticated merge/CAS
26
+ * builder. The marker is otherwise removed only after exact managed read-back
27
+ * has installed durable per-archive receipts.
28
+ */
29
+ export declare function publishLegacyLocalIndexCheckpointIfNeeded(dataDir: string, phrase: string, options?: PublishLocalContentIndexCheckpointOptions, dependencies?: Partial<UpgradePublicationDependencies>): Promise<LegacyIndexPublicationOutcome>;
30
+ export {};
@@ -0,0 +1,179 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, mkdir, open, readFile, rm, stat, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fail, isSealkeepError } from "./errors.js";
5
+ import { readIndexPublicationState } from "./index-publication-state.js";
6
+ import { managedIndexStore, remoteIndexStoreFor } from "./index-sync.js";
7
+ import { publishLocalContentIndexCheckpointToManagedCas, } from "./search.js";
8
+ const MARKER_VERSION = 1;
9
+ export const legacyIndexPublicationMarkerPath = (dataDir) => join(dataDir, "index", "managed-checkpoint-upgrade.v1.json");
10
+ const defaultDependencies = {
11
+ readPublicationState: readIndexPublicationState,
12
+ resolveInitialStore: (dataDir, phrase, options) => remoteIndexStoreFor(dataDir, phrase, undefined, {
13
+ signal: options.signal,
14
+ remoteIdleMs: options.remoteIdleMs,
15
+ maxBytesPerSecond: options.maxBytesPerSecond,
16
+ onProgress: options.onProgress,
17
+ }),
18
+ resolveRetryStore: (dataDir, options) => managedIndexStore(dataDir, {
19
+ signal: options.signal,
20
+ maxBytesPerSecond: options.maxBytesPerSecond,
21
+ onProgress: options.onProgress,
22
+ }),
23
+ publish: publishLocalContentIndexCheckpointToManagedCas,
24
+ };
25
+ function errorCode(error) {
26
+ return typeof error === "object" && error !== null && "code" in error
27
+ ? String(error.code)
28
+ : undefined;
29
+ }
30
+ async function checkpointPairExists(dataDir) {
31
+ const [ciphertext, envelope] = await Promise.all([
32
+ stat(join(dataDir, "index", "content-index.skindex")).catch(() => null),
33
+ stat(join(dataDir, "index", "content-index.json")).catch(() => null),
34
+ ]);
35
+ return Boolean(ciphertext?.isFile() && envelope?.isFile());
36
+ }
37
+ async function readMarker(dataDir) {
38
+ let raw;
39
+ try {
40
+ raw = await readFile(legacyIndexPublicationMarkerPath(dataDir), "utf8");
41
+ }
42
+ catch (error) {
43
+ if (errorCode(error) === "ENOENT")
44
+ return false;
45
+ throw error;
46
+ }
47
+ try {
48
+ const marker = JSON.parse(raw);
49
+ if (marker.version !== MARKER_VERSION || Object.keys(marker).length !== 1)
50
+ throw new Error("invalid marker");
51
+ }
52
+ catch {
53
+ return fail("ciphertext_integrity_failed", "The managed search-index upgrade marker is damaged; source sessions remain protected until it is repaired.");
54
+ }
55
+ return true;
56
+ }
57
+ async function syncDirectory(path) {
58
+ let handle;
59
+ try {
60
+ handle = await open(path, "r");
61
+ await handle.sync();
62
+ }
63
+ catch (error) {
64
+ // Windows cannot open directories for fsync. The marker inode itself is
65
+ // still synced before publication, which is Node's strongest primitive
66
+ // there; POSIX must durably publish the directory entry as well.
67
+ if (process.platform !== "win32")
68
+ throw error;
69
+ }
70
+ finally {
71
+ await handle?.close().catch(() => undefined);
72
+ }
73
+ }
74
+ async function removeMarker(dataDir) {
75
+ try {
76
+ await unlink(legacyIndexPublicationMarkerPath(dataDir));
77
+ await syncDirectory(join(dataDir, "index"));
78
+ }
79
+ catch (error) {
80
+ if (errorCode(error) !== "ENOENT")
81
+ throw error;
82
+ }
83
+ }
84
+ /**
85
+ * Durably marks a legacy publication attempt before its first managed write.
86
+ *
87
+ * The marker survives an outage or service upgrade, so a failed bridge is
88
+ * retried as the same conservative operation rather than falling through to a
89
+ * memory-heavy convergence rebuild on the next process. A hard-link publish
90
+ * makes the small state atomic without replacing another live worker's
91
+ * marker.
92
+ */
93
+ async function markPending(dataDir) {
94
+ const directory = join(dataDir, "index");
95
+ const marker = legacyIndexPublicationMarkerPath(dataDir);
96
+ const temporary = join(directory, `.managed-checkpoint-upgrade.${randomUUID()}.tmp`);
97
+ await mkdir(directory, { recursive: true, mode: 0o700 });
98
+ let handle;
99
+ try {
100
+ handle = await open(temporary, "wx", 0o600);
101
+ await handle.writeFile(`${JSON.stringify({ version: MARKER_VERSION })}\n`, "utf8");
102
+ await handle.sync();
103
+ await handle.close();
104
+ handle = undefined;
105
+ try {
106
+ await link(temporary, marker);
107
+ }
108
+ catch (error) {
109
+ if (errorCode(error) !== "EEXIST")
110
+ throw error;
111
+ }
112
+ await syncDirectory(directory);
113
+ }
114
+ finally {
115
+ await handle?.close().catch(() => undefined);
116
+ await rm(temporary, { force: true }).catch(() => undefined);
117
+ }
118
+ }
119
+ /**
120
+ * One-time upgrade bridge for a complete local v3 checkpoint which predates
121
+ * immutable managed index receipts.
122
+ *
123
+ * It runs only in the daemon-owned index child. A first attempt starts only
124
+ * when the receipt ledger is absent and the selected account index store has
125
+ * managed CAS. Once started, its marker makes every later daemon attempt retry
126
+ * this exact fail-closed bridge. The bridge never overwrites a different
127
+ * remote head; that one case exits to the ordinary authenticated merge/CAS
128
+ * builder. The marker is otherwise removed only after exact managed read-back
129
+ * has installed durable per-archive receipts.
130
+ */
131
+ export async function publishLegacyLocalIndexCheckpointIfNeeded(dataDir, phrase, options = {}, dependencies = {}) {
132
+ const deps = { ...defaultDependencies, ...dependencies };
133
+ const pending = await readMarker(dataDir);
134
+ if (!await checkpointPairExists(dataDir)) {
135
+ if (pending)
136
+ await removeMarker(dataDir);
137
+ return { status: "not-needed" };
138
+ }
139
+ let store;
140
+ if (pending) {
141
+ // Once an attempt has begun, an offline control plane is a retryable
142
+ // publication failure. It must not silently become a full local rebuild.
143
+ store = await deps.resolveRetryStore(dataDir, options);
144
+ }
145
+ else {
146
+ const state = await deps.readPublicationState(dataDir);
147
+ if (state.status !== "missing")
148
+ return { status: "not-needed" };
149
+ store = await deps.resolveInitialStore(dataDir, phrase, options);
150
+ if (!store?.cas || typeof store.cas.publishFile !== "function")
151
+ return { status: "not-needed" };
152
+ await markPending(dataDir);
153
+ }
154
+ if (!store?.cas || typeof store.cas.publishFile !== "function") {
155
+ return fail("provider_unsupported", "The managed search-index publication lane is unavailable; source sessions remain protected and Sealkeep will retry.");
156
+ }
157
+ let publication;
158
+ try {
159
+ publication = await deps.publish(dataDir, phrase, { ...options, store });
160
+ }
161
+ catch (error) {
162
+ const normalBuildRequired = isSealkeepError(error) && ((error.code === "lease_expired" && error.details.reason === "index_head_different")
163
+ || (error.code === "provider_unsupported" && error.details.reason === "legacy_index_format"));
164
+ if (normalBuildRequired) {
165
+ // The bridge itself never replaces a different checkpoint. Hand this
166
+ // case back to the ordinary builder, which authenticates and merges the
167
+ // remote generation before publishing a new union with CAS. The same
168
+ // builder upgrades an authenticated pre-v3 monolith. Keeping the marker
169
+ // for either case would retry an immutable input forever.
170
+ await removeMarker(dataDir);
171
+ return { status: "not-needed" };
172
+ }
173
+ throw error;
174
+ }
175
+ // Receipt installation is complete when publish resolves. Removing the
176
+ // marker afterwards cannot make a failed/partial network write authoritative.
177
+ await removeMarker(dataDir);
178
+ return { status: "published", publication };
179
+ }
@@ -0,0 +1,32 @@
1
+ export type IntegrationManagerChildReceipt = {
2
+ kind: string;
3
+ executable: string;
4
+ args: string[];
5
+ };
6
+ /** Marker understood by the daemon bootstrap and the standalone manager. */
7
+ export declare const INTEGRATION_MANAGER_GENERATION_ENV = "SEALKEEP_SERVICE_MANAGER_GENERATION";
8
+ export declare const INTEGRATION_MANAGER_GENERATION = "darwin-app-policy-v1";
9
+ /**
10
+ * The durable service manager's exact child command.
11
+ *
12
+ * A launchd job registered by an older Sealkeep release can retain its cached
13
+ * ProcessType=Background policy even after the plist is corrected. Starting
14
+ * the daemon through taskpolicy's application profile escapes that inherited
15
+ * job policy before Node evaluates the CLI; every helper the daemon later
16
+ * starts inherits the corrected policy too. Other platforms keep their byte-
17
+ * for-byte command because their native service definitions reload normally.
18
+ */
19
+ export declare function integrationManagerChildInvocation(receipt: IntegrationManagerChildReceipt, target?: NodeJS.Platform): {
20
+ executable: string;
21
+ args: string[];
22
+ };
23
+ /**
24
+ * Source copied outside the npm package when the background service is
25
+ * installed. npm 7+ deliberately runs no uninstall lifecycle scripts, so a
26
+ * package cannot clean hooks or services from inside code npm just removed.
27
+ * This tiny disclosed supervisor survives long enough to notice that the
28
+ * package target stayed absent beyond an upgrade grace period, remove only the
29
+ * integration paths named in its receipt, write a tombstone, then remove
30
+ * itself. Vaults, encrypted archives, outboxes, and keychain items are kept.
31
+ */
32
+ export declare const INTEGRATION_MANAGER_SOURCE: string;