sealkeep 0.5.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 +201 -0
- package/CHANGELOG.md +218 -0
- package/CONTROL_PLANE.md +86 -0
- package/LICENSE +34 -0
- package/README.md +249 -0
- package/THIRD_PARTY.md +22 -0
- package/THREAT_MODEL.md +107 -0
- package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
- package/dist/packages/vaultline-crypto/src/aead.js +24 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
- package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
- package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
- package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
- package/dist/packages/vaultline-crypto/src/format.js +43 -0
- package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
- package/dist/packages/vaultline-crypto/src/index.js +5 -0
- package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
- package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
- package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
- package/dist/packages/vaultline-crypto/src/stream.js +477 -0
- package/dist/site/index.html +1542 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +22 -0
- package/dist/src/activity.js +52 -0
- package/dist/src/adapters.d.ts +212 -0
- package/dist/src/adapters.js +533 -0
- package/dist/src/audit.d.ts +24 -0
- package/dist/src/audit.js +41 -0
- package/dist/src/autopilot.d.ts +77 -0
- package/dist/src/autopilot.js +148 -0
- package/dist/src/bip39-wordlist.d.ts +15 -0
- package/dist/src/bip39-wordlist.js +272 -0
- package/dist/src/branding.d.ts +31 -0
- package/dist/src/branding.js +31 -0
- package/dist/src/chunk-store.d.ts +142 -0
- package/dist/src/chunk-store.js +502 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2035 -0
- package/dist/src/cloud.d.ts +434 -0
- package/dist/src/cloud.js +851 -0
- package/dist/src/control-plane/auth.d.ts +62 -0
- package/dist/src/control-plane/auth.js +123 -0
- package/dist/src/control-plane/server.d.ts +31 -0
- package/dist/src/control-plane/server.js +263 -0
- package/dist/src/control-plane/store.d.ts +101 -0
- package/dist/src/control-plane/store.js +82 -0
- package/dist/src/control-plane-cli.d.ts +2 -0
- package/dist/src/control-plane-cli.js +37 -0
- package/dist/src/control-plane-server.d.ts +10 -0
- package/dist/src/control-plane-server.js +11 -0
- package/dist/src/control-plane.d.ts +78 -0
- package/dist/src/control-plane.js +61 -0
- package/dist/src/crypto.d.ts +56 -0
- package/dist/src/crypto.js +132 -0
- package/dist/src/daemon.d.ts +52 -0
- package/dist/src/daemon.js +142 -0
- package/dist/src/dashboard-cli.d.ts +2 -0
- package/dist/src/dashboard-cli.js +20 -0
- package/dist/src/disk.d.ts +110 -0
- package/dist/src/disk.js +169 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +198 -0
- package/dist/src/enroll.d.ts +27 -0
- package/dist/src/enroll.js +136 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/heartbeat.d.ts +89 -0
- package/dist/src/heartbeat.js +120 -0
- package/dist/src/index-sync.d.ts +53 -0
- package/dist/src/index-sync.js +147 -0
- package/dist/src/leakscan.d.ts +48 -0
- package/dist/src/leakscan.js +222 -0
- package/dist/src/local-api.d.ts +132 -0
- package/dist/src/local-api.js +1757 -0
- package/dist/src/managed-chunks.d.ts +55 -0
- package/dist/src/managed-chunks.js +108 -0
- package/dist/src/mcp-install.d.ts +52 -0
- package/dist/src/mcp-install.js +140 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +59 -0
- package/dist/src/migrate.d.ts +35 -0
- package/dist/src/migrate.js +88 -0
- package/dist/src/mnemonic.d.ts +60 -0
- package/dist/src/mnemonic.js +134 -0
- package/dist/src/net.d.ts +2 -0
- package/dist/src/net.js +16 -0
- package/dist/src/notify.d.ts +46 -0
- package/dist/src/notify.js +84 -0
- package/dist/src/offload.d.ts +117 -0
- package/dist/src/offload.js +331 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +44 -0
- package/dist/src/packages.d.ts +126 -0
- package/dist/src/packages.js +114 -0
- package/dist/src/passkey.d.ts +26 -0
- package/dist/src/passkey.js +54 -0
- package/dist/src/password-lock.d.ts +19 -0
- package/dist/src/password-lock.js +156 -0
- package/dist/src/paths.d.ts +9 -0
- package/dist/src/paths.js +24 -0
- package/dist/src/providers/gcs.d.ts +133 -0
- package/dist/src/providers/gcs.js +235 -0
- package/dist/src/providers/gdrive.d.ts +156 -0
- package/dist/src/providers/gdrive.js +335 -0
- package/dist/src/providers/index.d.ts +45 -0
- package/dist/src/providers/index.js +74 -0
- package/dist/src/providers/s3.d.ts +174 -0
- package/dist/src/providers/s3.js +345 -0
- package/dist/src/providers/sigv4.d.ts +78 -0
- package/dist/src/providers/sigv4.js +112 -0
- package/dist/src/queue.d.ts +185 -0
- package/dist/src/queue.js +286 -0
- package/dist/src/recovery.d.ts +40 -0
- package/dist/src/recovery.js +132 -0
- package/dist/src/rehydrate.d.ts +43 -0
- package/dist/src/rehydrate.js +66 -0
- package/dist/src/restore.d.ts +34 -0
- package/dist/src/restore.js +80 -0
- package/dist/src/retention.d.ts +251 -0
- package/dist/src/retention.js +446 -0
- package/dist/src/rotate.d.ts +47 -0
- package/dist/src/rotate.js +95 -0
- package/dist/src/search.d.ts +147 -0
- package/dist/src/search.js +677 -0
- package/dist/src/secrets.d.ts +86 -0
- package/dist/src/secrets.js +220 -0
- package/dist/src/service.d.ts +73 -0
- package/dist/src/service.js +197 -0
- package/dist/src/share.d.ts +34 -0
- package/dist/src/share.js +68 -0
- package/dist/src/spool.d.ts +97 -0
- package/dist/src/spool.js +213 -0
- package/dist/src/start-tui.d.ts +17 -0
- package/dist/src/start-tui.js +113 -0
- package/dist/src/start.d.ts +75 -0
- package/dist/src/start.js +101 -0
- package/dist/src/storage-setup.d.ts +49 -0
- package/dist/src/storage-setup.js +222 -0
- package/dist/src/storage-targets.d.ts +40 -0
- package/dist/src/storage-targets.js +147 -0
- package/dist/src/stream-to-cloud.d.ts +76 -0
- package/dist/src/stream-to-cloud.js +820 -0
- package/dist/src/sync-rules.d.ts +85 -0
- package/dist/src/sync-rules.js +125 -0
- package/dist/src/trash.d.ts +15 -0
- package/dist/src/trash.js +63 -0
- package/dist/src/tui.d.ts +18 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/types.d.ts +191 -0
- package/dist/src/types.js +3 -0
- package/dist/src/ui-server.d.ts +187 -0
- package/dist/src/ui-server.js +293 -0
- package/dist/src/ui.d.ts +41 -0
- package/dist/src/ui.js +102 -0
- package/dist/src/update.d.ts +30 -0
- package/dist/src/update.js +56 -0
- package/dist/src/upload.d.ts +46 -0
- package/dist/src/upload.js +80 -0
- package/dist/src/vault.d.ts +208 -0
- package/dist/src/vault.js +812 -0
- package/dist/src/watcher.d.ts +34 -0
- package/dist/src/watcher.js +121 -0
- package/dist/src/worker.d.ts +52 -0
- package/dist/src/worker.js +190 -0
- package/package.json +65 -0
- package/web/app.js +1372 -0
- package/web/index.html +476 -0
- package/web/rail.js +308 -0
- package/web/retention.html +17 -0
- package/web/rules-view.js +249 -0
- package/web/sessions-view.js +448 -0
- package/web/sessions.html +17 -0
- package/web/setup-api.js +181 -0
- package/web/setup-logic.js +394 -0
- package/web/setup.html +419 -0
- package/web/setup.js +697 -0
- package/web/style.css +990 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fail } from "./errors.js";
|
|
4
|
+
/** Deterministic bytes to sign: object keys sorted, no incidental whitespace. */
|
|
5
|
+
export function canonicalize(value) {
|
|
6
|
+
if (value === null || typeof value !== "object")
|
|
7
|
+
return JSON.stringify(value) ?? "null";
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
10
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
11
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalize(item)}`).join(",")}}`;
|
|
12
|
+
}
|
|
13
|
+
export function signManifest(manifest, privateKeyPem, keyId) {
|
|
14
|
+
const key = createPrivateKey(privateKeyPem);
|
|
15
|
+
if (key.asymmetricKeyType !== "ed25519")
|
|
16
|
+
fail("invalid_argument", "Release manifests must be signed with an ed25519 key");
|
|
17
|
+
return { manifest, keyId, algorithm: "ed25519", signature: edSign(null, Buffer.from(canonicalize(manifest)), key).toString("base64") };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Verifies a release manifest against a pinned key set. An unknown key id fails
|
|
21
|
+
* closed: a signature is only meaningful against a key the client already trusts.
|
|
22
|
+
*/
|
|
23
|
+
export function verifyManifest(signed, trustedKeys) {
|
|
24
|
+
if (signed.algorithm !== "ed25519")
|
|
25
|
+
fail("invalid_argument", `Unsupported signature algorithm: ${String(signed.algorithm)}`);
|
|
26
|
+
const publicKeyPem = trustedKeys[signed.keyId];
|
|
27
|
+
if (!publicKeyPem)
|
|
28
|
+
fail("forbidden", `Release signed by untrusted key id ${signed.keyId}`, { keyId: signed.keyId });
|
|
29
|
+
const valid = edVerify(null, Buffer.from(canonicalize(signed.manifest)), createPublicKey(publicKeyPem), Buffer.from(signed.signature, "base64"));
|
|
30
|
+
if (!valid)
|
|
31
|
+
fail("forbidden", "Release manifest signature does not verify", { keyId: signed.keyId });
|
|
32
|
+
return signed.manifest;
|
|
33
|
+
}
|
|
34
|
+
function parseRelease(release) {
|
|
35
|
+
const parts = release.replace(/^v/, "").split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
36
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part)))
|
|
37
|
+
fail("invalid_argument", `Unreadable release version: ${release}`);
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
/** Blocks rollback: a signed but older manifest must never be applied automatically. */
|
|
41
|
+
export function assertNotDowngrade(candidate, current) {
|
|
42
|
+
const [a, b, c] = parseRelease(candidate);
|
|
43
|
+
const [x, y, z] = parseRelease(current);
|
|
44
|
+
const newer = a > x || (a === x && (b > y || (b === y && c > z)));
|
|
45
|
+
if (!newer)
|
|
46
|
+
fail("forbidden", `Refusing to apply release ${candidate} over installed ${current}`, { candidate, current });
|
|
47
|
+
}
|
|
48
|
+
/** Confirms a downloaded artifact matches the signed manifest entry byte for byte. */
|
|
49
|
+
export async function verifyArtifact(path, entry) {
|
|
50
|
+
const bytes = await readFile(path).catch(() => fail("source_unreadable", `Release artifact is not readable: ${path}`, { path }));
|
|
51
|
+
if (bytes.length !== entry.bytes)
|
|
52
|
+
fail("ciphertext_integrity_failed", `Artifact ${entry.name} is ${bytes.length} bytes; the manifest says ${entry.bytes}`, { artifact: entry.name });
|
|
53
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
54
|
+
if (digest !== entry.sha256)
|
|
55
|
+
fail("ciphertext_integrity_failed", `Artifact ${entry.name} failed its checksum`, { artifact: entry.name });
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ProviderUploadClient, type UploadLease } from "./control-plane.js";
|
|
2
|
+
import { type EndpointOverrides } from "./providers/index.js";
|
|
3
|
+
export type UploadOptions = {
|
|
4
|
+
/** Routed multi-target override: upload to THIS storage instead of config.remoteStorage. */
|
|
5
|
+
remoteStorage?: {
|
|
6
|
+
provider: "s3" | "r2" | "b2" | "gcs" | "gdrive";
|
|
7
|
+
bucket: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
region?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Stamped into the record's remote for per-target usage accounting. */
|
|
12
|
+
targetId?: string;
|
|
13
|
+
/** Supply a client directly (tests, or an already-authenticated session). */
|
|
14
|
+
client?: ProviderUploadClient;
|
|
15
|
+
storageConfigId?: string;
|
|
16
|
+
overrides?: EndpointOverrides;
|
|
17
|
+
now?: number;
|
|
18
|
+
ttlMs?: number;
|
|
19
|
+
};
|
|
20
|
+
export type UploadOutcome = {
|
|
21
|
+
archiveId: string;
|
|
22
|
+
objectKey: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
checksum: string;
|
|
25
|
+
verified: boolean;
|
|
26
|
+
provider: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Uploads one archive's ciphertext and verifies it landed intact.
|
|
30
|
+
*
|
|
31
|
+
* The archive is only marked durable after the provider reports back the same byte
|
|
32
|
+
* count and the same SHA-256 the client computed locally. Anything else quarantines
|
|
33
|
+
* the attempt and leaves the source untouched: retention reads `remote.verifiedAt`,
|
|
34
|
+
* so an unverified upload can never make a local file reclaimable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function uploadArchive(dataDir: string, archiveId: string, options?: UploadOptions): Promise<UploadOutcome>;
|
|
37
|
+
/** Uploads every archive that has no verified remote object yet. */
|
|
38
|
+
export declare function uploadPending(dataDir: string, options?: UploadOptions): Promise<{
|
|
39
|
+
uploaded: UploadOutcome[];
|
|
40
|
+
failed: {
|
|
41
|
+
archiveId: string;
|
|
42
|
+
error: string;
|
|
43
|
+
}[];
|
|
44
|
+
skipped: number;
|
|
45
|
+
}>;
|
|
46
|
+
export type { UploadLease };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { assertLeaseUsable } from "./control-plane.js";
|
|
4
|
+
import { fail } from "./errors.js";
|
|
5
|
+
import { recordAudit } from "./audit.js";
|
|
6
|
+
import { createActiveLease } from "./providers/index.js";
|
|
7
|
+
import { uploadClientFromStore } from "./providers/index.js";
|
|
8
|
+
import { isV2 } from "./types.js";
|
|
9
|
+
import { listArchives, readConfig, writeRecord } from "./vault.js";
|
|
10
|
+
/**
|
|
11
|
+
* Uploads one archive's ciphertext and verifies it landed intact.
|
|
12
|
+
*
|
|
13
|
+
* The archive is only marked durable after the provider reports back the same byte
|
|
14
|
+
* count and the same SHA-256 the client computed locally. Anything else quarantines
|
|
15
|
+
* the attempt and leaves the source untouched: retention reads `remote.verifiedAt`,
|
|
16
|
+
* so an unverified upload can never make a local file reclaimable.
|
|
17
|
+
*/
|
|
18
|
+
export async function uploadArchive(dataDir, archiveId, options = {}) {
|
|
19
|
+
const config = await readConfig(dataDir);
|
|
20
|
+
const remoteStorage = options.remoteStorage ?? config.remoteStorage ?? fail("storage_not_configured", "No remote target configured. Run: sealkeep storage configure ...");
|
|
21
|
+
const record = (await listArchives(dataDir)).find((item) => item.id === archiveId);
|
|
22
|
+
if (!record)
|
|
23
|
+
fail("archive_not_found", `Archive not found: ${archiveId}`, { archiveId });
|
|
24
|
+
if (!isV2(record))
|
|
25
|
+
fail("invalid_argument", `Archive ${archiveId} uses format v1. Run: sealkeep migrate`, { archiveId });
|
|
26
|
+
const ciphertext = await readFile(record.objectPath);
|
|
27
|
+
const localChecksum = createHash("sha256").update(ciphertext).digest("base64");
|
|
28
|
+
const providerConfig = remoteStorage;
|
|
29
|
+
const lease = createActiveLease(providerConfig, { archiveId, ciphertextSha256: record.cipher.ciphertextSha256, bytes: ciphertext.length, now: options.now, ttlMs: options.ttlMs });
|
|
30
|
+
assertLeaseUsable(lease, options.now);
|
|
31
|
+
const client = options.client ?? await uploadClientFromStore(dataDir, options.storageConfigId ?? config.vaultId, providerConfig, options.overrides);
|
|
32
|
+
const uploaded = await client.upload(lease, ciphertext);
|
|
33
|
+
const head = await client.head(lease);
|
|
34
|
+
// What the provider should be holding. For a single-part object that is the
|
|
35
|
+
// whole-object SHA-256; for a multipart one it is S3's composite of the part
|
|
36
|
+
// digests, which `upload` returns. Comparing the stored checksum against
|
|
37
|
+
// `localChecksum` unconditionally would fail every multipart archive.
|
|
38
|
+
const expected = uploaded.remoteChecksum;
|
|
39
|
+
const problems = [];
|
|
40
|
+
if (!head.exists)
|
|
41
|
+
problems.push("the provider does not report the object as present");
|
|
42
|
+
if (head.bytes !== ciphertext.length)
|
|
43
|
+
problems.push(`the provider stored ${head.bytes} bytes; ${ciphertext.length} were sent`);
|
|
44
|
+
if (head.checksum === undefined) {
|
|
45
|
+
// Previously this case silently skipped the comparison, which left byte
|
|
46
|
+
// count as the only real check — an endpoint returning the right length of
|
|
47
|
+
// wrong bytes verified clean. Reclaim depends on this verdict, so an
|
|
48
|
+
// unprovable upload is a failed upload.
|
|
49
|
+
problems.push("the provider did not report a stored checksum, so the bytes it holds cannot be proven");
|
|
50
|
+
}
|
|
51
|
+
else if (head.checksum !== expected) {
|
|
52
|
+
problems.push(`the provider's stored checksum ${head.checksum} does not match the ${expected} we uploaded`);
|
|
53
|
+
}
|
|
54
|
+
if (problems.length > 0) {
|
|
55
|
+
await recordAudit(dataDir, "upload.verify", "denied", { archiveId, objectKey: lease.objectKey, problems: problems.length });
|
|
56
|
+
fail("ciphertext_integrity_failed", `Refusing to mark ${archiveId} durable: ${problems.join("; ")}`, { archiveId, objectKey: lease.objectKey });
|
|
57
|
+
}
|
|
58
|
+
const remote = { provider: lease.provider, bucket: remoteStorage.bucket, objectKey: lease.objectKey, bytes: ciphertext.length, checksum: localChecksum, verifiedAt: new Date(options.now ?? Date.now()).toISOString(), ...(options.targetId ? { targetId: options.targetId } : {}) };
|
|
59
|
+
const next = { ...record, remote };
|
|
60
|
+
await writeRecord(config.storage.root, next);
|
|
61
|
+
await recordAudit(dataDir, "upload.verify", "allowed", { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, provider: lease.provider });
|
|
62
|
+
return { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, checksum: localChecksum, verified: true, provider: lease.provider };
|
|
63
|
+
}
|
|
64
|
+
/** Uploads every archive that has no verified remote object yet. */
|
|
65
|
+
export async function uploadPending(dataDir, options = {}) {
|
|
66
|
+
const records = await listArchives(dataDir);
|
|
67
|
+
const uploaded = [];
|
|
68
|
+
const failed = [];
|
|
69
|
+
for (const record of records) {
|
|
70
|
+
if (isV2(record) && record.remote?.verifiedAt)
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
uploaded.push(await uploadArchive(dataDir, record.id, options));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
failed.push({ archiveId: record.id, error: error instanceof Error ? error.message : "upload failed" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { uploaded, failed, skipped: records.length - uploaded.length - failed.length };
|
|
80
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { type Recipient } from "../packages/vaultline-crypto/src/index.js";
|
|
2
|
+
import { type ArchiveRecord, type ArchiveRecordV2, type VaultConfig } from "./types.js";
|
|
3
|
+
import type { ProviderConfig } from "./control-plane.js";
|
|
4
|
+
/** Bumped when the adapter's preservation behaviour changes, recorded in every envelope. */
|
|
5
|
+
/**
|
|
6
|
+
* Writes one archive record atomically.
|
|
7
|
+
*
|
|
8
|
+
* Written to a temporary file beside the target and renamed into place, so a
|
|
9
|
+
* crash never leaves a half-written record that would strand an archive. This
|
|
10
|
+
* lived as three byte-identical private copies in upload, migrate and
|
|
11
|
+
* retention; a fourth was about to be added for the managed push path.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeRecord(root: string, record: ArchiveRecord): Promise<void>;
|
|
14
|
+
export declare const ADAPTER_VERSION = "1";
|
|
15
|
+
export declare const CONFIG_VERSION = 1;
|
|
16
|
+
/**
|
|
17
|
+
* ~/.sealkeep for new vaults; an existing ~/.Sealkeep keeps working forever —
|
|
18
|
+
* the rebrand must never strand a vault that predates it.
|
|
19
|
+
*/
|
|
20
|
+
export declare const defaultDataDir: () => string;
|
|
21
|
+
/**
|
|
22
|
+
* Reads and version-gates the vault config. A config written by a newer build is
|
|
23
|
+
* refused rather than silently reinterpreted; migrations attach here when v2 lands.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readConfig(dataDir: string): Promise<VaultConfig>;
|
|
26
|
+
export declare function vaultStatus(dataDir: string): Promise<{
|
|
27
|
+
vaultId: string;
|
|
28
|
+
provider: "local";
|
|
29
|
+
storageMode: "sealed" | "plain";
|
|
30
|
+
remoteStorage: {
|
|
31
|
+
provider: "vaultline" | "s3" | "r2" | "b2" | "gcs" | "gdrive";
|
|
32
|
+
bucket: string;
|
|
33
|
+
prefix: string;
|
|
34
|
+
region?: string;
|
|
35
|
+
} | undefined;
|
|
36
|
+
archiveCount: number;
|
|
37
|
+
archivedBytes: number;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function configureRemoteStorage(dataDir: string, remoteStorage: ProviderConfig): Promise<VaultConfig>;
|
|
40
|
+
export declare function initialize(dataDir: string, providedPhrase?: string, opts?: {
|
|
41
|
+
storageMode?: "sealed" | "plain";
|
|
42
|
+
}): Promise<{
|
|
43
|
+
config: VaultConfig;
|
|
44
|
+
phrase: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* The archive whose bytes are still a prefix of this source, and how many.
|
|
48
|
+
*
|
|
49
|
+
* Agent transcripts are append-only JSONL, which is the property that makes
|
|
50
|
+
* incremental archiving possible at all: if the first N bytes have not changed,
|
|
51
|
+
* everything before N is already sealed and only the tail is new. "Have not
|
|
52
|
+
* changed" is *proved* here, not assumed — the prefix is re-read and hashed
|
|
53
|
+
* against what the archive recorded, in constant memory — because a wrong
|
|
54
|
+
* answer would produce an archive that cannot reconstruct the file.
|
|
55
|
+
*
|
|
56
|
+
* The longest proven prefix wins, so a transcript archived at 1 MB and again at
|
|
57
|
+
* 3 MB reports the 3 MB archive. Nothing is returned when the file was
|
|
58
|
+
* rewritten, truncated, or has never been archived.
|
|
59
|
+
*/
|
|
60
|
+
export declare function provenAppendOffset(archives: readonly ArchiveRecord[], sourcePath: string, currentBytes: number): Promise<{
|
|
61
|
+
archiveId: string;
|
|
62
|
+
bytes: number;
|
|
63
|
+
} | null>;
|
|
64
|
+
/**
|
|
65
|
+
* The pointer a delta archive seals from: which archive already covers the
|
|
66
|
+
* transcript's first `baseBytes` bytes.
|
|
67
|
+
*
|
|
68
|
+
* Stored on the record as `delta`. Records are plain JSON, so readers that do
|
|
69
|
+
* not know the field carry it through unchanged; `deltaOf` is how it is read.
|
|
70
|
+
*/
|
|
71
|
+
export type ArchiveDelta = {
|
|
72
|
+
baseArchiveId: string;
|
|
73
|
+
baseBytes: number;
|
|
74
|
+
};
|
|
75
|
+
export type DeltaArchiveRecord = ArchiveRecordV2 & {
|
|
76
|
+
delta: ArchiveDelta;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The delta pointer on a record, when the record is a delta archive.
|
|
80
|
+
*
|
|
81
|
+
* Defensive on purpose: the field is not in the typed record (types.ts is the
|
|
82
|
+
* wire contract and moves separately), so this is the one place its shape is
|
|
83
|
+
* checked before anything trusts it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function deltaOf(record: ArchiveRecord): ArchiveDelta | null;
|
|
86
|
+
export type ArchiveResult = ArchiveRecord & {
|
|
87
|
+
deduplicated: boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Seals one transcript, reading and writing a chunk at a time.
|
|
91
|
+
*
|
|
92
|
+
* An existing archive is returned unchanged when the same source bytes are
|
|
93
|
+
* archived again, so a duplicate lifecycle event costs a hash rather than an
|
|
94
|
+
* archive.
|
|
95
|
+
*
|
|
96
|
+
* `hooks.delta` opts in to incremental sealing: when the newest archive of
|
|
97
|
+
* this path is a *proven* prefix of the live file, only the appended range is
|
|
98
|
+
* sealed and the record carries a `delta` pointer to its base. Off by default
|
|
99
|
+
* so every existing caller keeps writing complete, standalone archives; the
|
|
100
|
+
* queue worker turns it on, because re-sealing a growing session whole on
|
|
101
|
+
* every snapshot is exactly how 4 GB of transcript became 40 GB of storage.
|
|
102
|
+
*/
|
|
103
|
+
export declare function archiveFile(dataDir: string, sourcePath: string, rawPhrase: string, agent?: string, hooks?: {
|
|
104
|
+
onProgress?: (bytesRead: number) => void;
|
|
105
|
+
delta?: boolean;
|
|
106
|
+
}): Promise<ArchiveResult>;
|
|
107
|
+
/**
|
|
108
|
+
* The recovery phrase always gets a recipient, so a recovery kit alone can restore.
|
|
109
|
+
* Registered device and backup keys are added alongside it.
|
|
110
|
+
*/
|
|
111
|
+
export declare function configuredRecipients(config: VaultConfig, rawPhrase: string, group?: string): Recipient[];
|
|
112
|
+
export declare function listArchives(dataDir: string): Promise<ArchiveRecord[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Registers an X25519 public key that may open future archives. Only the public
|
|
115
|
+
* key is stored; the matching private key stays on its own device.
|
|
116
|
+
*/
|
|
117
|
+
export declare function addRecipient(dataDir: string, label: string, publicKeyBase64: string, group?: string): Promise<VaultConfig>;
|
|
118
|
+
/** Removes a recipient from future archives. Run `sealkeep rewrap` to revoke it on existing ones. */
|
|
119
|
+
export declare function removeRecipient(dataDir: string, id: string): Promise<VaultConfig>;
|
|
120
|
+
/** Persists a full config document after validation by the caller. */
|
|
121
|
+
export declare function writeConfig(dataDir: string, config: VaultConfig): Promise<void>;
|
|
122
|
+
export type RetentionCandidate = {
|
|
123
|
+
id: string;
|
|
124
|
+
agent: string;
|
|
125
|
+
path: string;
|
|
126
|
+
bytes: number;
|
|
127
|
+
createdAt: string;
|
|
128
|
+
eligible: false;
|
|
129
|
+
reason: string;
|
|
130
|
+
};
|
|
131
|
+
/** A non-mutating preview. Policy evaluation lives in `src/retention.ts`. */
|
|
132
|
+
export declare function previewRetention(dataDir: string, olderThanDays?: number): Promise<{
|
|
133
|
+
mode: "preview";
|
|
134
|
+
policy: import("./retention.js").RetentionPolicy;
|
|
135
|
+
olderThanDays: number;
|
|
136
|
+
graceDays: number;
|
|
137
|
+
sourceIdleDays: number | null;
|
|
138
|
+
minSourceBytes: number | null;
|
|
139
|
+
deleteAfterDays: number | null;
|
|
140
|
+
reclaimableBytes: number;
|
|
141
|
+
candidates: import("./retention.js").Candidate[];
|
|
142
|
+
}>;
|
|
143
|
+
/** One archive by id, or a named failure. Callers that need the record before the bytes start here. */
|
|
144
|
+
export declare function findArchive(dataDir: string, id: string): Promise<ArchiveRecord>;
|
|
145
|
+
/**
|
|
146
|
+
* Proves the phrase opens this archive, before the caller creates anything for it.
|
|
147
|
+
*
|
|
148
|
+
* Restoring streams straight to disk, so by the time a chunk fails there is
|
|
149
|
+
* already a partial file — and a partial file in a directory that had to be
|
|
150
|
+
* created is worse than no restore at all. Unwrapping the archive key needs
|
|
151
|
+
* only the envelope, costs one scrypt, and catches the failure people actually
|
|
152
|
+
* hit, so `restoreArchive` does it before it touches the filesystem.
|
|
153
|
+
*/
|
|
154
|
+
export declare function assertPhraseOpens(config: VaultConfig, record: ArchiveRecord, rawPhrase: string): void;
|
|
155
|
+
/**
|
|
156
|
+
* Restores one archive straight to a path, holding one chunk at a time.
|
|
157
|
+
*
|
|
158
|
+
* `destination` must be a path the caller is willing to see half-written: the
|
|
159
|
+
* whole-archive hashes are only proved once the last chunk has gone past, so a
|
|
160
|
+
* rejection can leave bytes behind. `restoreArchive` renames a temporary into
|
|
161
|
+
* place for exactly this reason.
|
|
162
|
+
*/
|
|
163
|
+
export declare function restoreRecordToFile(dataDir: string, record: ArchiveRecord, rawPhrase: string, destination: string,
|
|
164
|
+
/** Injectable so the offloaded-archive path can be proved without a bucket. */
|
|
165
|
+
options?: {
|
|
166
|
+
client?: import("./offload.js").FetchClient;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
bytes: number;
|
|
169
|
+
}>;
|
|
170
|
+
/**
|
|
171
|
+
* The records that reproduce a delta archive's transcript, oldest first:
|
|
172
|
+
* one complete base archive, then every delta in append order, ending with
|
|
173
|
+
* `target` itself. A record that is not a delta resolves to just itself.
|
|
174
|
+
*
|
|
175
|
+
* Pure and synchronous so callers that only want to *show* a chain — a
|
|
176
|
+
* dashboard, `sealkeep list` — can resolve it without touching ciphertext.
|
|
177
|
+
* Every link is checked against the next one's claim: a missing base or a
|
|
178
|
+
* base whose recorded length is not what the delta sealed from is refused
|
|
179
|
+
* here, before any restore starts writing.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveDeltaChain(archives: readonly ArchiveRecord[], target: ArchiveRecord): ArchiveRecord[];
|
|
182
|
+
/**
|
|
183
|
+
* Restores a delta archive by reassembling its chain: the base archive first,
|
|
184
|
+
* then each delta's appended range, concatenated in order into `destination`.
|
|
185
|
+
*
|
|
186
|
+
* Like `restoreRecordToFile`, the destination is written as the chain streams
|
|
187
|
+
* past, so callers restore through a temporary path and rename into place.
|
|
188
|
+
* Each link proves itself before its bytes land — stored-object hash before
|
|
189
|
+
* decryption, the envelope's own range hash inside `openArchiveToFile` — and
|
|
190
|
+
* the finished file is then proved *whole* against the newest record's
|
|
191
|
+
* full-transcript hash. A chain that cannot reproduce the exact bytes it
|
|
192
|
+
* promised is an error, never a nearly-right transcript.
|
|
193
|
+
*/
|
|
194
|
+
export declare function restoreDeltaChainToFile(dataDir: string, record: ArchiveRecord, rawPhrase: string, destination: string, options?: {
|
|
195
|
+
client?: import("./offload.js").FetchClient;
|
|
196
|
+
}): Promise<{
|
|
197
|
+
bytes: number;
|
|
198
|
+
chain: string[];
|
|
199
|
+
}>;
|
|
200
|
+
export declare function decryptRecord(dataDir: string, id: string, rawPhrase: string): Promise<{
|
|
201
|
+
record: ArchiveRecord;
|
|
202
|
+
plaintext: Buffer;
|
|
203
|
+
}>;
|
|
204
|
+
/** Kept for callers that name their own destination. Restore policy lives in `src/restore.ts`. */
|
|
205
|
+
export declare function recoverArchive(dataDir: string, id: string, destination: string, phrase: string): Promise<{
|
|
206
|
+
record: ArchiveRecord;
|
|
207
|
+
output: string;
|
|
208
|
+
}>;
|