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.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +252 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +332 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5940 -297
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/src/index-sync.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
64
|
-
await cloud.pushIndexObject(dataDir, "
|
|
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
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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,
|
|
293
|
+
return await byoIndexStore(dataDir, phrase, injected, byo, {
|
|
294
|
+
maxBytesPerSecond: options.maxBytesPerSecond,
|
|
295
|
+
onProgress: options.onProgress,
|
|
296
|
+
});
|
|
88
297
|
}
|
|
89
|
-
catch {
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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;
|