sealkeep 0.8.1 → 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 +244 -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 +1746 -166
- 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.d.ts +1 -0
- package/dist/src/migrate.js +33 -5
- 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 +235 -29
- package/dist/src/vault.js +1783 -190
- 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,349 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { decryptArchive, encryptArchive, x25519PublicKeyFromRaw, } from "../packages/sealkeep-crypto/src/index.js";
|
|
5
|
+
import { approveCloudDeviceEnrollment, bootstrapMachineRefFor, bootstrapTokenRefFor, claimCloudDeviceEnrollment, cloudUrl, completeCloudDeviceEnrollment, createCloudDeviceEnrollment, listPendingCloudDeviceEnrollments, machineRefFor, tokenRefFor, } from "./cloud.js";
|
|
6
|
+
import { equalHex, phraseCheck } from "./crypto.js";
|
|
7
|
+
import { fail, isSealkeepError } from "./errors.js";
|
|
8
|
+
import { canonicalPhrase } from "./mnemonic.js";
|
|
9
|
+
import { chooseBackend, forgetRecoveryPhrase, rememberRecoveryPhrase, resolveRecoveryPhrase, } from "./secrets.js";
|
|
10
|
+
import { initializeTransferredVault, readConfig, writeConfig } from "./vault.js";
|
|
11
|
+
/**
|
|
12
|
+
* Fresh-device enrollment without key custody.
|
|
13
|
+
*
|
|
14
|
+
* Cloud authentication links the account, but it must not become a decryption
|
|
15
|
+
* key. A fresh machine therefore creates an ephemeral X25519 keypair locally.
|
|
16
|
+
* An already-unlocked machine seals the vault identity directly to its public
|
|
17
|
+
* half, and SealKeep Cloud relays only that ciphertext. The target private key
|
|
18
|
+
* is stored in this machine's native secret backend and never crosses HTTP.
|
|
19
|
+
*/
|
|
20
|
+
const STATE_SERVICE = "sealkeep-device-enrollment";
|
|
21
|
+
const PAYLOAD_KIND = "sealkeep-personal-vault-enrollment";
|
|
22
|
+
const PAYLOAD_VERSION = 1;
|
|
23
|
+
const enrollmentStateRef = (requestId) => ({
|
|
24
|
+
service: STATE_SERVICE,
|
|
25
|
+
account: `${requestId}@${cloudUrl()}`,
|
|
26
|
+
});
|
|
27
|
+
function parseRawPublicKey(publicKey) {
|
|
28
|
+
const raw = Buffer.from(publicKey, "base64");
|
|
29
|
+
if (raw.length !== 32 || raw.toString("base64") !== publicKey) {
|
|
30
|
+
fail("invalid_argument", "A device enrollment public key must be a canonical 32-byte X25519 key");
|
|
31
|
+
}
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
/** A short comparison code derived from the key itself, never supplied by Cloud. */
|
|
35
|
+
export function deviceEnrollmentVerificationCode(publicKey) {
|
|
36
|
+
const digest = createHash("sha256").update(parseRawPublicKey(publicKey)).digest();
|
|
37
|
+
return String(digest.readUInt32BE(0) % 1_000_000).padStart(6, "0");
|
|
38
|
+
}
|
|
39
|
+
export function generateDeviceEnrollmentKeys() {
|
|
40
|
+
const pair = generateKeyPairSync("x25519");
|
|
41
|
+
return {
|
|
42
|
+
publicKey: pair.publicKey.export({ type: "spki", format: "der" }).subarray(-32).toString("base64"),
|
|
43
|
+
privateKey: pair.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function assertEnrollmentKeyPair(keys) {
|
|
47
|
+
parseRawPublicKey(keys.publicKey);
|
|
48
|
+
try {
|
|
49
|
+
const privateKey = createPrivateKey({ key: Buffer.from(keys.privateKey, "base64"), type: "pkcs8", format: "der" });
|
|
50
|
+
const derived = createPublicKey(privateKey).export({ type: "spki", format: "der" }).subarray(-32);
|
|
51
|
+
if (!derived.equals(Buffer.from(keys.publicKey, "base64")))
|
|
52
|
+
throw new Error("public key does not match");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
fail("invalid_argument", "The local device enrollment key pair is not valid");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function rememberDraft(dataDir, draft, preferred) {
|
|
59
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
60
|
+
await backend.set(enrollmentStateRef(draft.requestId), JSON.stringify(draft));
|
|
61
|
+
}
|
|
62
|
+
export async function readDeviceEnrollmentDraft(dataDir, requestId, preferred) {
|
|
63
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
64
|
+
const raw = await backend.get(enrollmentStateRef(requestId));
|
|
65
|
+
if (!raw)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const draft = JSON.parse(raw);
|
|
69
|
+
if (draft.version !== 1 || draft.requestId !== requestId || !draft.privateKey || !draft.requestSecret)
|
|
70
|
+
return null;
|
|
71
|
+
if (deviceEnrollmentVerificationCode(draft.publicKey) !== draft.verificationCode)
|
|
72
|
+
return null;
|
|
73
|
+
return draft;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Starts the target side after ordinary Cloud sign-in. The request secret is a
|
|
81
|
+
* capability for claiming this one response; it is not a vault key and cannot
|
|
82
|
+
* decrypt any archive.
|
|
83
|
+
*/
|
|
84
|
+
export async function beginDeviceEnrollment(dataDir, label, preferred) {
|
|
85
|
+
return beginDeviceEnrollmentWithKeys(dataDir, label, generateDeviceEnrollmentKeys(), preferred);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Starts the existing-vault approval lane with a key pair prepared before
|
|
89
|
+
* hosted account authorization. Reusing that exact public key means the
|
|
90
|
+
* account page and the existing device approve the same target, instead of
|
|
91
|
+
* silently changing identity between the two steps.
|
|
92
|
+
*/
|
|
93
|
+
export async function beginDeviceEnrollmentWithKeys(dataDir, label, keys, preferred) {
|
|
94
|
+
const cleanLabel = label.trim();
|
|
95
|
+
if (!cleanLabel)
|
|
96
|
+
fail("invalid_argument", "Name this machine before requesting your preserved memory");
|
|
97
|
+
assertEnrollmentKeyPair(keys);
|
|
98
|
+
const requestSecret = randomBytes(32).toString("base64url");
|
|
99
|
+
const created = await createCloudDeviceEnrollment(dataDir, {
|
|
100
|
+
label: cleanLabel,
|
|
101
|
+
publicKey: keys.publicKey,
|
|
102
|
+
requestSecret,
|
|
103
|
+
}, preferred);
|
|
104
|
+
const draft = {
|
|
105
|
+
version: 1,
|
|
106
|
+
requestId: created.id,
|
|
107
|
+
label: cleanLabel,
|
|
108
|
+
...keys,
|
|
109
|
+
requestSecret,
|
|
110
|
+
verificationCode: deviceEnrollmentVerificationCode(keys.publicKey),
|
|
111
|
+
createdAt: created.created_at,
|
|
112
|
+
expiresAt: created.expires_at,
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
await rememberDraft(dataDir, draft, preferred);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
fail("internal", `Cloud created the enrollment request, but this machine could not protect its private key locally: ${error instanceof Error ? error.message : "secret backend refused it"}. Do not approve this request; create a new one after the local keystore is available.`);
|
|
119
|
+
}
|
|
120
|
+
const { privateKey: _privateKey, requestSecret: _requestSecret, ...publicDraft } = draft;
|
|
121
|
+
return publicDraft;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Persists a request the hosted approval endpoint already created from the
|
|
125
|
+
* public key supplied at authorization start. The returned request secret is
|
|
126
|
+
* consumed only here, over the verifier-protected claim response, and then
|
|
127
|
+
* remains in the native secret backend with the private key.
|
|
128
|
+
*/
|
|
129
|
+
export async function rememberHostedDeviceEnrollment(dataDir, input, preferred) {
|
|
130
|
+
if (!Number.isSafeInteger(input.requestId) || input.requestId < 1 || input.requestSecret.length < 32) {
|
|
131
|
+
fail("internal", "Sealkeep Cloud returned an invalid preserved-memory enrollment request");
|
|
132
|
+
}
|
|
133
|
+
const cleanLabel = input.label.trim();
|
|
134
|
+
if (!cleanLabel)
|
|
135
|
+
fail("internal", "Sealkeep Cloud returned an unnamed preserved-memory enrollment request");
|
|
136
|
+
assertEnrollmentKeyPair(input.keys);
|
|
137
|
+
if (!Number.isFinite(Date.parse(input.createdAt)) || !Number.isFinite(Date.parse(input.expiresAt))) {
|
|
138
|
+
fail("internal", "Sealkeep Cloud returned invalid preserved-memory enrollment timing");
|
|
139
|
+
}
|
|
140
|
+
const draft = {
|
|
141
|
+
version: 1,
|
|
142
|
+
requestId: input.requestId,
|
|
143
|
+
label: cleanLabel,
|
|
144
|
+
...input.keys,
|
|
145
|
+
requestSecret: input.requestSecret,
|
|
146
|
+
verificationCode: deviceEnrollmentVerificationCode(input.keys.publicKey),
|
|
147
|
+
createdAt: input.createdAt,
|
|
148
|
+
expiresAt: input.expiresAt,
|
|
149
|
+
};
|
|
150
|
+
await rememberDraft(dataDir, draft, preferred);
|
|
151
|
+
const { privateKey: _privateKey, requestSecret: _requestSecret, ...publicDraft } = draft;
|
|
152
|
+
return publicDraft;
|
|
153
|
+
}
|
|
154
|
+
function enrollmentPayload(config, phrase) {
|
|
155
|
+
const canonical = canonicalPhrase(phrase);
|
|
156
|
+
if (!equalHex(config.recovery.phraseCheck, phraseCheck(canonical))) {
|
|
157
|
+
fail("recovery_phrase_mismatch", "This machine's stored recovery phrase does not open this vault, so enrollment was not approved");
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
kind: PAYLOAD_KIND,
|
|
161
|
+
version: PAYLOAD_VERSION,
|
|
162
|
+
vaultId: config.vaultId,
|
|
163
|
+
recoveryPhrase: canonical,
|
|
164
|
+
storageMode: config.storageMode === "plain" ? "plain" : "sealed",
|
|
165
|
+
...(config.recipients?.length ? { recipients: config.recipients } : {}),
|
|
166
|
+
...(config.remoteNaming ? { remoteNaming: config.remoteNaming } : {}),
|
|
167
|
+
approvedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function sealDeviceEnrollmentPayload(config, phrase, targetPublicKey) {
|
|
171
|
+
const raw = parseRawPublicKey(targetPublicKey);
|
|
172
|
+
const sealed = encryptArchive(Buffer.from(JSON.stringify(enrollmentPayload(config, phrase))), {
|
|
173
|
+
recipients: [{ kind: "x25519", publicKey: x25519PublicKeyFromRaw(raw) }],
|
|
174
|
+
adapter: { agent: "sealkeep-device-enrollment", version: String(PAYLOAD_VERSION) },
|
|
175
|
+
});
|
|
176
|
+
return { envelope: sealed.envelope, sealed: sealed.ciphertext.toString("base64") };
|
|
177
|
+
}
|
|
178
|
+
function openDeviceEnrollmentPayload(envelope, sealed, privateKeyBase64) {
|
|
179
|
+
let payload;
|
|
180
|
+
try {
|
|
181
|
+
const privateKey = createPrivateKey({ key: Buffer.from(privateKeyBase64, "base64"), type: "pkcs8", format: "der" });
|
|
182
|
+
payload = JSON.parse(decryptArchive(envelope, Buffer.from(sealed, "base64"), { privateKey }).toString("utf8"));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return fail("forbidden", "The approved enrollment package was not sealed to this machine. Nothing local was changed.");
|
|
186
|
+
}
|
|
187
|
+
if (!payload || typeof payload !== "object")
|
|
188
|
+
return fail("internal", "The approved enrollment package is malformed");
|
|
189
|
+
const value = payload;
|
|
190
|
+
if (value.kind !== PAYLOAD_KIND || value.version !== PAYLOAD_VERSION || typeof value.vaultId !== "string" || typeof value.recoveryPhrase !== "string") {
|
|
191
|
+
return fail("internal", "The approved enrollment package is not a supported SealKeep vault identity");
|
|
192
|
+
}
|
|
193
|
+
const phrase = canonicalPhrase(value.recoveryPhrase);
|
|
194
|
+
if (!phrase.trim())
|
|
195
|
+
return fail("internal", "The approved enrollment package contains no recovery material");
|
|
196
|
+
return { ...value, recoveryPhrase: phrase };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Existing-device side. Requiring the independently displayed code prevents a
|
|
200
|
+
* Cloud-side public-key substitution from being approved silently.
|
|
201
|
+
*/
|
|
202
|
+
export async function approveDeviceEnrollment(dataDir, requestId, displayedCode, preferred) {
|
|
203
|
+
const request = (await listPendingCloudDeviceEnrollments(dataDir, preferred)).find((item) => item.id === requestId);
|
|
204
|
+
if (!request)
|
|
205
|
+
fail("archive_not_found", "That device enrollment request is no longer pending");
|
|
206
|
+
const expected = deviceEnrollmentVerificationCode(request.public_key);
|
|
207
|
+
if (displayedCode.trim() !== expected) {
|
|
208
|
+
fail("forbidden", "The verification code does not match the new machine. Do not approve it; cancel and start enrollment again.");
|
|
209
|
+
}
|
|
210
|
+
const config = await readConfig(dataDir);
|
|
211
|
+
const phrase = await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, process.env, preferred);
|
|
212
|
+
if (!phrase) {
|
|
213
|
+
fail("recovery_phrase_missing", "This machine must be locally unlocked before it can approve another machine. Open SealKeep Settings and unlock this vault; do not send the phrase to Cloud or to the new machine.");
|
|
214
|
+
}
|
|
215
|
+
const bundle = sealDeviceEnrollmentPayload(config, phrase, request.public_key);
|
|
216
|
+
await approveCloudDeviceEnrollment(dataDir, requestId, bundle, preferred);
|
|
217
|
+
return { approved: true, requestId, label: request.label };
|
|
218
|
+
}
|
|
219
|
+
function restoredConfig(dataDir, payload) {
|
|
220
|
+
return {
|
|
221
|
+
version: 1,
|
|
222
|
+
vaultId: payload.vaultId,
|
|
223
|
+
storage: { provider: "local", root: join(dataDir, "archives") },
|
|
224
|
+
...(payload.storageMode === "plain" ? { storageMode: "plain" } : {}),
|
|
225
|
+
recovery: { phraseCheck: phraseCheck(payload.recoveryPhrase) },
|
|
226
|
+
...(payload.recipients?.length ? { recipients: payload.recipients } : {}),
|
|
227
|
+
...(payload.remoteNaming ? { remoteNaming: payload.remoteNaming } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function physicalArchiveCount(dataDir) {
|
|
231
|
+
return (await readdir(join(dataDir, "archives")).catch((error) => {
|
|
232
|
+
if (error.code === "ENOENT")
|
|
233
|
+
return [];
|
|
234
|
+
throw error;
|
|
235
|
+
})).filter((name) => name.endsWith(".json")).length;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Read-only adoption gate. Recovery codes call this before atomically spending
|
|
239
|
+
* a code, so an occupied data directory or a lost Cloud login cannot burn a
|
|
240
|
+
* valid code and then fail locally.
|
|
241
|
+
*/
|
|
242
|
+
export async function assertPersonalVaultAdoptable(dataDir, payload, preferred) {
|
|
243
|
+
const current = await readConfig(dataDir).catch((error) => {
|
|
244
|
+
if (isSealkeepError(error) && error.code === "vault_not_initialized")
|
|
245
|
+
return null;
|
|
246
|
+
throw error;
|
|
247
|
+
});
|
|
248
|
+
if (current?.vaultId === payload.vaultId)
|
|
249
|
+
return;
|
|
250
|
+
await assertFreshPersonalVaultTarget(dataDir, preferred);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Checks everything a fresh recovery target can check before a one-time code
|
|
254
|
+
* is redeemed. This deliberately does not need the encrypted vault payload.
|
|
255
|
+
*/
|
|
256
|
+
export async function assertFreshPersonalVaultTarget(dataDir, preferred) {
|
|
257
|
+
const current = await readConfig(dataDir).catch((error) => {
|
|
258
|
+
if (isSealkeepError(error) && error.code === "vault_not_initialized")
|
|
259
|
+
return null;
|
|
260
|
+
throw error;
|
|
261
|
+
});
|
|
262
|
+
// Count physical records, including malformed ones. A damaged archive must
|
|
263
|
+
// block replacement just as strongly as a readable archive; skipping it
|
|
264
|
+
// here could burn a one-time recovery code and overwrite evidence.
|
|
265
|
+
const existingArchives = await physicalArchiveCount(dataDir);
|
|
266
|
+
if (existingArchives > 0) {
|
|
267
|
+
fail("forbidden", "This machine already has local archives. SealKeep will not replace that vault during device enrollment; use a separate data directory or finish syncing the existing vault first.");
|
|
268
|
+
}
|
|
269
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
270
|
+
const oldTokenRef = current ? tokenRefFor(current.vaultId) : bootstrapTokenRefFor(dataDir);
|
|
271
|
+
if (!await backend.get(oldTokenRef)) {
|
|
272
|
+
fail("unauthorized", "This machine is no longer signed in to SealKeep Cloud, so its approved vault cannot be adopted");
|
|
273
|
+
}
|
|
274
|
+
return current;
|
|
275
|
+
}
|
|
276
|
+
export async function adoptPersonalVaultMaterial(dataDir, payload, preferred) {
|
|
277
|
+
await assertPersonalVaultAdoptable(dataDir, payload, preferred);
|
|
278
|
+
const current = await readConfig(dataDir).catch((error) => {
|
|
279
|
+
if (isSealkeepError(error) && error.code === "vault_not_initialized")
|
|
280
|
+
return null;
|
|
281
|
+
throw error;
|
|
282
|
+
});
|
|
283
|
+
if (current?.vaultId === payload.vaultId) {
|
|
284
|
+
await rememberRecoveryPhrase(dataDir, current.vaultId, payload.recoveryPhrase, preferred);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const existingArchives = await physicalArchiveCount(dataDir);
|
|
288
|
+
if (existingArchives > 0) {
|
|
289
|
+
fail("forbidden", "This machine already has local archives. SealKeep will not replace that vault during device enrollment; use a separate data directory or finish syncing the existing vault first.");
|
|
290
|
+
}
|
|
291
|
+
// Stage every new-vault secret before switching config identity. A crash can
|
|
292
|
+
// leave harmless duplicate keystore entries, but never a config whose Cloud
|
|
293
|
+
// token disappeared because it is still filed under a temporary or bootstrap
|
|
294
|
+
// identity.
|
|
295
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
296
|
+
const oldTokenRef = current ? tokenRefFor(current.vaultId) : bootstrapTokenRefFor(dataDir);
|
|
297
|
+
const oldMachineRef = current ? machineRefFor(current.vaultId) : bootstrapMachineRefFor(dataDir);
|
|
298
|
+
const nextTokenRef = tokenRefFor(payload.vaultId);
|
|
299
|
+
const nextMachineRef = machineRefFor(payload.vaultId);
|
|
300
|
+
const [token, machine] = await Promise.all([backend.get(oldTokenRef), backend.get(oldMachineRef)]);
|
|
301
|
+
if (!token)
|
|
302
|
+
fail("unauthorized", "This machine is no longer signed in to SealKeep Cloud, so its approved vault cannot be adopted");
|
|
303
|
+
try {
|
|
304
|
+
await backend.set(nextTokenRef, token);
|
|
305
|
+
if (machine)
|
|
306
|
+
await backend.set(nextMachineRef, machine);
|
|
307
|
+
await rememberRecoveryPhrase(dataDir, payload.vaultId, payload.recoveryPhrase, preferred);
|
|
308
|
+
const restored = restoredConfig(dataDir, payload);
|
|
309
|
+
if (current)
|
|
310
|
+
await writeConfig(dataDir, restored);
|
|
311
|
+
else
|
|
312
|
+
await initializeTransferredVault(dataDir, restored);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
await Promise.all([
|
|
316
|
+
backend.delete(nextTokenRef).catch(() => undefined),
|
|
317
|
+
backend.delete(nextMachineRef).catch(() => undefined),
|
|
318
|
+
forgetRecoveryPhrase(dataDir, payload.vaultId, preferred).catch(() => undefined),
|
|
319
|
+
]);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
await Promise.all([
|
|
323
|
+
backend.delete(oldTokenRef).catch(() => undefined),
|
|
324
|
+
backend.delete(oldMachineRef).catch(() => undefined),
|
|
325
|
+
...(current ? [forgetRecoveryPhrase(dataDir, current.vaultId, preferred).catch(() => undefined)] : []),
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
/** Target side: claim ciphertext, decrypt locally, adopt, then acknowledge. */
|
|
329
|
+
export async function completeDeviceEnrollment(dataDir, requestId, preferred) {
|
|
330
|
+
const draft = await readDeviceEnrollmentDraft(dataDir, requestId, preferred);
|
|
331
|
+
if (!draft)
|
|
332
|
+
fail("archive_not_found", "This machine no longer holds the private enrollment request. Start a new device enrollment request.");
|
|
333
|
+
const claimed = await claimCloudDeviceEnrollment(dataDir, requestId, draft.requestSecret, preferred);
|
|
334
|
+
const envelope = typeof claimed.envelope === "string" ? JSON.parse(claimed.envelope) : claimed.envelope;
|
|
335
|
+
const payload = openDeviceEnrollmentPayload(envelope, claimed.sealed, draft.privateKey);
|
|
336
|
+
await adoptPersonalVaultMaterial(dataDir, payload, preferred);
|
|
337
|
+
let cloudReceiptCleared = true;
|
|
338
|
+
try {
|
|
339
|
+
await completeCloudDeviceEnrollment(dataDir, requestId, draft.requestSecret, preferred);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
cloudReceiptCleared = false;
|
|
343
|
+
}
|
|
344
|
+
if (cloudReceiptCleared) {
|
|
345
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
346
|
+
await backend.delete(enrollmentStateRef(requestId)).catch(() => undefined);
|
|
347
|
+
}
|
|
348
|
+
return { requestId, vaultId: payload.vaultId, verificationCode: draft.verificationCode, cloudReceiptCleared };
|
|
349
|
+
}
|
package/dist/src/disk.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How much room archiving needs, and whether there is any.
|
|
3
3
|
*
|
|
4
|
-
* Archiving costs disk before it saves any.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* later, by a separate decision.
|
|
4
|
+
* Archiving costs disk before it saves any. The current chunked writer streams
|
|
5
|
+
* one compressed-and-sealed object to a partial file while the original still
|
|
6
|
+
* exists. That source is input already paid for; the new object's worst-case
|
|
7
|
+
* size, plus the standing reserve, is the additional free space admission must
|
|
8
|
+
* protect. The source is only ever reclaimed later, by a separate decision.
|
|
9
9
|
*
|
|
10
10
|
* That matters because of who this is for. The people who install this have
|
|
11
11
|
* nearly full disks; that is the problem it exists to solve. A background
|
|
@@ -40,18 +40,23 @@ export declare function resolveReserveBytes(dataDir: string): Promise<number>;
|
|
|
40
40
|
* `vault.ts` assumed agent transcripts compress about 2x. Measured on a real
|
|
41
41
|
* 1.6 GB Codex rollout it is 1.5x — the sealed copy is 68.8% of the source —
|
|
42
42
|
* because these files carry a lot of base64 and tool output that has already
|
|
43
|
-
* been compressed once.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* been compressed once. That remains useful when describing expected storage,
|
|
44
|
+
* but it cannot decide whether arbitrary not-yet-compressed input is safe to
|
|
45
|
+
* start.
|
|
46
46
|
*/
|
|
47
47
|
export declare const OBSERVED_COMPRESSION = 0.7;
|
|
48
|
+
/** The normal worker seals one independently searchable zstd frame at a time. */
|
|
49
|
+
export declare const LOCAL_SEAL_CHUNK_BYTES: number;
|
|
50
|
+
/** Maximum bytes the current local chunk-seal can add to the archive volume. */
|
|
51
|
+
export declare function maximumArchiveOutputBytes(sourceBytes: number): number;
|
|
48
52
|
/**
|
|
49
53
|
* What one archive of this size needs before it is safe to start.
|
|
50
54
|
*
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* The source already exists, and the chunked seal writes one new partial object
|
|
56
|
+
* beside it. Incompressible input is the worst case: the output can be larger
|
|
57
|
+
* than the source by the codec's documented bound plus one authentication tag
|
|
58
|
+
* per chunk. The measured 70% ratio above describes typical storage, never a
|
|
59
|
+
* safe admission threshold.
|
|
55
60
|
*/
|
|
56
61
|
export declare function spaceNeededFor(sourceBytes: number, reserveBytes?: number): number;
|
|
57
62
|
/** Free bytes on the volume holding `path`, or undefined where that cannot be asked. */
|
package/dist/src/disk.js
CHANGED
|
@@ -2,11 +2,11 @@ import { statfs } from "node:fs/promises";
|
|
|
2
2
|
/**
|
|
3
3
|
* How much room archiving needs, and whether there is any.
|
|
4
4
|
*
|
|
5
|
-
* Archiving costs disk before it saves any.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* later, by a separate decision.
|
|
5
|
+
* Archiving costs disk before it saves any. The current chunked writer streams
|
|
6
|
+
* one compressed-and-sealed object to a partial file while the original still
|
|
7
|
+
* exists. That source is input already paid for; the new object's worst-case
|
|
8
|
+
* size, plus the standing reserve, is the additional free space admission must
|
|
9
|
+
* protect. The source is only ever reclaimed later, by a separate decision.
|
|
10
10
|
*
|
|
11
11
|
* That matters because of who this is for. The people who install this have
|
|
12
12
|
* nearly full disks; that is the problem it exists to solve. A background
|
|
@@ -62,25 +62,51 @@ export async function resolveReserveBytes(dataDir) {
|
|
|
62
62
|
* `vault.ts` assumed agent transcripts compress about 2x. Measured on a real
|
|
63
63
|
* 1.6 GB Codex rollout it is 1.5x — the sealed copy is 68.8% of the source —
|
|
64
64
|
* because these files carry a lot of base64 and tool output that has already
|
|
65
|
-
* been compressed once.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* been compressed once. That remains useful when describing expected storage,
|
|
66
|
+
* but it cannot decide whether arbitrary not-yet-compressed input is safe to
|
|
67
|
+
* start.
|
|
68
68
|
*/
|
|
69
69
|
export const OBSERVED_COMPRESSION = 0.7;
|
|
70
|
+
/** The normal worker seals one independently searchable zstd frame at a time. */
|
|
71
|
+
export const LOCAL_SEAL_CHUNK_BYTES = 64 * 1024 * 1024;
|
|
72
|
+
const ZSTD_SMALL_INPUT_BYTES = 128 * 1024;
|
|
73
|
+
const ARCHIVE_AUTH_TAG_BYTES = 16;
|
|
74
|
+
/**
|
|
75
|
+
* Zstd's documented one-pass upper bound for one input buffer.
|
|
76
|
+
*
|
|
77
|
+
* This mirrors `ZSTD_COMPRESSBOUND`: unlike an observed compression ratio it
|
|
78
|
+
* remains true for already-compressed or adversarial input. The worker calls
|
|
79
|
+
* the one-shot codec once per chunk, then encryption appends one authentication
|
|
80
|
+
* tag to that chunk.
|
|
81
|
+
*/
|
|
82
|
+
function zstdCompressedBound(bytes) {
|
|
83
|
+
return bytes
|
|
84
|
+
+ Math.floor(bytes / 256)
|
|
85
|
+
+ (bytes < ZSTD_SMALL_INPUT_BYTES ? Math.floor((ZSTD_SMALL_INPUT_BYTES - bytes) / 2048) : 0);
|
|
86
|
+
}
|
|
87
|
+
/** Maximum bytes the current local chunk-seal can add to the archive volume. */
|
|
88
|
+
export function maximumArchiveOutputBytes(sourceBytes) {
|
|
89
|
+
const bytes = Math.max(0, Math.ceil(sourceBytes));
|
|
90
|
+
if (!Number.isFinite(bytes))
|
|
91
|
+
return Number.POSITIVE_INFINITY;
|
|
92
|
+
const completeChunks = Math.floor(bytes / LOCAL_SEAL_CHUNK_BYTES);
|
|
93
|
+
const remainder = bytes - completeChunks * LOCAL_SEAL_CHUNK_BYTES;
|
|
94
|
+
const chunks = Math.max(1, completeChunks + (remainder > 0 ? 1 : 0));
|
|
95
|
+
const compressed = completeChunks * zstdCompressedBound(LOCAL_SEAL_CHUNK_BYTES)
|
|
96
|
+
+ (remainder > 0 || completeChunks === 0 ? zstdCompressedBound(remainder) : 0);
|
|
97
|
+
return compressed + chunks * ARCHIVE_AUTH_TAG_BYTES;
|
|
98
|
+
}
|
|
70
99
|
/**
|
|
71
100
|
* What one archive of this size needs before it is safe to start.
|
|
72
101
|
*
|
|
73
|
-
* The
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
102
|
+
* The source already exists, and the chunked seal writes one new partial object
|
|
103
|
+
* beside it. Incompressible input is the worst case: the output can be larger
|
|
104
|
+
* than the source by the codec's documented bound plus one authentication tag
|
|
105
|
+
* per chunk. The measured 70% ratio above describes typical storage, never a
|
|
106
|
+
* safe admission threshold.
|
|
77
107
|
*/
|
|
78
108
|
export function spaceNeededFor(sourceBytes, reserveBytes = DISK_RESERVE_BYTES) {
|
|
79
|
-
|
|
80
|
-
// The ×2 that used to live here paid for the legacy layout's staged gzip
|
|
81
|
-
// body coexisting with the ciphertext; gzip-chunk seals in a single pass
|
|
82
|
-
// and the staging is gone. A 5 GB session now asks for ~5.5 GB free, not 9.
|
|
83
|
-
return Math.ceil(sourceBytes * Math.min(1, OBSERVED_COMPRESSION)) + reserveBytes;
|
|
109
|
+
return maximumArchiveOutputBytes(sourceBytes) + Math.max(0, Math.ceil(reserveBytes));
|
|
84
110
|
}
|
|
85
111
|
/** Free bytes on the volume holding `path`, or undefined where that cannot be asked. */
|
|
86
112
|
export async function freeBytes(path) {
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -8,4 +8,38 @@ export type DoctorReport = {
|
|
|
8
8
|
ok: boolean;
|
|
9
9
|
checks: Check[];
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The archive-store fact shared by Doctor and the dashboard status endpoint.
|
|
13
|
+
*
|
|
14
|
+
* Counts are deliberately metadata-only: this check never opens ciphertext or
|
|
15
|
+
* transcript files. `detail` contains at most the first affected filename,
|
|
16
|
+
* matching Doctor's existing safe diagnostic and never the storage directory.
|
|
17
|
+
*/
|
|
18
|
+
export type ArchiveIntegritySummary = {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Backward-compatible total of metadata files that could not be used. */
|
|
21
|
+
unreadableMetadataCount: number;
|
|
22
|
+
/** Files whose stable contents are not a valid archive record. */
|
|
23
|
+
invalidMetadataCount: number;
|
|
24
|
+
/** Files that still could not be read after bounded retries. */
|
|
25
|
+
transientMetadataReadErrorCount: number;
|
|
26
|
+
detachedArchiveCount: number;
|
|
27
|
+
/** The directory kept changing, so no coherent read-only snapshot was available. */
|
|
28
|
+
scanIncomplete: boolean;
|
|
29
|
+
detail: string;
|
|
30
|
+
};
|
|
31
|
+
export type ArchiveIntegrityScanOptions = {
|
|
32
|
+
/** Dependency seams keep descriptor-pressure and concurrent-change tests deterministic. */
|
|
33
|
+
readMetadata?: (path: string) => Promise<string>;
|
|
34
|
+
listDirectory?: (path: string) => Promise<string[]>;
|
|
35
|
+
concurrency?: number;
|
|
36
|
+
readAttempts?: number;
|
|
37
|
+
snapshotAttempts?: number;
|
|
38
|
+
};
|
|
39
|
+
/** Runtime context supplied by the local API when Doctor runs beside Autopilot. */
|
|
40
|
+
export type DoctorOptions = {
|
|
41
|
+
/** Native service-manager registration is present and running right now. */
|
|
42
|
+
nativeServicePresent?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export declare function archiveIntegritySummary(directory: string, options?: ArchiveIntegrityScanOptions): Promise<ArchiveIntegritySummary>;
|
|
45
|
+
export declare function runDoctor(dataDir: string, env?: NodeJS.ProcessEnv, home?: string, options?: DoctorOptions): Promise<DoctorReport>;
|