sealkeep 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +223 -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.js +4 -4
- 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 +316 -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 +5897 -290
- 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/recipients.d.ts +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,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One immutable sealed index segment: write it once, then open it and answer
|
|
3
|
+
* term lookups by reading only the blocks that hold the answer.
|
|
4
|
+
*
|
|
5
|
+
* The whole-index file this replaces made every seal rewrite the entire
|
|
6
|
+
* ciphertext and forced a lookup to decrypt the whole thing before a 64-row
|
|
7
|
+
* directory could narrow it down. A segment applies the same "seal in
|
|
8
|
+
* independent chunks, index the chunk boundaries" trick search.ts already
|
|
9
|
+
* uses for the legacy index, but at the grain of one write instead of the
|
|
10
|
+
* whole vault: nothing already sealed is ever read back, decrypted, or
|
|
11
|
+
* rewritten to add more. See docs/design/index-segments.md.
|
|
12
|
+
*
|
|
13
|
+
* File layout under `<dataDir>/index/segments/`:
|
|
14
|
+
* `<segmentId>.skseg` the sealed, chunk-compressed body (JSONL rows)
|
|
15
|
+
* `<segmentId>.skseg.json` that body's envelope, in the clear (it carries
|
|
16
|
+
* no plaintext — same shape as content-index.json)
|
|
17
|
+
* `<segmentId>.skdir` the sealed directory sidecar (byte offsets)
|
|
18
|
+
*
|
|
19
|
+
* Every file is published temp-then-rename, and a failed write removes
|
|
20
|
+
* everything this call produced — a half-written segment must never be
|
|
21
|
+
* mistaken for a real one.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
24
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { decryptArchive as openEnvelope, encryptArchive as sealEnvelope, sealArchiveToFile, } from "../packages/sealkeep-crypto/src/index.js";
|
|
27
|
+
import { chunkWindowForRange, openChunkWindow, supportsChunkAccess, unwrapWindowKey } from "../packages/sealkeep-crypto/src/chunk-access.js";
|
|
28
|
+
import { fail } from "./errors.js";
|
|
29
|
+
import { SEGMENT_DIRECTORY_STRIDE, } from "./index-segment-types.js";
|
|
30
|
+
const SEGMENT_ID_PATTERN = /^[0-9a-f-]{36}$/;
|
|
31
|
+
// Rows below this size ride the same coalesced write writeContentIndexCheckpoint
|
|
32
|
+
// uses; a handful of write() syscalls beats one per row on a 600-token segment.
|
|
33
|
+
const COALESCE_BYTES = 1024 * 1024;
|
|
34
|
+
// ~4 MiB raw per rows() window: bounded memory regardless of segment size.
|
|
35
|
+
const ROWS_WINDOW_BYTES = 4 * 1024 * 1024;
|
|
36
|
+
// 8 x stride rows bounds a wide prefix the same way MAX_RANGED_WINDOWS_PER_TERM
|
|
37
|
+
// bounds it for the legacy whole-index directory.
|
|
38
|
+
const MAX_LOOKUP_WINDOWS_PER_TERM = 8;
|
|
39
|
+
const segmentsDir = (dataDir) => join(dataDir, "index", "segments");
|
|
40
|
+
const openedSegments = new Map();
|
|
41
|
+
const OPENED_SEGMENTS_MAX = 64;
|
|
42
|
+
const phraseDigest = (phrase) => createHash("sha256").update(phrase, "utf8").digest("hex");
|
|
43
|
+
function rememberOpenedSegment(cacheKey, opened) {
|
|
44
|
+
if (openedSegments.size >= OPENED_SEGMENTS_MAX) {
|
|
45
|
+
const oldest = openedSegments.keys().next().value;
|
|
46
|
+
if (oldest !== undefined) {
|
|
47
|
+
openedSegments.get(oldest)?.archiveKey.fill(0);
|
|
48
|
+
openedSegments.delete(oldest);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
openedSegments.set(cacheKey, opened);
|
|
52
|
+
}
|
|
53
|
+
function assertSegmentId(segmentId) {
|
|
54
|
+
if (!SEGMENT_ID_PATTERN.test(segmentId))
|
|
55
|
+
fail("invalid_argument", "Segment id must be a UUID", { segmentId });
|
|
56
|
+
}
|
|
57
|
+
function isEnoent(error) {
|
|
58
|
+
return error?.code === "ENOENT";
|
|
59
|
+
}
|
|
60
|
+
const isArchiveRow = (row) => row[0] === "a";
|
|
61
|
+
const isTokenRow = (row) => row[0] === "t";
|
|
62
|
+
const isMetaRow = (row) => row[0] === "m";
|
|
63
|
+
/** The last directory entry whose token is at or before `term`, or -1. */
|
|
64
|
+
function directoryPosition(entries, term) {
|
|
65
|
+
let low = 0;
|
|
66
|
+
let high = entries.length - 1;
|
|
67
|
+
let found = -1;
|
|
68
|
+
while (low <= high) {
|
|
69
|
+
const mid = (low + high) >> 1;
|
|
70
|
+
if (entries[mid][0] <= term) {
|
|
71
|
+
found = mid;
|
|
72
|
+
low = mid + 1;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
high = mid - 1;
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
/** Normalizes the writer's row source: callers may hand over either kind of iterable. */
|
|
80
|
+
async function* asRowStream(rows) {
|
|
81
|
+
if (Symbol.asyncIterator in rows)
|
|
82
|
+
yield* rows;
|
|
83
|
+
else
|
|
84
|
+
yield* rows;
|
|
85
|
+
}
|
|
86
|
+
/** Atomic publication that never leaves this call's temp behind on error. Same
|
|
87
|
+
* pattern as the private writeIndexFile in search.ts, reimplemented here since
|
|
88
|
+
* that helper is not exported. */
|
|
89
|
+
async function writeSegmentFile(target, contents) {
|
|
90
|
+
const temp = `${target}.${randomUUID()}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
await writeFile(temp, contents, { mode: 0o600 });
|
|
93
|
+
await rename(temp, target);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function writeSealedDirectory(directoryPath, phrase, directory) {
|
|
100
|
+
const plaintext = Buffer.from(JSON.stringify(directory), "utf8");
|
|
101
|
+
let sealed;
|
|
102
|
+
try {
|
|
103
|
+
sealed = sealEnvelope(plaintext, {
|
|
104
|
+
recipients: [{ kind: "phrase", phrase }],
|
|
105
|
+
compression: "zstd",
|
|
106
|
+
adapter: { agent: "sealkeep-index-segment-directory", version: "1" },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
plaintext.fill(0);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await writeSegmentFile(directoryPath, `${JSON.stringify({ version: 1, envelope: sealed.envelope, ciphertext: sealed.ciphertext.toString("base64") })}\n`);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
sealed.ciphertext.fill(0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function segmentPaths(dataDir, segmentId) {
|
|
120
|
+
assertSegmentId(segmentId);
|
|
121
|
+
const dir = segmentsDir(dataDir);
|
|
122
|
+
return {
|
|
123
|
+
body: join(dir, `${segmentId}.skseg`),
|
|
124
|
+
envelope: join(dir, `${segmentId}.skseg.json`),
|
|
125
|
+
directory: join(dir, `${segmentId}.skdir`),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function removeSegment(dataDir, segmentId) {
|
|
129
|
+
const paths = await segmentPaths(dataDir, segmentId);
|
|
130
|
+
await Promise.all([
|
|
131
|
+
rm(paths.body, { force: true }),
|
|
132
|
+
rm(paths.envelope, { force: true }),
|
|
133
|
+
rm(paths.directory, { force: true }),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
const REGION_RANK = { a: 0, t: 1, m: 2 };
|
|
137
|
+
const REGION_NAME = { 0: "archives", 1: "tokens", 2: "meta" };
|
|
138
|
+
export const writeSegment = async (dataDir, phrase, input, options = {}) => {
|
|
139
|
+
const paths = await segmentPaths(dataDir, input.segmentId);
|
|
140
|
+
const dir = segmentsDir(dataDir);
|
|
141
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
142
|
+
const generation = randomUUID();
|
|
143
|
+
const plaintextTemp = join(dir, `.${input.segmentId}.${generation}.jsonl`);
|
|
144
|
+
const bodyTemp = `${paths.body}.${generation}.tmp`;
|
|
145
|
+
const state = {
|
|
146
|
+
offset: 0,
|
|
147
|
+
lastRank: -1,
|
|
148
|
+
lastKey: undefined,
|
|
149
|
+
regions: { archives: [0, 0], tokens: [0, 0], meta: [0, 0] },
|
|
150
|
+
entries: [],
|
|
151
|
+
archives: 0,
|
|
152
|
+
tokens: 0,
|
|
153
|
+
tokenRowIndex: 0,
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
const handle = await open(plaintextTemp, "wx", 0o600);
|
|
157
|
+
try {
|
|
158
|
+
const pending = [];
|
|
159
|
+
let pendingBytes = 0;
|
|
160
|
+
let rowsSeen = 0;
|
|
161
|
+
const flush = async () => {
|
|
162
|
+
if (pending.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
options.signal?.throwIfAborted();
|
|
165
|
+
await handle.writeFile(pending.join(""), { encoding: "utf8" });
|
|
166
|
+
pending.length = 0;
|
|
167
|
+
pendingBytes = 0;
|
|
168
|
+
};
|
|
169
|
+
// Written by the writer, not supplied: every reader can trust it without
|
|
170
|
+
// asking whoever produced the rows to have gotten it right.
|
|
171
|
+
const header = { version: 4, segmentId: input.segmentId, builtAt: input.builtAt };
|
|
172
|
+
const headerLine = `${JSON.stringify(header)}\n`;
|
|
173
|
+
await handle.writeFile(headerLine, { encoding: "utf8" });
|
|
174
|
+
state.offset = Buffer.byteLength(headerLine, "utf8");
|
|
175
|
+
for await (const row of asRowStream(input.rows)) {
|
|
176
|
+
options.signal?.throwIfAborted();
|
|
177
|
+
const kind = row[0];
|
|
178
|
+
const key = row[1];
|
|
179
|
+
const rank = REGION_RANK[kind];
|
|
180
|
+
if (rank < state.lastRank) {
|
|
181
|
+
fail("invalid_argument", `Segment rows are out of order: a "${kind}" row followed a row from a later region`, { segmentId: input.segmentId });
|
|
182
|
+
}
|
|
183
|
+
if (rank !== state.lastRank) {
|
|
184
|
+
if (state.lastRank !== -1)
|
|
185
|
+
state.regions[REGION_NAME[state.lastRank]][1] = state.offset;
|
|
186
|
+
state.regions[REGION_NAME[rank]] = [state.offset, state.offset];
|
|
187
|
+
state.lastRank = rank;
|
|
188
|
+
state.lastKey = undefined;
|
|
189
|
+
}
|
|
190
|
+
if (state.lastKey !== undefined && key <= state.lastKey) {
|
|
191
|
+
fail("invalid_argument", `Segment rows are out of order: "${key}" did not sort after "${state.lastKey}"`, { segmentId: input.segmentId });
|
|
192
|
+
}
|
|
193
|
+
state.lastKey = key;
|
|
194
|
+
if (kind === "t") {
|
|
195
|
+
if (state.tokenRowIndex % SEGMENT_DIRECTORY_STRIDE === 0)
|
|
196
|
+
state.entries.push([key, state.offset]);
|
|
197
|
+
state.tokenRowIndex += 1;
|
|
198
|
+
state.tokens += 1;
|
|
199
|
+
}
|
|
200
|
+
else if (kind === "a") {
|
|
201
|
+
state.archives += 1;
|
|
202
|
+
}
|
|
203
|
+
const serialized = `${JSON.stringify(row)}\n`;
|
|
204
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
205
|
+
if (pendingBytes > 0 && pendingBytes + bytes > COALESCE_BYTES)
|
|
206
|
+
await flush();
|
|
207
|
+
if (bytes > COALESCE_BYTES)
|
|
208
|
+
await handle.writeFile(serialized, { encoding: "utf8" });
|
|
209
|
+
else {
|
|
210
|
+
pending.push(serialized);
|
|
211
|
+
pendingBytes += bytes;
|
|
212
|
+
}
|
|
213
|
+
state.offset += bytes;
|
|
214
|
+
rowsSeen += 1;
|
|
215
|
+
if (rowsSeen % 2_048 === 0) {
|
|
216
|
+
if (options.onYield)
|
|
217
|
+
await options.onYield();
|
|
218
|
+
else
|
|
219
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (state.lastRank !== -1)
|
|
223
|
+
state.regions[REGION_NAME[state.lastRank]][1] = state.offset;
|
|
224
|
+
await flush();
|
|
225
|
+
await handle.sync();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
const sealedFile = await sealArchiveToFile({ path: plaintextTemp }, bodyTemp, {
|
|
231
|
+
recipients: [{ kind: "phrase", phrase }],
|
|
232
|
+
// Per chunk, not whole-file: the point of a segment is that a lookup
|
|
233
|
+
// opens two chunks of it, never the whole body.
|
|
234
|
+
compression: "zstd-chunk",
|
|
235
|
+
adapter: { agent: "sealkeep-index-segment", version: "1" },
|
|
236
|
+
signal: options.signal,
|
|
237
|
+
onYield: options.onYield,
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
await rename(bodyTemp, paths.body);
|
|
241
|
+
await writeSegmentFile(paths.envelope, `${JSON.stringify(sealedFile.envelope, null, 2)}\n`);
|
|
242
|
+
const directory = {
|
|
243
|
+
version: 1,
|
|
244
|
+
segmentId: input.segmentId,
|
|
245
|
+
envelopeArchiveId: sealedFile.envelope.archiveId,
|
|
246
|
+
builtAt: input.builtAt,
|
|
247
|
+
stride: SEGMENT_DIRECTORY_STRIDE,
|
|
248
|
+
regions: state.regions,
|
|
249
|
+
entries: state.entries,
|
|
250
|
+
archives: state.archives,
|
|
251
|
+
tokens: state.tokens,
|
|
252
|
+
};
|
|
253
|
+
await writeSealedDirectory(paths.directory, phrase, directory);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
await Promise.all([
|
|
257
|
+
rm(paths.body, { force: true }),
|
|
258
|
+
rm(paths.envelope, { force: true }),
|
|
259
|
+
rm(paths.directory, { force: true }),
|
|
260
|
+
]);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
const bytes = sealedFile.envelope.chunks.reduce((total, chunk) => total + chunk.storedBytes, 0);
|
|
264
|
+
return { id: input.segmentId, builtAt: input.builtAt, archives: state.archives, tokens: state.tokens, bytes };
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await rm(plaintextTemp, { force: true }).catch(() => undefined);
|
|
268
|
+
await rm(bodyTemp, { force: true }).catch(() => undefined);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
async function readSegmentEnvelope(envelopePath, segmentId) {
|
|
272
|
+
let text;
|
|
273
|
+
try {
|
|
274
|
+
text = await readFile(envelopePath, "utf8");
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (isEnoent(error))
|
|
278
|
+
return fail("invalid_argument", `No segment ${segmentId} envelope at ${envelopePath}`, { segmentId });
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(text);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return fail("invalid_argument", `Segment ${segmentId} envelope is not valid JSON`, { segmentId });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function readSegmentDirectory(directoryPath, phrase, segmentId) {
|
|
289
|
+
let text;
|
|
290
|
+
try {
|
|
291
|
+
text = await readFile(directoryPath, "utf8");
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (isEnoent(error))
|
|
295
|
+
return fail("invalid_argument", `No segment ${segmentId} directory at ${directoryPath}`, { segmentId });
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
let stored;
|
|
299
|
+
try {
|
|
300
|
+
stored = JSON.parse(text);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return fail("invalid_argument", `Segment ${segmentId} directory is not valid JSON`, { segmentId });
|
|
304
|
+
}
|
|
305
|
+
if (stored.version !== 1 || !stored.envelope || typeof stored.ciphertext !== "string") {
|
|
306
|
+
return fail("invalid_argument", `Segment ${segmentId} directory is malformed`, { segmentId });
|
|
307
|
+
}
|
|
308
|
+
// A wrong phrase and a damaged directory ciphertext cannot be told apart
|
|
309
|
+
// from outside; reporting the phrase is the actionable answer for nearly
|
|
310
|
+
// every case that reaches this branch, exactly as the legacy whole-index
|
|
311
|
+
// reader in search.ts treats any decrypt failure of its own directory.
|
|
312
|
+
let plaintext;
|
|
313
|
+
try {
|
|
314
|
+
plaintext = openEnvelope(stored.envelope, Buffer.from(stored.ciphertext, "base64"), { phrase });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return fail("recovery_phrase_mismatch", `Segment ${segmentId} directory would not open with this phrase`, { segmentId });
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(plaintext.toString("utf8"));
|
|
321
|
+
if (parsed.version !== 1
|
|
322
|
+
|| typeof parsed.segmentId !== "string"
|
|
323
|
+
|| typeof parsed.envelopeArchiveId !== "string"
|
|
324
|
+
|| typeof parsed.builtAt !== "string"
|
|
325
|
+
|| typeof parsed.stride !== "number"
|
|
326
|
+
|| !parsed.regions
|
|
327
|
+
|| !Array.isArray(parsed.entries)
|
|
328
|
+
|| typeof parsed.archives !== "number"
|
|
329
|
+
|| typeof parsed.tokens !== "number") {
|
|
330
|
+
return fail("ciphertext_integrity_failed", `Segment ${segmentId} directory contents are invalid`, { segmentId });
|
|
331
|
+
}
|
|
332
|
+
return parsed;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
plaintext.fill(0);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export async function openSegment(dataDir, phrase, segmentId) {
|
|
339
|
+
const paths = await segmentPaths(dataDir, segmentId);
|
|
340
|
+
const envelope = await readSegmentEnvelope(paths.envelope, segmentId);
|
|
341
|
+
if (!supportsChunkAccess(envelope)) {
|
|
342
|
+
return fail("ciphertext_integrity_failed", `Segment ${segmentId} body is not chunk-addressable`, { segmentId });
|
|
343
|
+
}
|
|
344
|
+
const directoryIdentity = await stat(paths.directory)
|
|
345
|
+
.then((info) => `${info.size}:${info.mtimeMs}`)
|
|
346
|
+
.catch((error) => (isEnoent(error) ? fail("invalid_argument", `No segment ${segmentId} directory at ${paths.directory}`, { segmentId }) : Promise.reject(error)));
|
|
347
|
+
const cacheKey = `${segmentId}:${phraseDigest(phrase)}`;
|
|
348
|
+
let opened = openedSegments.get(cacheKey);
|
|
349
|
+
if (opened && (opened.envelopeArchiveId !== envelope.archiveId || opened.directoryIdentity !== directoryIdentity)) {
|
|
350
|
+
opened.archiveKey.fill(0);
|
|
351
|
+
openedSegments.delete(cacheKey);
|
|
352
|
+
opened = undefined;
|
|
353
|
+
}
|
|
354
|
+
if (!opened) {
|
|
355
|
+
const directory = await readSegmentDirectory(paths.directory, phrase, segmentId);
|
|
356
|
+
// Binds the sealed sidecar to the exact sealed body: a directory copied from
|
|
357
|
+
// another segment decrypts fine under the same phrase but names a body that
|
|
358
|
+
// is not this one.
|
|
359
|
+
if (directory.envelopeArchiveId !== envelope.archiveId) {
|
|
360
|
+
return fail("ciphertext_integrity_failed", `Segment ${segmentId} directory does not match its sealed body`, { segmentId });
|
|
361
|
+
}
|
|
362
|
+
// The directory opened under this phrase, so a body that will not is a
|
|
363
|
+
// damaged envelope, not a wrong phrase.
|
|
364
|
+
let archiveKey;
|
|
365
|
+
try {
|
|
366
|
+
archiveKey = unwrapWindowKey(envelope, { phrase });
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return fail("ciphertext_integrity_failed", `Segment ${segmentId} body key would not unwrap under the phrase that opened its directory`, { segmentId });
|
|
370
|
+
}
|
|
371
|
+
opened = { envelopeArchiveId: envelope.archiveId, directoryIdentity, directory, archiveKey };
|
|
372
|
+
rememberOpenedSegment(cacheKey, opened);
|
|
373
|
+
}
|
|
374
|
+
const { directory, archiveKey } = opened;
|
|
375
|
+
const expectedBytes = envelope.chunks.reduce((total, chunk) => total + chunk.storedBytes, 0);
|
|
376
|
+
let bodySize;
|
|
377
|
+
try {
|
|
378
|
+
bodySize = (await stat(paths.body)).size;
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (isEnoent(error))
|
|
382
|
+
return fail("invalid_argument", `Segment ${segmentId} has no sealed body in ${dataDir}`, { segmentId });
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
if (bodySize !== expectedBytes) {
|
|
386
|
+
return fail("ciphertext_integrity_failed", `Segment ${segmentId} body is ${bodySize} bytes; its envelope describes ${expectedBytes}`, { segmentId });
|
|
387
|
+
}
|
|
388
|
+
const handle = await open(paths.body, "r");
|
|
389
|
+
return new SealedSegmentReader(segmentId, directory, envelope, phrase, archiveKey, handle);
|
|
390
|
+
}
|
|
391
|
+
class SealedSegmentReader {
|
|
392
|
+
id;
|
|
393
|
+
directory;
|
|
394
|
+
#envelope;
|
|
395
|
+
#phrase;
|
|
396
|
+
/** Shared with the per-process cache; never zeroized by a reader. */
|
|
397
|
+
#archiveKey;
|
|
398
|
+
#handle;
|
|
399
|
+
#closed = false;
|
|
400
|
+
#fixed = null;
|
|
401
|
+
constructor(id, directory, envelope, phrase, archiveKey, handle) {
|
|
402
|
+
this.id = id;
|
|
403
|
+
this.directory = directory;
|
|
404
|
+
this.#envelope = envelope;
|
|
405
|
+
this.#phrase = phrase;
|
|
406
|
+
this.#archiveKey = archiveKey;
|
|
407
|
+
this.#handle = handle;
|
|
408
|
+
}
|
|
409
|
+
async #readRaw(start, end, signal) {
|
|
410
|
+
if (end <= start)
|
|
411
|
+
return Buffer.alloc(0);
|
|
412
|
+
signal?.throwIfAborted();
|
|
413
|
+
const window = chunkWindowForRange(this.#envelope, start, end);
|
|
414
|
+
const slice = Buffer.allocUnsafe(window.storedLength);
|
|
415
|
+
const { bytesRead } = await this.#handle.read(slice, 0, window.storedLength, window.storedOffset);
|
|
416
|
+
if (bytesRead !== window.storedLength) {
|
|
417
|
+
return fail("ciphertext_integrity_failed", `Segment ${this.id} body is shorter than its envelope describes`, { segmentId: this.id });
|
|
418
|
+
}
|
|
419
|
+
return openChunkWindow(this.#envelope, window, slice, { phrase: this.#phrase }, { archiveKey: this.#archiveKey, signal });
|
|
420
|
+
}
|
|
421
|
+
async #readRows(start, end, signal) {
|
|
422
|
+
const raw = await this.#readRaw(start, end, signal);
|
|
423
|
+
if (raw.length === 0)
|
|
424
|
+
return [];
|
|
425
|
+
const rows = [];
|
|
426
|
+
for (const line of raw.toString("utf8").split("\n"))
|
|
427
|
+
if (line)
|
|
428
|
+
rows.push(JSON.parse(line));
|
|
429
|
+
return rows;
|
|
430
|
+
}
|
|
431
|
+
async #fixedRegions(signal) {
|
|
432
|
+
if (this.#fixed)
|
|
433
|
+
return this.#fixed;
|
|
434
|
+
const [archiveRows, metaRows] = await Promise.all([
|
|
435
|
+
this.#readRows(this.directory.regions.archives[0], this.directory.regions.archives[1], signal),
|
|
436
|
+
this.#readRows(this.directory.regions.meta[0], this.directory.regions.meta[1], signal),
|
|
437
|
+
]);
|
|
438
|
+
const archives = {};
|
|
439
|
+
for (const row of archiveRows)
|
|
440
|
+
if (isArchiveRow(row))
|
|
441
|
+
archives[row[1]] = row.slice(2);
|
|
442
|
+
const meta = {};
|
|
443
|
+
for (const row of metaRows)
|
|
444
|
+
if (isMetaRow(row))
|
|
445
|
+
meta[row[1]] = row[2];
|
|
446
|
+
this.#fixed = { archives, meta };
|
|
447
|
+
return this.#fixed;
|
|
448
|
+
}
|
|
449
|
+
async lookup(terms, options = {}) {
|
|
450
|
+
const prefixKeep = options.prefixKeep ?? 64;
|
|
451
|
+
const signal = options.signal;
|
|
452
|
+
signal?.throwIfAborted();
|
|
453
|
+
const { archives, meta } = await this.#fixedRegions(signal);
|
|
454
|
+
const tokens = {};
|
|
455
|
+
const prefixSeen = new Map();
|
|
456
|
+
const { entries } = this.directory;
|
|
457
|
+
for (const term of new Set(terms)) {
|
|
458
|
+
if (entries.length === 0)
|
|
459
|
+
continue;
|
|
460
|
+
const position = Math.max(0, directoryPosition(entries, term));
|
|
461
|
+
let windows = 0;
|
|
462
|
+
for (let k = position; k < entries.length && windows < MAX_LOOKUP_WINDOWS_PER_TERM; k += 1, windows += 1) {
|
|
463
|
+
const start = entries[k][1];
|
|
464
|
+
const end = k + 1 < entries.length ? entries[k + 1][1] : this.directory.regions.tokens[1];
|
|
465
|
+
for (const row of await this.#readRows(start, end, signal)) {
|
|
466
|
+
if (!isTokenRow(row))
|
|
467
|
+
continue;
|
|
468
|
+
const token = row[1];
|
|
469
|
+
if (token === term) {
|
|
470
|
+
tokens[token] = row[2];
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (token.startsWith(term)) {
|
|
474
|
+
const seen = prefixSeen.get(term) ?? 0;
|
|
475
|
+
if (seen < prefixKeep) {
|
|
476
|
+
tokens[token] = row[2];
|
|
477
|
+
prefixSeen.set(term, seen + 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Rows are sorted: once the next stride entry's token no longer
|
|
482
|
+
// carries this prefix, nothing after it can either.
|
|
483
|
+
const next = entries[k + 1]?.[0];
|
|
484
|
+
if (next === undefined || !next.startsWith(term))
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return { archives: { ...archives }, tokens, meta: { ...meta } };
|
|
489
|
+
}
|
|
490
|
+
async *rows(region, options = {}) {
|
|
491
|
+
const [regionStart, regionEnd] = this.directory.regions[region];
|
|
492
|
+
const signal = options.signal;
|
|
493
|
+
let cursor = regionStart;
|
|
494
|
+
let carry = "";
|
|
495
|
+
while (cursor < regionEnd) {
|
|
496
|
+
signal?.throwIfAborted();
|
|
497
|
+
const windowEnd = Math.min(cursor + ROWS_WINDOW_BYTES, regionEnd);
|
|
498
|
+
const raw = await this.#readRaw(cursor, windowEnd, signal);
|
|
499
|
+
cursor = windowEnd;
|
|
500
|
+
const text = carry + raw.toString("utf8");
|
|
501
|
+
const lines = text.split("\n");
|
|
502
|
+
carry = lines.pop() ?? "";
|
|
503
|
+
for (const line of lines)
|
|
504
|
+
if (line)
|
|
505
|
+
yield JSON.parse(line);
|
|
506
|
+
}
|
|
507
|
+
if (carry)
|
|
508
|
+
yield JSON.parse(carry);
|
|
509
|
+
}
|
|
510
|
+
async close() {
|
|
511
|
+
if (this.#closed)
|
|
512
|
+
return;
|
|
513
|
+
this.#closed = true;
|
|
514
|
+
await this.#handle.close();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type SegmentEntry, type SegmentManifest, type SegmentRow } from "./index-segment-types.js";
|
|
2
|
+
import { applyArchiveTokens, type ContentIndex } from "./search.js";
|
|
3
|
+
import { type RemoteIndexStore } from "./index-sync.js";
|
|
4
|
+
type IndexableRecord = Parameters<typeof applyArchiveTokens>[1];
|
|
5
|
+
/** Sorted "a", then "t", then "m" rows — the region order every segment
|
|
6
|
+
* writer requires. Declared synchronous (not `async`, despite the design
|
|
7
|
+
* doc's signature): an `async function` may only return a `Promise`, and
|
|
8
|
+
* every caller here — `writeSegment`'s `rows` parameter — accepts a plain
|
|
9
|
+
* `Iterable<SegmentRow>` directly, so there is nothing for a Promise to buy. */
|
|
10
|
+
export declare function segmentRowsFromIndex(index: ContentIndex): Iterable<SegmentRow>;
|
|
11
|
+
/**
|
|
12
|
+
* Segments mode is a one-way door for a vault: once `sealkeep index migrate`
|
|
13
|
+
* has run, it must never silently fall back to the legacy whole-blob build
|
|
14
|
+
* (and its whole-index-in-memory cost) just because `dropContentIndex`
|
|
15
|
+
* happened to empty the manifest. So this is a FILE-existence check, not a
|
|
16
|
+
* segment-count one — `dropContentIndex` deliberately leaves behind an empty
|
|
17
|
+
* manifest (see `resetSegments`) rather than removing it, precisely so this
|
|
18
|
+
* keeps answering true.
|
|
19
|
+
*/
|
|
20
|
+
export declare function segmentsEnabled(dataDir: string): Promise<boolean>;
|
|
21
|
+
/** The archive ids segments cover on this machine: the union of every
|
|
22
|
+
* segment's `archiveIds` minus tombstones — exactly what `coverage.json`
|
|
23
|
+
* holds, computed straight from the plaintext manifest. No segment body is
|
|
24
|
+
* opened and no phrase is needed. */
|
|
25
|
+
export declare function manifestCoverage(dataDir: string): Promise<Set<string>>;
|
|
26
|
+
/** Archives another machine sealed whose rows a segment here still carries — searchable, never this machine's to prune. */
|
|
27
|
+
export declare function manifestRemoteCoverage(dataDir: string): Promise<Set<string>>;
|
|
28
|
+
/**
|
|
29
|
+
* `coverage.json` exactly as `saveLocal` writes it in search.ts: the unsealed
|
|
30
|
+
* sidecar `index status`, doctor, and the daemon read to answer "what is
|
|
31
|
+
* searchable" without the recovery phrase. Always re-derived from the
|
|
32
|
+
* manifest, so it can never drift from what the segments actually cover.
|
|
33
|
+
*/
|
|
34
|
+
export declare function writeCoverageFromManifest(dataDir: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Empties a vault's segment set back to nothing while keeping it IN segments
|
|
37
|
+
* mode (the manifest file itself survives, freshly empty) — the counterpart
|
|
38
|
+
* `dropContentIndex` needs so a drop-then-rebuild never regresses to the
|
|
39
|
+
* legacy blob, and the extraction-policy bump needs so a stale segment set
|
|
40
|
+
* can be discarded wholesale rather than patched in place (segments are
|
|
41
|
+
* immutable; there is no way to re-extract one that already exists).
|
|
42
|
+
*/
|
|
43
|
+
export declare function resetSegments(dataDir: string): Promise<SegmentManifest>;
|
|
44
|
+
/**
|
|
45
|
+
* `planCompaction` over the current manifest; when it has work, stream the
|
|
46
|
+
* merge and replace the merged segments with the single result, then bring
|
|
47
|
+
* `coverage.json` and any stray `.skseg` files left by an interrupted prior
|
|
48
|
+
* attempt back in line with the manifest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function maybeCompact(dataDir: string, phrase: string, options?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
onYield?: () => void | Promise<void>;
|
|
53
|
+
smallOnly?: boolean;
|
|
54
|
+
}): Promise<SegmentEntry | null>;
|
|
55
|
+
/** One archive, sealed at seal time: no whole-index rewrite, just a new
|
|
56
|
+
* one-archive segment. Also publishes it (step 2): a segment is exactly the
|
|
57
|
+
* unit a peer wants as soon as possible, not just at the next background
|
|
58
|
+
* build. A publish failure never fails the seal that produced the segment —
|
|
59
|
+
* it is already durable locally — so it is caught and named for the next
|
|
60
|
+
* successful publish (a build's, or this machine's next seal) to pick up. */
|
|
61
|
+
export declare function appendArchiveSegment(dataDir: string, phrase: string, record: IndexableRecord, tokensByChunk: ReadonlyMap<number, ReadonlySet<string>>, options?: {
|
|
62
|
+
sync?: boolean;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
remoteIdleMs?: number;
|
|
65
|
+
maxBytesPerSecond?: number;
|
|
66
|
+
}): Promise<SegmentEntry>;
|
|
67
|
+
/** The catch-up build's per-batch equivalent: one segment for however many
|
|
68
|
+
* archives a checkpoint's worth of indexing collected. */
|
|
69
|
+
export declare function appendBatchSegment(dataDir: string, phrase: string, batch: ContentIndex, options?: {
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
onYield?: () => void | Promise<void>;
|
|
72
|
+
}): Promise<SegmentEntry | null>;
|
|
73
|
+
export declare function retireArchiveIds(dataDir: string, archiveIds: readonly string[]): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Open every segment, answer `terms` from each concurrently, and merge into
|
|
76
|
+
* one `ContentIndex` a query-time caller reads exactly like the legacy blob:
|
|
77
|
+
* archives keep the richer (longer) row, meta merges field by field with the
|
|
78
|
+
* later (newest-last) segment winning except a real `summary` over scaffold
|
|
79
|
+
* and unioned `commits`/`files`, and token postings union, drop anything
|
|
80
|
+
* tombstoned, and rank chronologically before the usual per-token cap.
|
|
81
|
+
*/
|
|
82
|
+
export declare function lookupSegments(dataDir: string, phrase: string, terms: readonly string[], options?: {
|
|
83
|
+
prefixKeep?: number;
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
onYield?: () => void | Promise<void>;
|
|
86
|
+
}): Promise<ContentIndex>;
|
|
87
|
+
/**
|
|
88
|
+
* The blob for cloud publication, produced from segments without ever
|
|
89
|
+
* holding more than the streaming merge needs: `mergeSegmentRows` folds every
|
|
90
|
+
* segment into archive/token/meta rows with tombstones already applied, and
|
|
91
|
+
* `persistContentIndex` seals and pushes exactly as the legacy build does.
|
|
92
|
+
* `maxTokens` is the background child's guard — a merge that would exceed it
|
|
93
|
+
* is abandoned (nothing written) rather than produced and then rejected.
|
|
94
|
+
*
|
|
95
|
+
* The blob-fallback publish path (step 2 addendum): a store with no generic
|
|
96
|
+
* keyed storage (managed today) cannot hold segments, so this is still how
|
|
97
|
+
* such a vault reaches the cloud at all. A successful push also records
|
|
98
|
+
* `published-blob.json`, so `publishPending` knows the manifest it merged is
|
|
99
|
+
* no longer waiting, and a peer without segments support (`pullSegments`'s
|
|
100
|
+
* own blob fallback) can tell its own publication apart from one worth
|
|
101
|
+
* converting.
|
|
102
|
+
*/
|
|
103
|
+
export declare function publishFromSegments(dataDir: string, phrase: string, options: {
|
|
104
|
+
store: RemoteIndexStore | null;
|
|
105
|
+
signal?: AbortSignal;
|
|
106
|
+
onYield?: () => void | Promise<void>;
|
|
107
|
+
maxTokens?: number;
|
|
108
|
+
}): Promise<boolean>;
|
|
109
|
+
/**
|
|
110
|
+
* One-time move of the existing whole-index blob into a single segment. The
|
|
111
|
+
* blob files are left exactly where they are — the cloud path still reads
|
|
112
|
+
* them until the next publish, and a vault with neither blob nor manifest is
|
|
113
|
+
* simply "build one" (unchanged). Accepts a raw or already-canonical phrase,
|
|
114
|
+
* like the other public entry points in search.ts (`buildContentIndex`,
|
|
115
|
+
* `loadContentIndex`): `sealkeep index migrate` hands it the CLI's raw
|
|
116
|
+
* `--recovery-phrase` value directly.
|
|
117
|
+
*/
|
|
118
|
+
export declare function migrateBlobToSegments(dataDir: string, rawPhrase: string): Promise<{
|
|
119
|
+
migrated: boolean;
|
|
120
|
+
archives: number;
|
|
121
|
+
tokens: number;
|
|
122
|
+
}>;
|
|
123
|
+
export {};
|