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,86 @@
|
|
|
1
|
+
import type { GdriveCredentials } from "./providers/gdrive.js";
|
|
2
|
+
export type SecretRef = {
|
|
3
|
+
service: string;
|
|
4
|
+
account: string;
|
|
5
|
+
};
|
|
6
|
+
export type BackendName = "macos-keychain" | "secret-service" | "windows-dpapi" | "file";
|
|
7
|
+
export interface SecretBackend {
|
|
8
|
+
readonly name: BackendName;
|
|
9
|
+
/** True when this backend can actually be used on this machine right now. */
|
|
10
|
+
available(): Promise<boolean>;
|
|
11
|
+
get(ref: SecretRef): Promise<string | null>;
|
|
12
|
+
set(ref: SecretRef, value: string): Promise<void>;
|
|
13
|
+
delete(ref: SecretRef): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Owner-only file storage. This is the documented fallback when no OS keystore is
|
|
17
|
+
* present; it refuses to read a secret whose file is readable by group or others.
|
|
18
|
+
*/
|
|
19
|
+
export declare class FileSecretBackend implements SecretBackend {
|
|
20
|
+
private readonly root;
|
|
21
|
+
readonly name: "file";
|
|
22
|
+
constructor(root: string);
|
|
23
|
+
private path;
|
|
24
|
+
available(): Promise<boolean>;
|
|
25
|
+
get(ref: SecretRef): Promise<string | null>;
|
|
26
|
+
set(ref: SecretRef, value: string): Promise<void>;
|
|
27
|
+
delete(ref: SecretRef): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function secretBackends(dataDir: string): SecretBackend[];
|
|
30
|
+
/**
|
|
31
|
+
* Picks the strongest backend this machine supports. The file fallback always
|
|
32
|
+
* qualifies. `VAULTLINE_SECRET_BACKEND` pins the choice, which is how a test suite
|
|
33
|
+
* or a sandboxed environment stays out of the real OS keystore.
|
|
34
|
+
*/
|
|
35
|
+
export declare function chooseBackend(dataDir: string, preferred?: BackendName, env?: NodeJS.ProcessEnv): Promise<SecretBackend>;
|
|
36
|
+
export type ProviderCredentials = {
|
|
37
|
+
accessKeyId: string;
|
|
38
|
+
secretAccessKey: string;
|
|
39
|
+
sessionToken?: string;
|
|
40
|
+
} | {
|
|
41
|
+
clientEmail: string;
|
|
42
|
+
privateKey: string;
|
|
43
|
+
} | GdriveCredentials;
|
|
44
|
+
/**
|
|
45
|
+
* Remembering the recovery phrase.
|
|
46
|
+
*
|
|
47
|
+
* This is opt-in and it is a real trade-off, so it is stated plainly rather than
|
|
48
|
+
* buried: a phrase in the OS keystore is readable by anything running as this user
|
|
49
|
+
* once the account is unlocked. That is the same bargain a password manager or a
|
|
50
|
+
* synced-folder client makes, and it is what lets the background service seal
|
|
51
|
+
* sessions without a human pasting a secret on every boot.
|
|
52
|
+
*
|
|
53
|
+
* The written-down phrase remains the disaster-recovery path. This copy is a
|
|
54
|
+
* convenience for one machine and is never uploaded, synced, or included in a backup
|
|
55
|
+
* of the vault directory.
|
|
56
|
+
*/
|
|
57
|
+
export declare function rememberRecoveryPhrase(dataDir: string, vaultId: string, phrase: string, preferred?: BackendName): Promise<{
|
|
58
|
+
backend: BackendName;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function recallRecoveryPhrase(dataDir: string, vaultId: string, preferred?: BackendName): Promise<string | null>;
|
|
61
|
+
export declare function forgetRecoveryPhrase(dataDir: string, vaultId: string, preferred?: BackendName): Promise<{
|
|
62
|
+
backend: BackendName;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Where every command looks for the phrase, in order: an explicit flag, the
|
|
66
|
+
* environment, then this machine's keystore. Returns null rather than throwing so
|
|
67
|
+
* callers can give an instruction instead of a stack trace.
|
|
68
|
+
*
|
|
69
|
+
* The vault password (password-lock.ts) is deliberately not a source here. A
|
|
70
|
+
* password is exchanged for the phrase at an explicit moment — a prompt, or the
|
|
71
|
+
* unlock endpoint — never resolved ambiently, because an ambient password would
|
|
72
|
+
* quietly become a second phrase: one more thing lying around that opens
|
|
73
|
+
* everything, without the keystore's opt-in or its stated trade-off.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveRecoveryPhrase(dataDir: string, vaultId: string, explicit?: string, env?: NodeJS.ProcessEnv, preferred?: BackendName): Promise<string | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Provider credentials are addressed by storage config id, never by bucket name,
|
|
78
|
+
* so rotating a config cannot silently reuse an old credential.
|
|
79
|
+
*/
|
|
80
|
+
export declare function storeProviderCredentials(dataDir: string, storageConfigId: string, credentials: ProviderCredentials, preferred?: BackendName): Promise<{
|
|
81
|
+
backend: BackendName;
|
|
82
|
+
}>;
|
|
83
|
+
export declare function loadProviderCredentials(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<ProviderCredentials>;
|
|
84
|
+
export declare function deleteProviderCredentials(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<{
|
|
85
|
+
backend: BackendName;
|
|
86
|
+
}>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
async function commandExists(command) {
|
|
9
|
+
try {
|
|
10
|
+
await run(platform() === "win32" ? "where" : "which", [command]);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* macOS Keychain.
|
|
19
|
+
*
|
|
20
|
+
* Note: `security add-generic-password` takes the secret as an argument, so it is
|
|
21
|
+
* briefly visible in this user's own process list. Anyone who can see it can also
|
|
22
|
+
* read the keychain as that user, so this does not widen the trust boundary — but it
|
|
23
|
+
* is why writes go through a short-lived process and are never shell-interpolated.
|
|
24
|
+
*/
|
|
25
|
+
class MacosKeychain {
|
|
26
|
+
name = "macos-keychain";
|
|
27
|
+
async available() { return platform() === "darwin" && (await commandExists("security")); }
|
|
28
|
+
async get(ref) {
|
|
29
|
+
try {
|
|
30
|
+
return (await run("security", ["find-generic-password", "-s", ref.service, "-a", ref.account, "-w"])).stdout.trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async set(ref, value) {
|
|
37
|
+
await run("security", ["add-generic-password", "-U", "-s", ref.service, "-a", ref.account, "-w", value]);
|
|
38
|
+
}
|
|
39
|
+
async delete(ref) {
|
|
40
|
+
try {
|
|
41
|
+
await run("security", ["delete-generic-password", "-s", ref.service, "-a", ref.account]);
|
|
42
|
+
}
|
|
43
|
+
catch { /* already absent */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Freedesktop Secret Service via `secret-tool`, which reads the secret from stdin. */
|
|
47
|
+
class SecretService {
|
|
48
|
+
name = "secret-service";
|
|
49
|
+
async available() { return platform() === "linux" && (await commandExists("secret-tool")); }
|
|
50
|
+
async get(ref) {
|
|
51
|
+
try {
|
|
52
|
+
return (await run("secret-tool", ["lookup", "service", ref.service, "account", ref.account])).stdout.trim() || null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async set(ref, value) {
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
const child = execFile("secret-tool", ["store", "--label", `${ref.service}:${ref.account}`, "service", ref.service, "account", ref.account], (error) => error ? reject(error) : resolve());
|
|
61
|
+
child.stdin?.end(value);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async delete(ref) {
|
|
65
|
+
try {
|
|
66
|
+
await run("secret-tool", ["clear", "service", ref.service, "account", ref.account]);
|
|
67
|
+
}
|
|
68
|
+
catch { /* already absent */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Windows DPAPI through PowerShell. The ciphertext is bound to the current user
|
|
73
|
+
* account, so the stored file is useless to any other user on the machine.
|
|
74
|
+
*/
|
|
75
|
+
class WindowsDpapi {
|
|
76
|
+
root;
|
|
77
|
+
name = "windows-dpapi";
|
|
78
|
+
constructor(root) {
|
|
79
|
+
this.root = root;
|
|
80
|
+
}
|
|
81
|
+
path(ref) { return join(this.root, `${encodeURIComponent(ref.service)}.${encodeURIComponent(ref.account)}.dpapi`); }
|
|
82
|
+
async available() { return platform() === "win32" && (await commandExists("powershell")); }
|
|
83
|
+
async get(ref) {
|
|
84
|
+
const stored = await readFile(this.path(ref), "utf8").catch(() => null);
|
|
85
|
+
if (!stored)
|
|
86
|
+
return null;
|
|
87
|
+
const script = `$secure = ConvertTo-SecureString -String $env:VL_BLOB; [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))`;
|
|
88
|
+
const { stdout } = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { env: { ...process.env, VL_BLOB: stored.trim() } });
|
|
89
|
+
return stdout.trim();
|
|
90
|
+
}
|
|
91
|
+
async set(ref, value) {
|
|
92
|
+
const script = `ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $env:VL_SECRET -AsPlainText -Force)`;
|
|
93
|
+
const { stdout } = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { env: { ...process.env, VL_SECRET: value } });
|
|
94
|
+
await mkdir(this.root, { recursive: true });
|
|
95
|
+
await writeFile(this.path(ref), stdout.trim(), { mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
async delete(ref) { await rm(this.path(ref), { force: true }); }
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Owner-only file storage. This is the documented fallback when no OS keystore is
|
|
101
|
+
* present; it refuses to read a secret whose file is readable by group or others.
|
|
102
|
+
*/
|
|
103
|
+
export class FileSecretBackend {
|
|
104
|
+
root;
|
|
105
|
+
name = "file";
|
|
106
|
+
constructor(root) {
|
|
107
|
+
this.root = root;
|
|
108
|
+
}
|
|
109
|
+
path(ref) { return join(this.root, `${encodeURIComponent(ref.service)}.${encodeURIComponent(ref.account)}.secret`); }
|
|
110
|
+
async available() { return true; }
|
|
111
|
+
async get(ref) {
|
|
112
|
+
const path = this.path(ref);
|
|
113
|
+
const info = await stat(path).catch(() => null);
|
|
114
|
+
if (!info)
|
|
115
|
+
return null;
|
|
116
|
+
if (platform() !== "win32" && (info.mode & 0o077) !== 0) {
|
|
117
|
+
fail("forbidden", `Refusing to read ${path}: mode ${(info.mode & 0o777).toString(8)} allows other users to read it. Run: chmod 600 ${path}`, { path });
|
|
118
|
+
}
|
|
119
|
+
return (await readFile(path, "utf8")).trim() || null;
|
|
120
|
+
}
|
|
121
|
+
async set(ref, value) {
|
|
122
|
+
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
123
|
+
await writeFile(this.path(ref), `${value}\n`, { mode: 0o600 });
|
|
124
|
+
await chmod(this.path(ref), 0o600);
|
|
125
|
+
}
|
|
126
|
+
async delete(ref) { await rm(this.path(ref), { force: true }); }
|
|
127
|
+
}
|
|
128
|
+
export function secretBackends(dataDir) {
|
|
129
|
+
return [new MacosKeychain(), new SecretService(), new WindowsDpapi(join(dataDir, "secrets")), new FileSecretBackend(join(dataDir, "secrets"))];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Picks the strongest backend this machine supports. The file fallback always
|
|
133
|
+
* qualifies. `VAULTLINE_SECRET_BACKEND` pins the choice, which is how a test suite
|
|
134
|
+
* or a sandboxed environment stays out of the real OS keystore.
|
|
135
|
+
*/
|
|
136
|
+
export async function chooseBackend(dataDir, preferred, env = process.env) {
|
|
137
|
+
preferred = preferred ?? env.VAULTLINE_SECRET_BACKEND;
|
|
138
|
+
const backends = secretBackends(dataDir);
|
|
139
|
+
if (preferred) {
|
|
140
|
+
const chosen = backends.find((backend) => backend.name === preferred);
|
|
141
|
+
if (!chosen)
|
|
142
|
+
fail("invalid_argument", `Unknown secret backend: ${preferred}`);
|
|
143
|
+
if (!(await chosen.available()))
|
|
144
|
+
fail("invalid_argument", `Secret backend ${preferred} is not available on this machine`);
|
|
145
|
+
return chosen;
|
|
146
|
+
}
|
|
147
|
+
for (const backend of backends)
|
|
148
|
+
if (await backend.available())
|
|
149
|
+
return backend;
|
|
150
|
+
return new FileSecretBackend(join(dataDir, "secrets"));
|
|
151
|
+
}
|
|
152
|
+
const SERVICE = "vaultline-provider";
|
|
153
|
+
const PHRASE_SERVICE = "vaultline-recovery";
|
|
154
|
+
/**
|
|
155
|
+
* Remembering the recovery phrase.
|
|
156
|
+
*
|
|
157
|
+
* This is opt-in and it is a real trade-off, so it is stated plainly rather than
|
|
158
|
+
* buried: a phrase in the OS keystore is readable by anything running as this user
|
|
159
|
+
* once the account is unlocked. That is the same bargain a password manager or a
|
|
160
|
+
* synced-folder client makes, and it is what lets the background service seal
|
|
161
|
+
* sessions without a human pasting a secret on every boot.
|
|
162
|
+
*
|
|
163
|
+
* The written-down phrase remains the disaster-recovery path. This copy is a
|
|
164
|
+
* convenience for one machine and is never uploaded, synced, or included in a backup
|
|
165
|
+
* of the vault directory.
|
|
166
|
+
*/
|
|
167
|
+
export async function rememberRecoveryPhrase(dataDir, vaultId, phrase, preferred) {
|
|
168
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
169
|
+
await backend.set({ service: PHRASE_SERVICE, account: vaultId }, phrase);
|
|
170
|
+
return { backend: backend.name };
|
|
171
|
+
}
|
|
172
|
+
export async function recallRecoveryPhrase(dataDir, vaultId, preferred) {
|
|
173
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
174
|
+
return backend.get({ service: PHRASE_SERVICE, account: vaultId });
|
|
175
|
+
}
|
|
176
|
+
export async function forgetRecoveryPhrase(dataDir, vaultId, preferred) {
|
|
177
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
178
|
+
await backend.delete({ service: PHRASE_SERVICE, account: vaultId });
|
|
179
|
+
return { backend: backend.name };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Where every command looks for the phrase, in order: an explicit flag, the
|
|
183
|
+
* environment, then this machine's keystore. Returns null rather than throwing so
|
|
184
|
+
* callers can give an instruction instead of a stack trace.
|
|
185
|
+
*
|
|
186
|
+
* The vault password (password-lock.ts) is deliberately not a source here. A
|
|
187
|
+
* password is exchanged for the phrase at an explicit moment — a prompt, or the
|
|
188
|
+
* unlock endpoint — never resolved ambiently, because an ambient password would
|
|
189
|
+
* quietly become a second phrase: one more thing lying around that opens
|
|
190
|
+
* everything, without the keystore's opt-in or its stated trade-off.
|
|
191
|
+
*/
|
|
192
|
+
export async function resolveRecoveryPhrase(dataDir, vaultId, explicit, env = process.env, preferred) {
|
|
193
|
+
return explicit ?? env.VAULTLINE_RECOVERY_PHRASE ?? (await recallRecoveryPhrase(dataDir, vaultId, preferred).catch(() => null));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Provider credentials are addressed by storage config id, never by bucket name,
|
|
197
|
+
* so rotating a config cannot silently reuse an old credential.
|
|
198
|
+
*/
|
|
199
|
+
export async function storeProviderCredentials(dataDir, storageConfigId, credentials, preferred) {
|
|
200
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
201
|
+
await backend.set({ service: SERVICE, account: storageConfigId }, JSON.stringify(credentials));
|
|
202
|
+
return { backend: backend.name };
|
|
203
|
+
}
|
|
204
|
+
export async function loadProviderCredentials(dataDir, storageConfigId, preferred) {
|
|
205
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
206
|
+
const raw = await backend.get({ service: SERVICE, account: storageConfigId });
|
|
207
|
+
if (!raw)
|
|
208
|
+
fail("signer_not_configured", `No stored credentials for storage config ${storageConfigId}. Run: sealkeep storage credentials set`, { storageConfigId, backend: backend.name });
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(raw);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return fail("invalid_argument", `Stored credentials for ${storageConfigId} are not readable JSON`, { storageConfigId });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function deleteProviderCredentials(dataDir, storageConfigId, preferred) {
|
|
217
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
218
|
+
await backend.delete({ service: SERVICE, account: storageConfigId });
|
|
219
|
+
return { backend: backend.name };
|
|
220
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type ServiceKind = "launchd" | "systemd" | "schtasks" | "unsupported";
|
|
2
|
+
export type ServiceOptions = {
|
|
3
|
+
dataDir: string;
|
|
4
|
+
/** The command the service runs. Defaults to this install's `sealkeep daemon`. */
|
|
5
|
+
executable?: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
home?: string;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
/**
|
|
10
|
+
* Environment the service runs with.
|
|
11
|
+
*
|
|
12
|
+
* A service manager starts the daemon with a bare environment — it does not
|
|
13
|
+
* inherit the shell that installed it. The upload pass is gated on
|
|
14
|
+
* `VAULTLINE_ENABLE_SIGNER`, so a service definition that carries no
|
|
15
|
+
* environment produces a daemon that seals sessions, uploads nothing, and
|
|
16
|
+
* therefore reclaims nothing, logging one line to `daemon.log` that nobody
|
|
17
|
+
* reads. Whatever the installer decides uploads need, it has to be written
|
|
18
|
+
* into the unit.
|
|
19
|
+
*/
|
|
20
|
+
environment?: Record<string, string>;
|
|
21
|
+
/** Injected in tests so no real service manager is touched. */
|
|
22
|
+
exec?: (file: string, args: string[]) => Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
export type ServicePlan = {
|
|
25
|
+
kind: ServiceKind;
|
|
26
|
+
label: string;
|
|
27
|
+
path: string;
|
|
28
|
+
contents: string;
|
|
29
|
+
enable: [string, string[]][];
|
|
30
|
+
disable: [string, string[]][];
|
|
31
|
+
};
|
|
32
|
+
export declare const SERVICE_LABEL = "ai.vaultline.agent";
|
|
33
|
+
/**
|
|
34
|
+
* Describes the service this machine would install, without touching anything.
|
|
35
|
+
*
|
|
36
|
+
* Keeping the plan separate from the act means `autopilot --dry-run` can show the
|
|
37
|
+
* exact file and commands, and tests can assert them without a service manager.
|
|
38
|
+
*/
|
|
39
|
+
export declare function servicePlan(options: ServiceOptions): ServicePlan;
|
|
40
|
+
export type ServiceOutcome = {
|
|
41
|
+
kind: ServiceKind;
|
|
42
|
+
installed: boolean;
|
|
43
|
+
path: string;
|
|
44
|
+
ranCommands: string[];
|
|
45
|
+
note?: string;
|
|
46
|
+
};
|
|
47
|
+
/** Writes the unit file and asks the platform's service manager to start it at login. */
|
|
48
|
+
export declare function installService(options: ServiceOptions): Promise<ServiceOutcome>;
|
|
49
|
+
export declare function uninstallService(options: ServiceOptions): Promise<ServiceOutcome>;
|
|
50
|
+
/** Reports whether the unit file this build would write is currently present. */
|
|
51
|
+
export declare function serviceStatus(options: ServiceOptions): Promise<{
|
|
52
|
+
kind: ServiceKind;
|
|
53
|
+
present: boolean;
|
|
54
|
+
path: string;
|
|
55
|
+
runs?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* The environment a service unit needs beyond its flags. A service manager
|
|
59
|
+
* starts the daemon bare, so anything the installing session was told about
|
|
60
|
+
* where secrets live has to ride along in the unit — a vault set up with
|
|
61
|
+
* VAULTLINE_SECRET_BACKEND=file kept its phrase in a keystore the daemon then
|
|
62
|
+
* never looked in, and every tick died on "No recovery phrase available".
|
|
63
|
+
*/
|
|
64
|
+
export declare function serviceUnitEnvironment(base?: Record<string, string>, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
65
|
+
/** The argv the service should run: this package's CLI, with the daemon subcommand. */
|
|
66
|
+
export declare function daemonInvocation(dataDir: string, options?: {
|
|
67
|
+
reclaim?: boolean;
|
|
68
|
+
cliPath?: string;
|
|
69
|
+
}): {
|
|
70
|
+
executable: string;
|
|
71
|
+
args: string[];
|
|
72
|
+
};
|
|
73
|
+
export declare function serviceKind(target?: NodeJS.Platform): ServiceKind;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fail } from "./errors.js";
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
export const SERVICE_LABEL = "ai.vaultline.agent";
|
|
10
|
+
function kindFor(target) {
|
|
11
|
+
if (target === "darwin")
|
|
12
|
+
return "launchd";
|
|
13
|
+
if (target === "linux")
|
|
14
|
+
return "systemd";
|
|
15
|
+
if (target === "win32")
|
|
16
|
+
return "schtasks";
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
function escapeXml(value) {
|
|
20
|
+
return value.replace(/[<>&'"]/g, (char) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[char]);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Describes the service this machine would install, without touching anything.
|
|
24
|
+
*
|
|
25
|
+
* Keeping the plan separate from the act means `autopilot --dry-run` can show the
|
|
26
|
+
* exact file and commands, and tests can assert them without a service manager.
|
|
27
|
+
*/
|
|
28
|
+
export function servicePlan(options) {
|
|
29
|
+
const target = options.platform ?? platform();
|
|
30
|
+
const kind = kindFor(target);
|
|
31
|
+
const home = options.home ?? homedir();
|
|
32
|
+
const executable = options.executable ?? process.execPath;
|
|
33
|
+
const args = options.args ?? [];
|
|
34
|
+
const logs = join(options.dataDir, "logs");
|
|
35
|
+
const environment = options.environment ?? {};
|
|
36
|
+
const environmentEntries = Object.entries(environment);
|
|
37
|
+
if (kind === "launchd") {
|
|
38
|
+
const path = join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
39
|
+
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
|
+
<plist version="1.0">
|
|
42
|
+
<dict>
|
|
43
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
44
|
+
<key>ProgramArguments</key>
|
|
45
|
+
<array>
|
|
46
|
+
${[executable, ...args].map((value) => ` <string>${escapeXml(value)}</string>`).join("\n")}
|
|
47
|
+
</array>
|
|
48
|
+
${environmentEntries.length === 0 ? "" : ` <key>EnvironmentVariables</key>
|
|
49
|
+
<dict>
|
|
50
|
+
${environmentEntries.map(([key, value]) => ` <key>${escapeXml(key)}</key><string>${escapeXml(value)}</string>`).join("\n")}
|
|
51
|
+
</dict>
|
|
52
|
+
`} <key>RunAtLoad</key><true/>
|
|
53
|
+
<key>KeepAlive</key>
|
|
54
|
+
<dict><key>SuccessfulExit</key><false/></dict>
|
|
55
|
+
<key>ProcessType</key><string>Background</string>
|
|
56
|
+
<key>LowPriorityIO</key><true/>
|
|
57
|
+
<key>Nice</key><integer>5</integer>
|
|
58
|
+
<key>StandardOutPath</key><string>${escapeXml(join(logs, "daemon.log"))}</string>
|
|
59
|
+
<key>StandardErrorPath</key><string>${escapeXml(join(logs, "daemon.error.log"))}</string>
|
|
60
|
+
</dict>
|
|
61
|
+
</plist>
|
|
62
|
+
`;
|
|
63
|
+
return {
|
|
64
|
+
kind, label: SERVICE_LABEL, path, contents,
|
|
65
|
+
enable: [["launchctl", ["load", "-w", path]]],
|
|
66
|
+
disable: [["launchctl", ["unload", "-w", path]]]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (kind === "systemd") {
|
|
70
|
+
const path = join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "systemd", "user", "vaultline.service");
|
|
71
|
+
const contents = `[Unit]
|
|
72
|
+
Description=Sealkeep — seals and archives AI coding-agent sessions
|
|
73
|
+
Documentation=https://github.com/vaultline/vaultline
|
|
74
|
+
After=default.target
|
|
75
|
+
|
|
76
|
+
[Service]
|
|
77
|
+
Type=simple
|
|
78
|
+
ExecStart=${[executable, ...args].map((value) => JSON.stringify(value)).join(" ")}
|
|
79
|
+
${environmentEntries.map(([key, value]) => `Environment=${key}=${value}\n`).join("")}Restart=on-failure
|
|
80
|
+
RestartSec=20
|
|
81
|
+
Nice=5
|
|
82
|
+
IOSchedulingClass=idle
|
|
83
|
+
|
|
84
|
+
[Install]
|
|
85
|
+
WantedBy=default.target
|
|
86
|
+
`;
|
|
87
|
+
return {
|
|
88
|
+
kind, label: "vaultline.service", path, contents,
|
|
89
|
+
enable: [["systemctl", ["--user", "daemon-reload"]], ["systemctl", ["--user", "enable", "--now", "vaultline.service"]]],
|
|
90
|
+
disable: [["systemctl", ["--user", "disable", "--now", "vaultline.service"]]]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (kind === "schtasks") {
|
|
94
|
+
const path = join(options.dataDir, "vaultline-task.xml");
|
|
95
|
+
const invocation = [executable, ...args].map((value) => `"${value}"`).join(" ");
|
|
96
|
+
// schtasks has no environment block, so anything the daemon needs has to be
|
|
97
|
+
// set inside the command it runs.
|
|
98
|
+
const commandLine = environmentEntries.length === 0
|
|
99
|
+
? invocation
|
|
100
|
+
: `cmd /c "${environmentEntries.map(([key, value]) => `set ${key}=${value}&& `).join("")}${invocation}"`;
|
|
101
|
+
return {
|
|
102
|
+
kind, label: "Sealkeep", path, contents: commandLine,
|
|
103
|
+
enable: [["schtasks", ["/create", "/f", "/tn", "Sealkeep", "/sc", "onlogon", "/rl", "limited", "/tr", commandLine]]],
|
|
104
|
+
disable: [["schtasks", ["/delete", "/f", "/tn", "Sealkeep"]]]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { kind, label: "", path: "", contents: "", enable: [], disable: [] };
|
|
108
|
+
}
|
|
109
|
+
/** Writes the unit file and asks the platform's service manager to start it at login. */
|
|
110
|
+
export async function installService(options) {
|
|
111
|
+
const plan = servicePlan(options);
|
|
112
|
+
if (plan.kind === "unsupported") {
|
|
113
|
+
return { kind: plan.kind, installed: false, path: "", ranCommands: [], note: `No background service is available on ${options.platform ?? platform()}. Run \`sealkeep daemon\` yourself.` };
|
|
114
|
+
}
|
|
115
|
+
const exec = options.exec ?? ((file, args) => run(file, args));
|
|
116
|
+
await mkdir(join(options.dataDir, "logs"), { recursive: true, mode: 0o700 });
|
|
117
|
+
if (plan.kind !== "schtasks") {
|
|
118
|
+
await mkdir(dirname(plan.path), { recursive: true });
|
|
119
|
+
await writeFile(plan.path, plan.contents, { mode: 0o600 });
|
|
120
|
+
}
|
|
121
|
+
const ranCommands = [];
|
|
122
|
+
for (const [file, args] of plan.enable) {
|
|
123
|
+
try {
|
|
124
|
+
await exec(file, args);
|
|
125
|
+
ranCommands.push([file, ...args].join(" "));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { kind: plan.kind, installed: false, path: plan.path, ranCommands, note: `${file} failed: ${error instanceof Error ? error.message.split("\n")[0] : "unknown error"}` };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { kind: plan.kind, installed: true, path: plan.path, ranCommands };
|
|
132
|
+
}
|
|
133
|
+
export async function uninstallService(options) {
|
|
134
|
+
const plan = servicePlan(options);
|
|
135
|
+
if (plan.kind === "unsupported")
|
|
136
|
+
return { kind: plan.kind, installed: false, path: "", ranCommands: [] };
|
|
137
|
+
const exec = options.exec ?? ((file, args) => run(file, args));
|
|
138
|
+
const ranCommands = [];
|
|
139
|
+
for (const [file, args] of plan.disable) {
|
|
140
|
+
try {
|
|
141
|
+
await exec(file, args);
|
|
142
|
+
ranCommands.push([file, ...args].join(" "));
|
|
143
|
+
}
|
|
144
|
+
catch { /* already stopped */ }
|
|
145
|
+
}
|
|
146
|
+
if (plan.kind !== "schtasks")
|
|
147
|
+
await rm(plan.path, { force: true });
|
|
148
|
+
return { kind: plan.kind, installed: false, path: plan.path, ranCommands };
|
|
149
|
+
}
|
|
150
|
+
/** Reports whether the unit file this build would write is currently present. */
|
|
151
|
+
export async function serviceStatus(options) {
|
|
152
|
+
const plan = servicePlan(options);
|
|
153
|
+
if (plan.kind === "unsupported")
|
|
154
|
+
return { kind: plan.kind, present: false, path: "" };
|
|
155
|
+
if (plan.kind === "schtasks") {
|
|
156
|
+
const exec = options.exec ?? ((file, args) => run(file, args));
|
|
157
|
+
const present = await exec("schtasks", ["/query", "/tn", "Sealkeep"]).then(() => true).catch(() => false);
|
|
158
|
+
return { kind: plan.kind, present, path: plan.path };
|
|
159
|
+
}
|
|
160
|
+
const contents = await readFile(plan.path, "utf8").catch(() => null);
|
|
161
|
+
const runs = contents?.match(/ExecStart=(.*)/)?.[1] ?? contents?.match(/<string>([^<]*daemon[^<]*)<\/string>/)?.[1];
|
|
162
|
+
return { kind: plan.kind, present: contents !== null, path: plan.path, runs: runs ?? undefined };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The environment a service unit needs beyond its flags. A service manager
|
|
166
|
+
* starts the daemon bare, so anything the installing session was told about
|
|
167
|
+
* where secrets live has to ride along in the unit — a vault set up with
|
|
168
|
+
* VAULTLINE_SECRET_BACKEND=file kept its phrase in a keystore the daemon then
|
|
169
|
+
* never looked in, and every tick died on "No recovery phrase available".
|
|
170
|
+
*/
|
|
171
|
+
export function serviceUnitEnvironment(base = {}, env = process.env) {
|
|
172
|
+
return { ...(env.VAULTLINE_SECRET_BACKEND ? { VAULTLINE_SECRET_BACKEND: env.VAULTLINE_SECRET_BACKEND } : {}), ...base };
|
|
173
|
+
}
|
|
174
|
+
/** The argv the service should run: this package's CLI, with the daemon subcommand. */
|
|
175
|
+
export function daemonInvocation(dataDir, options = {}) {
|
|
176
|
+
let cli = options.cliPath ?? process.argv[1] ?? "vaultline";
|
|
177
|
+
// A service unit runs plain node, and plain node cannot execute TypeScript.
|
|
178
|
+
// Installed packages always resolve to dist/cli.js, but a source checkout
|
|
179
|
+
// running under tsx resolves to src/cli.ts — writing that path into launchd
|
|
180
|
+
// produced a service that crash-looped on its first import, which is worse
|
|
181
|
+
// than refusing. The built CLI next to the source is the honest substitute;
|
|
182
|
+
// absent a build, the refusal says what to run instead of installing a unit
|
|
183
|
+
// that can never start.
|
|
184
|
+
if (/\.(ts|mts|cts|tsx)$/.test(cli)) {
|
|
185
|
+
// The published layout is dist/src/cli.js (package.json bin agrees).
|
|
186
|
+
const built = join(dirname(dirname(cli)), "dist", "src", "cli.js");
|
|
187
|
+
if (!existsSync(built)) {
|
|
188
|
+
fail("invalid_argument", "The background service runs plain node, which cannot execute the TypeScript source this session is running from. Run `npm run build` first so dist/src/cli.js exists, then install the service again.");
|
|
189
|
+
}
|
|
190
|
+
cli = built;
|
|
191
|
+
}
|
|
192
|
+
const args = [cli, "daemon", "--data-dir", dataDir];
|
|
193
|
+
if (options.reclaim)
|
|
194
|
+
args.push("--reclaim");
|
|
195
|
+
return { executable: process.execPath, args };
|
|
196
|
+
}
|
|
197
|
+
export function serviceKind(target = platform()) { return kindFor(target); }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing one archive, phase one: a self-carrying bundle.
|
|
3
|
+
*
|
|
4
|
+
* The bundle is the same framed object the cloud stores — envelope plus
|
|
5
|
+
* ciphertext — except its key slots are REPLACED with a single wrap under a
|
|
6
|
+
* share passcode. Replaced, not added: the recipient gets a thing the vault
|
|
7
|
+
* phrase deliberately does not open and the passcode does, so handing over a
|
|
8
|
+
* bundle never hands over anything about the vault, and revoking a share is
|
|
9
|
+
* as simple as not sending the passcode. The content bytes are the archive's
|
|
10
|
+
* own ciphertext, re-wrapped in kilobytes — nothing is re-encrypted.
|
|
11
|
+
*
|
|
12
|
+
* The passcode travels however the sender chooses (said aloud, another
|
|
13
|
+
* channel); the bundle can go over anything, because without the passcode it
|
|
14
|
+
* is noise. Team spaces later replace the passcode with member keys — same
|
|
15
|
+
* slot machinery, different recipients.
|
|
16
|
+
*/
|
|
17
|
+
export type ShareResult = {
|
|
18
|
+
bundlePath: string;
|
|
19
|
+
passcode: string;
|
|
20
|
+
bytes: number;
|
|
21
|
+
archiveId: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function generateSharePasscode(): string;
|
|
24
|
+
export declare function createShareBundle(dataDir: string, archiveId: string, bundlePath: string, options?: {
|
|
25
|
+
vaultPhrase: string;
|
|
26
|
+
passcode?: string;
|
|
27
|
+
}): Promise<ShareResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Opens a bundle anywhere — no vault, no account, no config. The passcode is
|
|
30
|
+
* the whole key, which is exactly the promise made to the person receiving it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function openShareBundle(bundlePath: string, destination: string, passcode: string): Promise<{
|
|
33
|
+
bytes: number;
|
|
34
|
+
}>;
|