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