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.
Files changed (180) hide show
  1. package/ARCHITECTURE.md +201 -0
  2. package/CHANGELOG.md +218 -0
  3. package/CONTROL_PLANE.md +86 -0
  4. package/LICENSE +34 -0
  5. package/README.md +249 -0
  6. package/THIRD_PARTY.md +22 -0
  7. package/THREAT_MODEL.md +107 -0
  8. package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
  9. package/dist/packages/vaultline-crypto/src/aead.js +24 -0
  10. package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
  11. package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
  12. package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
  13. package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
  14. package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
  15. package/dist/packages/vaultline-crypto/src/format.js +43 -0
  16. package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
  17. package/dist/packages/vaultline-crypto/src/index.js +5 -0
  18. package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
  19. package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
  20. package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
  21. package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
  22. package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
  23. package/dist/packages/vaultline-crypto/src/stream.js +477 -0
  24. package/dist/site/index.html +1542 -0
  25. package/dist/site.zip +0 -0
  26. package/dist/src/activity.d.ts +22 -0
  27. package/dist/src/activity.js +52 -0
  28. package/dist/src/adapters.d.ts +212 -0
  29. package/dist/src/adapters.js +533 -0
  30. package/dist/src/audit.d.ts +24 -0
  31. package/dist/src/audit.js +41 -0
  32. package/dist/src/autopilot.d.ts +77 -0
  33. package/dist/src/autopilot.js +148 -0
  34. package/dist/src/bip39-wordlist.d.ts +15 -0
  35. package/dist/src/bip39-wordlist.js +272 -0
  36. package/dist/src/branding.d.ts +31 -0
  37. package/dist/src/branding.js +31 -0
  38. package/dist/src/chunk-store.d.ts +142 -0
  39. package/dist/src/chunk-store.js +502 -0
  40. package/dist/src/cli.d.ts +2 -0
  41. package/dist/src/cli.js +2035 -0
  42. package/dist/src/cloud.d.ts +434 -0
  43. package/dist/src/cloud.js +851 -0
  44. package/dist/src/control-plane/auth.d.ts +62 -0
  45. package/dist/src/control-plane/auth.js +123 -0
  46. package/dist/src/control-plane/server.d.ts +31 -0
  47. package/dist/src/control-plane/server.js +263 -0
  48. package/dist/src/control-plane/store.d.ts +101 -0
  49. package/dist/src/control-plane/store.js +82 -0
  50. package/dist/src/control-plane-cli.d.ts +2 -0
  51. package/dist/src/control-plane-cli.js +37 -0
  52. package/dist/src/control-plane-server.d.ts +10 -0
  53. package/dist/src/control-plane-server.js +11 -0
  54. package/dist/src/control-plane.d.ts +78 -0
  55. package/dist/src/control-plane.js +61 -0
  56. package/dist/src/crypto.d.ts +56 -0
  57. package/dist/src/crypto.js +132 -0
  58. package/dist/src/daemon.d.ts +52 -0
  59. package/dist/src/daemon.js +142 -0
  60. package/dist/src/dashboard-cli.d.ts +2 -0
  61. package/dist/src/dashboard-cli.js +20 -0
  62. package/dist/src/disk.d.ts +110 -0
  63. package/dist/src/disk.js +169 -0
  64. package/dist/src/doctor.d.ts +11 -0
  65. package/dist/src/doctor.js +198 -0
  66. package/dist/src/enroll.d.ts +27 -0
  67. package/dist/src/enroll.js +136 -0
  68. package/dist/src/errors.d.ts +26 -0
  69. package/dist/src/errors.js +23 -0
  70. package/dist/src/heartbeat.d.ts +89 -0
  71. package/dist/src/heartbeat.js +120 -0
  72. package/dist/src/index-sync.d.ts +53 -0
  73. package/dist/src/index-sync.js +147 -0
  74. package/dist/src/leakscan.d.ts +48 -0
  75. package/dist/src/leakscan.js +222 -0
  76. package/dist/src/local-api.d.ts +132 -0
  77. package/dist/src/local-api.js +1757 -0
  78. package/dist/src/managed-chunks.d.ts +55 -0
  79. package/dist/src/managed-chunks.js +108 -0
  80. package/dist/src/mcp-install.d.ts +52 -0
  81. package/dist/src/mcp-install.js +140 -0
  82. package/dist/src/mcp.d.ts +1 -0
  83. package/dist/src/mcp.js +59 -0
  84. package/dist/src/migrate.d.ts +35 -0
  85. package/dist/src/migrate.js +88 -0
  86. package/dist/src/mnemonic.d.ts +60 -0
  87. package/dist/src/mnemonic.js +134 -0
  88. package/dist/src/net.d.ts +2 -0
  89. package/dist/src/net.js +16 -0
  90. package/dist/src/notify.d.ts +46 -0
  91. package/dist/src/notify.js +84 -0
  92. package/dist/src/offload.d.ts +117 -0
  93. package/dist/src/offload.js +331 -0
  94. package/dist/src/onboarding.d.ts +10 -0
  95. package/dist/src/onboarding.js +44 -0
  96. package/dist/src/packages.d.ts +126 -0
  97. package/dist/src/packages.js +114 -0
  98. package/dist/src/passkey.d.ts +26 -0
  99. package/dist/src/passkey.js +54 -0
  100. package/dist/src/password-lock.d.ts +19 -0
  101. package/dist/src/password-lock.js +156 -0
  102. package/dist/src/paths.d.ts +9 -0
  103. package/dist/src/paths.js +24 -0
  104. package/dist/src/providers/gcs.d.ts +133 -0
  105. package/dist/src/providers/gcs.js +235 -0
  106. package/dist/src/providers/gdrive.d.ts +156 -0
  107. package/dist/src/providers/gdrive.js +335 -0
  108. package/dist/src/providers/index.d.ts +45 -0
  109. package/dist/src/providers/index.js +74 -0
  110. package/dist/src/providers/s3.d.ts +174 -0
  111. package/dist/src/providers/s3.js +345 -0
  112. package/dist/src/providers/sigv4.d.ts +78 -0
  113. package/dist/src/providers/sigv4.js +112 -0
  114. package/dist/src/queue.d.ts +185 -0
  115. package/dist/src/queue.js +286 -0
  116. package/dist/src/recovery.d.ts +40 -0
  117. package/dist/src/recovery.js +132 -0
  118. package/dist/src/rehydrate.d.ts +43 -0
  119. package/dist/src/rehydrate.js +66 -0
  120. package/dist/src/restore.d.ts +34 -0
  121. package/dist/src/restore.js +80 -0
  122. package/dist/src/retention.d.ts +251 -0
  123. package/dist/src/retention.js +446 -0
  124. package/dist/src/rotate.d.ts +47 -0
  125. package/dist/src/rotate.js +95 -0
  126. package/dist/src/search.d.ts +147 -0
  127. package/dist/src/search.js +677 -0
  128. package/dist/src/secrets.d.ts +86 -0
  129. package/dist/src/secrets.js +220 -0
  130. package/dist/src/service.d.ts +73 -0
  131. package/dist/src/service.js +197 -0
  132. package/dist/src/share.d.ts +34 -0
  133. package/dist/src/share.js +68 -0
  134. package/dist/src/spool.d.ts +97 -0
  135. package/dist/src/spool.js +213 -0
  136. package/dist/src/start-tui.d.ts +17 -0
  137. package/dist/src/start-tui.js +113 -0
  138. package/dist/src/start.d.ts +75 -0
  139. package/dist/src/start.js +101 -0
  140. package/dist/src/storage-setup.d.ts +49 -0
  141. package/dist/src/storage-setup.js +222 -0
  142. package/dist/src/storage-targets.d.ts +40 -0
  143. package/dist/src/storage-targets.js +147 -0
  144. package/dist/src/stream-to-cloud.d.ts +76 -0
  145. package/dist/src/stream-to-cloud.js +820 -0
  146. package/dist/src/sync-rules.d.ts +85 -0
  147. package/dist/src/sync-rules.js +125 -0
  148. package/dist/src/trash.d.ts +15 -0
  149. package/dist/src/trash.js +63 -0
  150. package/dist/src/tui.d.ts +18 -0
  151. package/dist/src/tui.js +179 -0
  152. package/dist/src/types.d.ts +191 -0
  153. package/dist/src/types.js +3 -0
  154. package/dist/src/ui-server.d.ts +187 -0
  155. package/dist/src/ui-server.js +293 -0
  156. package/dist/src/ui.d.ts +41 -0
  157. package/dist/src/ui.js +102 -0
  158. package/dist/src/update.d.ts +30 -0
  159. package/dist/src/update.js +56 -0
  160. package/dist/src/upload.d.ts +46 -0
  161. package/dist/src/upload.js +80 -0
  162. package/dist/src/vault.d.ts +208 -0
  163. package/dist/src/vault.js +812 -0
  164. package/dist/src/watcher.d.ts +34 -0
  165. package/dist/src/watcher.js +121 -0
  166. package/dist/src/worker.d.ts +52 -0
  167. package/dist/src/worker.js +190 -0
  168. package/package.json +65 -0
  169. package/web/app.js +1372 -0
  170. package/web/index.html +476 -0
  171. package/web/rail.js +308 -0
  172. package/web/retention.html +17 -0
  173. package/web/rules-view.js +249 -0
  174. package/web/sessions-view.js +448 -0
  175. package/web/sessions.html +17 -0
  176. package/web/setup-api.js +181 -0
  177. package/web/setup-logic.js +394 -0
  178. package/web/setup.html +419 -0
  179. package/web/setup.js +697 -0
  180. 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) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", "'": "&apos;", '"': "&quot;" })[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
+ }>;