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,55 @@
|
|
|
1
|
+
import { type ChunkStoreClient } from "./chunk-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Managed chunk folders, Paul's way: no chunk support in the control plane at
|
|
4
|
+
* all. The client already chunks — every sealed chunk is an ordinary file of
|
|
5
|
+
* known length — so each one is DELIVERED as an ordinary file, through the
|
|
6
|
+
* same upload lease, the same completion metering, the same download lease,
|
|
7
|
+
* and the same delete that a whole archive uses today. The plane never learns
|
|
8
|
+
* a new concept; it signs file PUTs, which is the one thing it always did.
|
|
9
|
+
*
|
|
10
|
+
* The mapping is mechanical. Chunk storage addresses objects as
|
|
11
|
+
* `<folder>/<name>`; for a managed vault the folder IS the archive id, and
|
|
12
|
+
* the object's cloud reference is `<archiveId>.<name>`:
|
|
13
|
+
*
|
|
14
|
+
* f0be…12af/chunk-000000 → vault_ref f0be…12af.chunk-000000
|
|
15
|
+
* f0be…12af/envelope.vlmeta → vault_ref f0be…12af.envelope.vlmeta
|
|
16
|
+
*
|
|
17
|
+
* Deterministic both ways, so any machine of the vault re-derives every
|
|
18
|
+
* reference from the archive id and the chunk count — no listing, no state.
|
|
19
|
+
* What the plane sees is the grouping it already had (one archive's rows sum
|
|
20
|
+
* to the same bytes one object used to), and never a name: project, date and
|
|
21
|
+
* session live only in the sealed sidecar, exactly as on an own bucket.
|
|
22
|
+
*
|
|
23
|
+
* Presence and size are proven against STORAGE (`statCiphertext`'s one-byte
|
|
24
|
+
* ranged read), not against the plane's rows — a lease that was minted but
|
|
25
|
+
* whose PUT never landed answers "absent", which is precisely what resume
|
|
26
|
+
* must see so it re-sends the same bytes under the same nonce (identical
|
|
27
|
+
* ciphertext; the invariant holds).
|
|
28
|
+
*/
|
|
29
|
+
export declare function managedRefForKey(key: string): string;
|
|
30
|
+
/** Every cloud reference an archive's chunk folder occupies, re-derived from the record. */
|
|
31
|
+
export declare function managedChunkRefs(archiveId: string, chunkCount: number): {
|
|
32
|
+
chunks: string[];
|
|
33
|
+
envelope: string;
|
|
34
|
+
};
|
|
35
|
+
export type CloudRowLike = {
|
|
36
|
+
vault_ref: string;
|
|
37
|
+
bytes?: number | string;
|
|
38
|
+
} & Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Folds chunk-family rows into one synthetic row per archive for listings:
|
|
41
|
+
* the family's reference is the archive id, its bytes are the chunk sum (the
|
|
42
|
+
* envelope sidecar is bookkeeping, not payload — same accounting as BYO).
|
|
43
|
+
* Rows that are not family members pass through untouched.
|
|
44
|
+
*/
|
|
45
|
+
export declare function collapseCloudRows<T extends CloudRowLike>(rows: T[]): (T & {
|
|
46
|
+
chunked?: true;
|
|
47
|
+
members?: string[];
|
|
48
|
+
})[];
|
|
49
|
+
/**
|
|
50
|
+
* The four chunk-store verbs, spoken over the managed account. One upload
|
|
51
|
+
* lease per object — request, PUT, complete — is the entire write path;
|
|
52
|
+
* `pullCiphertext` hash-checks every read against the digest recorded at
|
|
53
|
+
* upload before handing it back.
|
|
54
|
+
*/
|
|
55
|
+
export declare function managedChunkClient(dataDir: string): ChunkStoreClient;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { fail, isVaultlineError } from "./errors.js";
|
|
3
|
+
import { chunkObjectName, ENVELOPE_OBJECT } from "./chunk-store.js";
|
|
4
|
+
import { completeUpload, deleteCloudArchive, pullCiphertext, putCiphertext, requestUpload, statCiphertext } from "./cloud.js";
|
|
5
|
+
/**
|
|
6
|
+
* Managed chunk folders, Paul's way: no chunk support in the control plane at
|
|
7
|
+
* all. The client already chunks — every sealed chunk is an ordinary file of
|
|
8
|
+
* known length — so each one is DELIVERED as an ordinary file, through the
|
|
9
|
+
* same upload lease, the same completion metering, the same download lease,
|
|
10
|
+
* and the same delete that a whole archive uses today. The plane never learns
|
|
11
|
+
* a new concept; it signs file PUTs, which is the one thing it always did.
|
|
12
|
+
*
|
|
13
|
+
* The mapping is mechanical. Chunk storage addresses objects as
|
|
14
|
+
* `<folder>/<name>`; for a managed vault the folder IS the archive id, and
|
|
15
|
+
* the object's cloud reference is `<archiveId>.<name>`:
|
|
16
|
+
*
|
|
17
|
+
* f0be…12af/chunk-000000 → vault_ref f0be…12af.chunk-000000
|
|
18
|
+
* f0be…12af/envelope.vlmeta → vault_ref f0be…12af.envelope.vlmeta
|
|
19
|
+
*
|
|
20
|
+
* Deterministic both ways, so any machine of the vault re-derives every
|
|
21
|
+
* reference from the archive id and the chunk count — no listing, no state.
|
|
22
|
+
* What the plane sees is the grouping it already had (one archive's rows sum
|
|
23
|
+
* to the same bytes one object used to), and never a name: project, date and
|
|
24
|
+
* session live only in the sealed sidecar, exactly as on an own bucket.
|
|
25
|
+
*
|
|
26
|
+
* Presence and size are proven against STORAGE (`statCiphertext`'s one-byte
|
|
27
|
+
* ranged read), not against the plane's rows — a lease that was minted but
|
|
28
|
+
* whose PUT never landed answers "absent", which is precisely what resume
|
|
29
|
+
* must see so it re-sends the same bytes under the same nonce (identical
|
|
30
|
+
* ciphertext; the invariant holds).
|
|
31
|
+
*/
|
|
32
|
+
export function managedRefForKey(key) {
|
|
33
|
+
const slash = key.indexOf("/");
|
|
34
|
+
if (slash <= 0 || slash === key.length - 1 || key.indexOf("/", slash + 1) !== -1) {
|
|
35
|
+
return fail("invalid_argument", `Not a managed chunk key (expected <archiveId>/<object>): ${key}`);
|
|
36
|
+
}
|
|
37
|
+
return `${key.slice(0, slash)}.${key.slice(slash + 1)}`;
|
|
38
|
+
}
|
|
39
|
+
/** Every cloud reference an archive's chunk folder occupies, re-derived from the record. */
|
|
40
|
+
export function managedChunkRefs(archiveId, chunkCount) {
|
|
41
|
+
return {
|
|
42
|
+
chunks: Array.from({ length: chunkCount }, (_, index) => managedRefForKey(`${archiveId}/${chunkObjectName(index)}`)),
|
|
43
|
+
envelope: managedRefForKey(`${archiveId}/${ENVELOPE_OBJECT}`)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Recognises a cloud row that is one member of a chunk family, by its reference alone. */
|
|
47
|
+
const FAMILY_REF = /^(.+)\.(chunk-\d{6}|envelope\.vlmeta)$/;
|
|
48
|
+
/**
|
|
49
|
+
* Folds chunk-family rows into one synthetic row per archive for listings:
|
|
50
|
+
* the family's reference is the archive id, its bytes are the chunk sum (the
|
|
51
|
+
* envelope sidecar is bookkeeping, not payload — same accounting as BYO).
|
|
52
|
+
* Rows that are not family members pass through untouched.
|
|
53
|
+
*/
|
|
54
|
+
export function collapseCloudRows(rows) {
|
|
55
|
+
const families = new Map();
|
|
56
|
+
const plain = [];
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const match = FAMILY_REF.exec(row.vault_ref);
|
|
59
|
+
if (!match) {
|
|
60
|
+
plain.push(row);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const family = families.get(match[1]) ?? { rows: [], chunkBytes: 0, members: [] };
|
|
64
|
+
family.rows.push(row);
|
|
65
|
+
family.members.push(row.vault_ref);
|
|
66
|
+
if (match[2].startsWith("chunk-"))
|
|
67
|
+
family.chunkBytes += Number(row.bytes) || 0;
|
|
68
|
+
families.set(match[1], family);
|
|
69
|
+
}
|
|
70
|
+
const folded = [...families.entries()].map(([id, family]) => ({
|
|
71
|
+
...family.rows[0], vault_ref: id, bytes: family.chunkBytes, chunked: true, members: family.members
|
|
72
|
+
}));
|
|
73
|
+
return [...plain, ...folded];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The four chunk-store verbs, spoken over the managed account. One upload
|
|
77
|
+
* lease per object — request, PUT, complete — is the entire write path;
|
|
78
|
+
* `pullCiphertext` hash-checks every read against the digest recorded at
|
|
79
|
+
* upload before handing it back.
|
|
80
|
+
*/
|
|
81
|
+
export function managedChunkClient(dataDir) {
|
|
82
|
+
return {
|
|
83
|
+
async putObject(key, body) {
|
|
84
|
+
const ref = managedRefForKey(key);
|
|
85
|
+
const digest = createHash("sha256").update(body).digest("hex");
|
|
86
|
+
const lease = await requestUpload(dataDir, { vaultRef: ref, ciphertextSha256: digest, bytes: body.length });
|
|
87
|
+
await putCiphertext(lease, body);
|
|
88
|
+
await completeUpload(dataDir, lease.lease_id, body.length);
|
|
89
|
+
return { checksum: digest };
|
|
90
|
+
},
|
|
91
|
+
async headObject(key) {
|
|
92
|
+
return statCiphertext(dataDir, managedRefForKey(key));
|
|
93
|
+
},
|
|
94
|
+
async getObject(key) {
|
|
95
|
+
return (await pullCiphertext(dataDir, managedRefForKey(key))).ciphertext;
|
|
96
|
+
},
|
|
97
|
+
async deleteObject(key) {
|
|
98
|
+
try {
|
|
99
|
+
await deleteCloudArchive(dataDir, managedRefForKey(key));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isVaultlineError(error) && (error.code === "archive_not_found" || /returned 404|not[ _-]?found/i.test(error.message)))
|
|
103
|
+
return;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { type SetupAgentId } from "./adapters.js";
|
|
3
|
+
declare const run: typeof execFile.__promisify__;
|
|
4
|
+
/**
|
|
5
|
+
* Registering Sealkeep's MCP server with the agents on this machine.
|
|
6
|
+
*
|
|
7
|
+
* Archiving sessions and being able to *ask about* them are different products,
|
|
8
|
+
* and only the first one was wired. `src/mcp.ts` has always exposed
|
|
9
|
+
* `vaultline_search` and `vaultline_recover`, but nothing installed it and
|
|
10
|
+
* nothing told an agent it existed — a door with no sign on it. This is the
|
|
11
|
+
* sign.
|
|
12
|
+
*
|
|
13
|
+
* What is deliberately NOT here: any claim that an agent can reach archives it
|
|
14
|
+
* does not hold locally. The MCP server searches this machine's index. A
|
|
15
|
+
* cloud-wide memory needs server-side metadata search, which does not exist
|
|
16
|
+
* yet, and promising it here would be the kind of false claim this file was
|
|
17
|
+
* written to stop.
|
|
18
|
+
*/
|
|
19
|
+
export type McpInstallResult = {
|
|
20
|
+
agents: {
|
|
21
|
+
agent: SetupAgentId;
|
|
22
|
+
label: string;
|
|
23
|
+
installed: boolean;
|
|
24
|
+
detail: string;
|
|
25
|
+
fragmentPath?: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
/** What every agent needs to start the server, in one place so the copies cannot drift. */
|
|
29
|
+
export declare function mcpServerSpec(dataDir: string, executable?: string): {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
env: {
|
|
33
|
+
VAULTLINE_DATA_DIR: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether the command we are about to hand another program can actually be run.
|
|
38
|
+
*
|
|
39
|
+
* Registering `vaultline` when it is not on PATH — a checkout, an npx run, a
|
|
40
|
+
* shell whose PATH the agent does not inherit — writes a server the agent
|
|
41
|
+
* reports as "Failed to connect: ENOENT" forever, while this command claims
|
|
42
|
+
* success. Reporting a registration that cannot start is the same false claim
|
|
43
|
+
* as an archive that was never verified, and it is caught the same way: prove
|
|
44
|
+
* it before saying it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function executableResolves(executable?: string, exec?: typeof execFile.__promisify__): Promise<boolean>;
|
|
47
|
+
export declare function installMcpServer(dataDir: string, options?: {
|
|
48
|
+
only?: string;
|
|
49
|
+
exec?: typeof run;
|
|
50
|
+
home?: string;
|
|
51
|
+
}): Promise<McpInstallResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { detectSetupAgents } from "./adapters.js";
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
/** What every agent needs to start the server, in one place so the copies cannot drift. */
|
|
9
|
+
export function mcpServerSpec(dataDir, executable = "vaultline") {
|
|
10
|
+
return {
|
|
11
|
+
command: executable,
|
|
12
|
+
args: ["mcp"],
|
|
13
|
+
env: { VAULTLINE_DATA_DIR: dataDir }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether the command we are about to hand another program can actually be run.
|
|
18
|
+
*
|
|
19
|
+
* Registering `vaultline` when it is not on PATH — a checkout, an npx run, a
|
|
20
|
+
* shell whose PATH the agent does not inherit — writes a server the agent
|
|
21
|
+
* reports as "Failed to connect: ENOENT" forever, while this command claims
|
|
22
|
+
* success. Reporting a registration that cannot start is the same false claim
|
|
23
|
+
* as an archive that was never verified, and it is caught the same way: prove
|
|
24
|
+
* it before saying it.
|
|
25
|
+
*/
|
|
26
|
+
export async function executableResolves(executable = "vaultline", exec = run) {
|
|
27
|
+
try {
|
|
28
|
+
await exec(process.platform === "win32" ? "where" : "which", [executable]);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Claude Code owns its own config file, so it is asked rather than edited.
|
|
37
|
+
*
|
|
38
|
+
* Writing ~/.claude.json directly is what produced a registration the Spala
|
|
39
|
+
* installer then refused as foreign — the client normalises what it stores, so
|
|
40
|
+
* a hand-written entry never matches what the client would have written.
|
|
41
|
+
*/
|
|
42
|
+
async function installForClaude(dataDir, exec = run) {
|
|
43
|
+
const spec = mcpServerSpec(dataDir);
|
|
44
|
+
try {
|
|
45
|
+
await exec("claude", [
|
|
46
|
+
"mcp", "add", "--transport", "stdio", "--scope", "user", "vaultline",
|
|
47
|
+
"--env", `VAULTLINE_DATA_DIR=${dataDir}`,
|
|
48
|
+
"--", spec.command, ...spec.args
|
|
49
|
+
]);
|
|
50
|
+
return { installed: true, detail: "registered with Claude Code — ask it to search your sessions" };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : "could not run the claude CLI";
|
|
54
|
+
if (/already exists/i.test(message))
|
|
55
|
+
return { installed: true, detail: "already registered with Claude Code" };
|
|
56
|
+
return { installed: false, detail: `could not register: ${message.split("\n")[0]}` };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Codex configures itself in TOML. This still ships no TOML *parser* — but it
|
|
61
|
+
* does not need one to APPEND safely: a `[mcp_servers.vaultline]` table at the
|
|
62
|
+
* end of the file is a new top-level table, valid TOML regardless of what
|
|
63
|
+
* precedes it, as long as that section does not already exist. So the rule is:
|
|
64
|
+
* config file present and section absent → back it up, append, registered;
|
|
65
|
+
* anything else → the reviewable fragment, exactly as before. The backup makes
|
|
66
|
+
* the edit as reversible as never having made it.
|
|
67
|
+
*/
|
|
68
|
+
async function installForCodex(dataDir, home) {
|
|
69
|
+
const spec = mcpServerSpec(dataDir);
|
|
70
|
+
const fragmentPath = join(dataDir, "integrations", "codex-mcp.toml");
|
|
71
|
+
const table = [
|
|
72
|
+
"[mcp_servers.vaultline]",
|
|
73
|
+
`command = ${JSON.stringify(spec.command)}`,
|
|
74
|
+
`args = ${JSON.stringify(spec.args)}`,
|
|
75
|
+
"",
|
|
76
|
+
"[mcp_servers.vaultline.env]",
|
|
77
|
+
`VAULTLINE_DATA_DIR = ${JSON.stringify(dataDir)}`,
|
|
78
|
+
""
|
|
79
|
+
].join("\n");
|
|
80
|
+
await mkdir(dirname(fragmentPath), { recursive: true });
|
|
81
|
+
await writeFile(fragmentPath, `# Merge into ~/.codex/config.toml to let Codex search your archived sessions.\n${table}`, { mode: 0o600 });
|
|
82
|
+
const configPath = join(home ?? homedir(), ".codex", "config.toml");
|
|
83
|
+
const current = await readFile(configPath, "utf8").catch(() => null);
|
|
84
|
+
if (current === null) {
|
|
85
|
+
return { installed: false, detail: `Codex has no config.toml yet — merge this once it exists: ${fragmentPath}`, fragmentPath };
|
|
86
|
+
}
|
|
87
|
+
if (current.includes("[mcp_servers.vaultline]")) {
|
|
88
|
+
return { installed: true, detail: "already registered in ~/.codex/config.toml", fragmentPath };
|
|
89
|
+
}
|
|
90
|
+
const backupPath = `${configPath}.vaultline-backup-${Date.now()}`;
|
|
91
|
+
await writeFile(backupPath, current, { mode: 0o600 });
|
|
92
|
+
await writeFile(configPath, `${current}${current.endsWith("\n") ? "" : "\n"}\n${table}`, { mode: 0o600 });
|
|
93
|
+
return { installed: true, detail: `registered in ~/.codex/config.toml (backup kept beside it)`, fragmentPath, backupPath };
|
|
94
|
+
}
|
|
95
|
+
export async function installMcpServer(dataDir, options = {}) {
|
|
96
|
+
const detected = await detectSetupAgents(options.home);
|
|
97
|
+
const agents = [];
|
|
98
|
+
/**
|
|
99
|
+
* Checked once, and it gates *writing into another program's config* — not
|
|
100
|
+
* writing a fragment into our own data directory.
|
|
101
|
+
*
|
|
102
|
+
* A registration that cannot start is worse than none: the agent shows a
|
|
103
|
+
* permanent connection error and the person has no idea Sealkeep put it
|
|
104
|
+
* there. A fragment is inert until someone merges it deliberately, so
|
|
105
|
+
* withholding it would only remove information.
|
|
106
|
+
*/
|
|
107
|
+
const runnable = await executableResolves("vaultline", options.exec);
|
|
108
|
+
const notOnPath = "`vaultline` is not on PATH, so the server could not start. Install it globally (npm i -g vaultline) and run this again.";
|
|
109
|
+
for (const found of detected) {
|
|
110
|
+
if (!found.detected)
|
|
111
|
+
continue;
|
|
112
|
+
if (options.only && options.only !== found.agent)
|
|
113
|
+
continue;
|
|
114
|
+
if (found.agent === "claude") {
|
|
115
|
+
agents.push(runnable
|
|
116
|
+
? { agent: found.agent, label: found.label, ...(await installForClaude(dataDir, options.exec)) }
|
|
117
|
+
: { agent: found.agent, label: found.label, installed: false, detail: `not registered — ${notOnPath}` });
|
|
118
|
+
}
|
|
119
|
+
else if (found.agent === "codex") {
|
|
120
|
+
// A registration that cannot start is worse than none (same rule as
|
|
121
|
+
// Claude): only a runnable `vaultline` earns the config append; without
|
|
122
|
+
// it the fragment is written and the advice says why.
|
|
123
|
+
const fragment = runnable
|
|
124
|
+
? await installForCodex(dataDir, options.home)
|
|
125
|
+
: await installForCodex(dataDir, "/nonexistent-home-so-only-the-fragment-is-written");
|
|
126
|
+
agents.push({ ...fragment, agent: found.agent, label: found.label,
|
|
127
|
+
detail: runnable ? fragment.detail : `not registered — ${notOnPath}`, installed: runnable ? fragment.installed : false });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Detected, but Sealkeep has no adapter for its sessions, so an MCP
|
|
131
|
+
// server pointed at this vault would answer questions about history it
|
|
132
|
+
// never captured. Saying nothing is better than answering wrongly.
|
|
133
|
+
agents.push({
|
|
134
|
+
agent: found.agent, label: found.label, installed: false,
|
|
135
|
+
detail: "not registered — Sealkeep does not archive this agent's sessions yet, so it would have nothing to search"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { agents };
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/mcp.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { archiveFile, defaultDataDir, listArchives, previewRetention, vaultStatus } from "./vault.js";
|
|
5
|
+
import { restoreArchive } from "./restore.js";
|
|
6
|
+
import { providers } from "./control-plane.js";
|
|
7
|
+
const dataDir = process.env.VAULTLINE_DATA_DIR ?? defaultDataDir();
|
|
8
|
+
const phrase = process.env.VAULTLINE_RECOVERY_PHRASE;
|
|
9
|
+
const server = new McpServer({ name: "vaultline", version: "0.1.0" });
|
|
10
|
+
function text(value) { return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] }; }
|
|
11
|
+
function requirePhrase() { if (!phrase)
|
|
12
|
+
throw new Error("VAULTLINE_RECOVERY_PHRASE is required for archive and recovery actions"); return phrase; }
|
|
13
|
+
server.registerTool("vaultline_status", {
|
|
14
|
+
title: "Sealkeep status",
|
|
15
|
+
description: "Read local Sealkeep health, archive count, and encrypted storage usage. No session content is returned.",
|
|
16
|
+
inputSchema: {}
|
|
17
|
+
}, async () => text(await vaultStatus(dataDir)));
|
|
18
|
+
server.registerTool("vaultline_search", {
|
|
19
|
+
title: "Search archived sessions",
|
|
20
|
+
description: "Searches local encrypted archive metadata. It returns identifiers and source metadata, never decrypts transcript contents.",
|
|
21
|
+
inputSchema: { query: z.string().min(1).max(200), agent: z.string().optional() }
|
|
22
|
+
}, async ({ query, agent }) => {
|
|
23
|
+
const normalized = query.toLowerCase();
|
|
24
|
+
const matches = (await listArchives(dataDir)).filter((item) => (!agent || item.source.agent === agent) && `${item.id} ${item.source.path} ${item.source.agent}`.toLowerCase().includes(normalized));
|
|
25
|
+
return text(matches.map(({ id, createdAt, source }) => ({ id, createdAt, agent: source.agent, path: source.path, bytes: source.bytes })));
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("vaultline_prepare_archive", {
|
|
28
|
+
title: "Archive a session",
|
|
29
|
+
description: "Encrypts and queues a local transcript for archive. The transcript is encrypted before it reaches any storage provider.",
|
|
30
|
+
inputSchema: { path: z.string().min(1), agent: z.string().default("custom") },
|
|
31
|
+
annotations: { destructiveHint: false }
|
|
32
|
+
}, async ({ path, agent }) => text(await archiveFile(dataDir, path, requirePhrase(), agent)));
|
|
33
|
+
server.registerTool("vaultline_recover", {
|
|
34
|
+
title: "Recover an archived session",
|
|
35
|
+
description: "Decrypts one selected archive into a destination path on this device. This is a local action. It refuses to overwrite an existing file unless the caller explicitly chooses backup or replace.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
archiveId: z.string().uuid(),
|
|
38
|
+
destination: z.string().min(1).optional(),
|
|
39
|
+
native: z.boolean().default(false).describe("Restore to the adapter's original transcript path instead of a chosen destination"),
|
|
40
|
+
overwrite: z.enum(["refuse", "backup", "replace"]).default("refuse")
|
|
41
|
+
},
|
|
42
|
+
annotations: { destructiveHint: true }
|
|
43
|
+
}, async ({ archiveId, destination, native, overwrite }) => {
|
|
44
|
+
const outcome = await restoreArchive(dataDir, archiveId, requirePhrase(), { destination, native, overwrite });
|
|
45
|
+
return text({ archiveId: outcome.record.id, output: outcome.output, bytes: outcome.bytes, native: outcome.native, backupPath: outcome.backupPath });
|
|
46
|
+
});
|
|
47
|
+
server.registerTool("vaultline_create_upload", {
|
|
48
|
+
title: "Get upload lease status",
|
|
49
|
+
description: "Reports the storage lease mechanism used by Sealkeep. In the local MVP provider, no remote URL is necessary; the future hosted control plane returns a short-lived, object-scoped signed URL.",
|
|
50
|
+
inputSchema: { provider: z.enum(["vaultline", "s3", "r2", "gcs"]).default("vaultline"), bucket: z.string().min(1), prefix: z.string().min(1), ciphertextSha256: z.string().regex(/^[a-f0-9]{64}$/), bytes: z.number().int().positive() }
|
|
51
|
+
}, async ({ provider, bucket, prefix, ciphertextSha256, bytes }) => text(providers[provider].createUploadLease({ provider: provider, bucket, prefix }, { ciphertextSha256, bytes })));
|
|
52
|
+
server.registerTool("vaultline_retention_preview", {
|
|
53
|
+
title: "Preview local session reclamation",
|
|
54
|
+
description: "Lists old source transcripts that could eventually be reclaimed. This is read-only and always preserves sources until a verified remote archive exists.",
|
|
55
|
+
inputSchema: { olderThanDays: z.number().min(0).default(30) },
|
|
56
|
+
annotations: { readOnlyHint: true }
|
|
57
|
+
}, async ({ olderThanDays }) => text(await previewRetention(dataDir, olderThanDays)));
|
|
58
|
+
const transport = new StdioServerTransport();
|
|
59
|
+
server.connect(transport).catch((error) => { console.error(error); process.exit(1); });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type MigrationOutcome = {
|
|
2
|
+
id: string;
|
|
3
|
+
from: 1 | 2;
|
|
4
|
+
to: 2;
|
|
5
|
+
migrated: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Re-encrypts one v1 archive into the v2 envelope, keeping its archive id.
|
|
9
|
+
*
|
|
10
|
+
* The replacement object is written beside the original and decrypted back before
|
|
11
|
+
* anything is replaced, so a failure at any point leaves the readable v1 archive
|
|
12
|
+
* exactly as it was.
|
|
13
|
+
*/
|
|
14
|
+
export declare function migrateArchive(dataDir: string, id: string, rawPhrase: string): Promise<MigrationOutcome>;
|
|
15
|
+
export declare function migrateVault(dataDir: string, phrase: string): Promise<{
|
|
16
|
+
total: number;
|
|
17
|
+
migrated: number;
|
|
18
|
+
alreadyCurrent: number;
|
|
19
|
+
outcomes: MigrationOutcome[];
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Rewraps every v2 archive for the vault's current recipient set. This is how a
|
|
23
|
+
* removed device loses access: the archive key is re-wrapped without it, and no
|
|
24
|
+
* ciphertext is rewritten.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rewrapVault(dataDir: string, rawPhrase: string, options?: {
|
|
27
|
+
group?: string;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
rewrapped: number;
|
|
30
|
+
skipped: {
|
|
31
|
+
id: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}[];
|
|
34
|
+
recipients: number;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { decryptArchive as openEnvelope, encryptArchive as sealEnvelope, rewrapArchive } from "../packages/vaultline-crypto/src/index.js";
|
|
4
|
+
import { equalHex, matchesPhraseCheck, sha256 } from "./crypto.js";
|
|
5
|
+
import { canonicalPhrase } from "./mnemonic.js";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
import { recordAudit } from "./audit.js";
|
|
8
|
+
import { isV2 } from "./types.js";
|
|
9
|
+
import { ADAPTER_VERSION, configuredRecipients, decryptRecord, listArchives, readConfig, writeRecord } from "./vault.js";
|
|
10
|
+
/**
|
|
11
|
+
* Re-encrypts one v1 archive into the v2 envelope, keeping its archive id.
|
|
12
|
+
*
|
|
13
|
+
* The replacement object is written beside the original and decrypted back before
|
|
14
|
+
* anything is replaced, so a failure at any point leaves the readable v1 archive
|
|
15
|
+
* exactly as it was.
|
|
16
|
+
*/
|
|
17
|
+
export async function migrateArchive(dataDir, id, rawPhrase) {
|
|
18
|
+
const phrase = canonicalPhrase(rawPhrase);
|
|
19
|
+
const config = await readConfig(dataDir);
|
|
20
|
+
const records = await listArchives(dataDir);
|
|
21
|
+
const record = records.find((item) => item.id === id);
|
|
22
|
+
if (!record)
|
|
23
|
+
fail("archive_not_found", `Archive not found: ${id}`, { archiveId: id });
|
|
24
|
+
if (isV2(record))
|
|
25
|
+
return { id, from: 2, to: 2, migrated: false };
|
|
26
|
+
const { plaintext } = await decryptRecord(dataDir, id, phrase);
|
|
27
|
+
// Match what vault.ts writes, so a migrated archive is shaped like a fresh one.
|
|
28
|
+
const { envelope, ciphertext } = sealEnvelope(plaintext, { recipients: configuredRecipients(config, phrase), archiveId: id, createdAt: record.createdAt, compression: "gzip", adapter: { agent: record.source.agent, version: ADAPTER_VERSION } });
|
|
29
|
+
const staged = `${record.objectPath}.v2-${randomUUID()}`;
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(staged, ciphertext, { mode: 0o600 });
|
|
32
|
+
const verified = openEnvelope(envelope, await readFile(staged), { phrase });
|
|
33
|
+
if (!equalHex(sha256(verified), record.source.sha256))
|
|
34
|
+
fail("plaintext_integrity_failed", `Migrated archive ${id} did not reproduce the original bytes`, { archiveId: id });
|
|
35
|
+
const next = {
|
|
36
|
+
version: 2, id, createdAt: record.createdAt, source: record.source,
|
|
37
|
+
cipher: { algorithm: envelope.suite, ciphertextSha256: sha256(ciphertext), storedBytes: ciphertext.length, chunks: envelope.chunks.length },
|
|
38
|
+
envelope, objectPath: record.objectPath
|
|
39
|
+
};
|
|
40
|
+
await rename(staged, record.objectPath);
|
|
41
|
+
await writeRecord(config.storage.root, next);
|
|
42
|
+
await recordAudit(dataDir, "archive.migrate", "allowed", { archiveId: id, from: 1, to: 2 });
|
|
43
|
+
return { id, from: 1, to: 2, migrated: true };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await rm(staged, { force: true });
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function migrateVault(dataDir, phrase) {
|
|
51
|
+
const records = await listArchives(dataDir);
|
|
52
|
+
const outcomes = [];
|
|
53
|
+
for (const record of records)
|
|
54
|
+
outcomes.push(await migrateArchive(dataDir, record.id, phrase));
|
|
55
|
+
return { total: records.length, migrated: outcomes.filter((outcome) => outcome.migrated).length, alreadyCurrent: outcomes.filter((outcome) => !outcome.migrated).length, outcomes };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rewraps every v2 archive for the vault's current recipient set. This is how a
|
|
59
|
+
* removed device loses access: the archive key is re-wrapped without it, and no
|
|
60
|
+
* ciphertext is rewritten.
|
|
61
|
+
*/
|
|
62
|
+
export async function rewrapVault(dataDir, rawPhrase, options = {}) {
|
|
63
|
+
const phrase = canonicalPhrase(rawPhrase);
|
|
64
|
+
const config = await readConfig(dataDir);
|
|
65
|
+
// A plain vault has no envelopes to rewrap: its archives are readable files
|
|
66
|
+
// by the owner's choice at setup. Without this refusal the loop below threw
|
|
67
|
+
// an untyped error on the first record's missing envelope.
|
|
68
|
+
if (config.storageMode === "plain") {
|
|
69
|
+
fail("invalid_argument", "This vault stores archives unencrypted, so there are no keys to rewrap. Recipients only apply to sealed vaults.");
|
|
70
|
+
}
|
|
71
|
+
if (!matchesPhraseCheck(config.recovery.phraseCheck, phrase))
|
|
72
|
+
fail("recovery_phrase_mismatch", "Recovery phrase does not match this vault");
|
|
73
|
+
const recipients = configuredRecipients(config, phrase, options.group);
|
|
74
|
+
const records = await listArchives(dataDir);
|
|
75
|
+
const rewrapped = [];
|
|
76
|
+
const skipped = [];
|
|
77
|
+
for (const record of records) {
|
|
78
|
+
if (!isV2(record)) {
|
|
79
|
+
skipped.push({ id: record.id, reason: "v1 archive; run `sealkeep migrate` first" });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const envelope = rewrapArchive(record.envelope, { phrase }, recipients);
|
|
83
|
+
await writeRecord(config.storage.root, { ...record, envelope });
|
|
84
|
+
rewrapped.push(record.id);
|
|
85
|
+
}
|
|
86
|
+
await recordAudit(dataDir, "archive.rewrap", "allowed", { rewrapped: rewrapped.length, skipped: skipped.length, recipients: recipients.length, group: options.group ?? null });
|
|
87
|
+
return { rewrapped: rewrapped.length, skipped, recipients: recipients.length };
|
|
88
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcribable recovery phrases, in the format the rest of the world already
|
|
3
|
+
* uses.
|
|
4
|
+
*
|
|
5
|
+
* A recovery phrase is written on paper by a person under stress, months before
|
|
6
|
+
* it matters, and typed back on a different machine. Base64 fails all three
|
|
7
|
+
* tests. Words do not: they survive bad handwriting, they can be read aloud,
|
|
8
|
+
* and a checksum catches the transcription mistakes people actually make.
|
|
9
|
+
*
|
|
10
|
+
* This used to be a bespoke 256-word list, which worked but left the phrase
|
|
11
|
+
* marooned in one program. BIP-39 is the same idea with an installed base:
|
|
12
|
+
* hardware wallets, steel backup plates, password managers and phone apps all
|
|
13
|
+
* know how to hold these words, and people who have done this before recognise
|
|
14
|
+
* them on sight. Twenty-four words carry 256 bits of entropy; twelve carry 128
|
|
15
|
+
* and are accepted for people who want a shorter thing to write down.
|
|
16
|
+
*
|
|
17
|
+
* What this does *not* mean is that a wallet can open an archive. A wallet is
|
|
18
|
+
* somewhere to keep the words, not a second key: Sealkeep still derives its
|
|
19
|
+
* key from the phrase with scrypt and a per-vault salt, which is deliberately
|
|
20
|
+
* slower and better salted than BIP-39's own seed derivation. Anyone claiming
|
|
21
|
+
* otherwise is selling something.
|
|
22
|
+
*
|
|
23
|
+
* The list is frozen by the specification and verified by test rather than
|
|
24
|
+
* trusted, because one altered word would produce phrases no other tool reads.
|
|
25
|
+
*/
|
|
26
|
+
export declare const WORDS: readonly string[];
|
|
27
|
+
/** 256 bits, so twenty-four words. Twelve-word phrases are read but not issued. */
|
|
28
|
+
export declare const ENTROPY_BYTES = 32;
|
|
29
|
+
export declare const PHRASE_WORDS = 24;
|
|
30
|
+
/** Normalizes what a person actually types: stray case, punctuation, and runs of spaces. */
|
|
31
|
+
export declare function normalize(phrase: string): string[];
|
|
32
|
+
export declare function entropyToPhrase(entropy: Buffer): string;
|
|
33
|
+
export declare function generateRecoveryPhrase(): string;
|
|
34
|
+
/** Words within one edit of the given word, so a typo gets a suggestion rather than a shrug. */
|
|
35
|
+
export declare function suggestions(word: string, limit?: number): string[];
|
|
36
|
+
export type PhraseCheck = {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
words: string[];
|
|
39
|
+
problems: string[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Explains exactly what is wrong with a phrase, because "invalid" is useless to
|
|
43
|
+
* someone holding a piece of paper. Unknown words get spelling suggestions; a
|
|
44
|
+
* checksum failure means a word is in the wrong place or was misread.
|
|
45
|
+
*/
|
|
46
|
+
export declare function inspectPhrase(phrase: string): PhraseCheck;
|
|
47
|
+
/** True when a phrase is one this build generated. Older free-text phrases return false. */
|
|
48
|
+
export declare function isMnemonic(phrase: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The form a phrase takes before any key is derived from it.
|
|
51
|
+
*
|
|
52
|
+
* People read words off paper and type them back with different capitalisation and
|
|
53
|
+
* spacing. Deriving a key from the raw string would reject a correct phrase for
|
|
54
|
+
* cosmetic reasons — the worst possible failure for a recovery flow. A recognised
|
|
55
|
+
* word phrase is therefore reduced to canonical lowercase, single-spaced form.
|
|
56
|
+
*
|
|
57
|
+
* Anything that is not a valid word phrase passes through untouched, because vaults
|
|
58
|
+
* created before the word list hold arbitrary text whose exact bytes are the secret.
|
|
59
|
+
*/
|
|
60
|
+
export declare function canonicalPhrase(phrase: string): string;
|