sealkeep 0.5.2 → 0.6.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/dist/site.zip CHANGED
Binary file
@@ -286,11 +286,11 @@ export function hookConfig(agent, executable = "vaultline", dataDir = "~/.vaultl
286
286
  PostCompact: entry(3),
287
287
  SessionStart: rehydrateEntry
288
288
  },
289
- _vaultlineNote: "Run `sealkeep queue run` (with VAULTLINE_RECOVERY_PHRASE set) to encrypt queued sessions. If this Codex version has no SessionStart hook, the entry is ignored; `sealkeep recover <session-file>` is the manual door."
289
+ _vaultlineNote: "Run `sealkeep queue run` (with SEALKEEP_RECOVERY_PHRASE set) to encrypt queued sessions. If this Codex version has no SessionStart hook, the entry is ignored; `sealkeep recover <session-file>` is the manual door."
290
290
  };
291
291
  return {
292
292
  hooks: { SessionEnd: entry(5), SessionStart: rehydrateEntry },
293
- _vaultlineNote: "Merge the hooks object into ~/.claude/settings.json. The hook stores no secret; run `sealkeep queue run` with VAULTLINE_RECOVERY_PHRASE to encrypt queued sessions. SessionStart(resume) puts archived transcripts back before the resume reads them."
293
+ _vaultlineNote: "Merge the hooks object into ~/.claude/settings.json. The hook stores no secret; run `sealkeep queue run` with SEALKEEP_RECOVERY_PHRASE to encrypt queued sessions. SessionStart(resume) puts archived transcripts back before the resume reads them."
294
294
  };
295
295
  }
296
296
  /**
@@ -34,11 +34,11 @@ export async function enableAutopilot(dataDir, options = {}) {
34
34
  remembered = await rememberRecoveryPhrase(dataDir, config.vaultId, phrase, options.backend);
35
35
  }
36
36
  catch (error) {
37
- notes.push(`Could not use this machine's keystore (${error instanceof Error ? error.message.split("\n")[0] : "unknown"}). The service will need VAULTLINE_RECOVERY_PHRASE in its environment.`);
37
+ notes.push(`Could not use this machine's keystore (${error instanceof Error ? error.message.split("\n")[0] : "unknown"}). The service will need SEALKEEP_RECOVERY_PHRASE in its environment.`);
38
38
  }
39
39
  }
40
40
  else if (!remember) {
41
- notes.push("The phrase was not stored. Put VAULTLINE_RECOVERY_PHRASE in the service environment yourself, or sessions will queue up unsealed.");
41
+ notes.push("The phrase was not stored. Put SEALKEEP_RECOVERY_PHRASE in the service environment yourself, or sessions will queue up unsealed.");
42
42
  }
43
43
  const desired = { policy: reclaim ? "archive-and-reclaim" : "sync-only", olderThanDays: options.olderThanDays ?? DEFAULTS.olderThanDays, graceDays: options.graceDays ?? DEFAULTS.graceDays };
44
44
  // A vault written before archive lifecycle existed has no deleteAfterDays; absent means never.
@@ -89,7 +89,7 @@ export async function enableAutopilot(dataDir, options = {}) {
89
89
  // this the daemon takes the "uploads are disabled" branch on every tick,
90
90
  // and since reclaiming requires a verified remote copy, it also never frees
91
91
  // a byte — the one thing the user installed it to do.
92
- environment: serviceUnitEnvironment(hasRemoteTarget ? { VAULTLINE_ENABLE_SIGNER: "1" } : {})
92
+ environment: serviceUnitEnvironment(hasRemoteTarget ? { SEALKEEP_ENABLE_SIGNER: "1" } : {})
93
93
  };
94
94
  const service = options.dryRun
95
95
  ? { kind: servicePlan(serviceOptions).kind, installed: false, path: servicePlan(serviceOptions).path, ranCommands: [], note: "dry run: nothing was installed" }
@@ -407,6 +407,12 @@ async function runChunkSeal(ctx, args) {
407
407
  indexed = true;
408
408
  }
409
409
  catch { /* the next `index build` re-indexes this archive */ }
410
+ // The account learns what each destination holds from the machines that
411
+ // write to them — refresh the stamps now that this archive is recorded.
412
+ {
413
+ const { pushTargetUsage } = await import("./storage-targets.js");
414
+ void pushTargetUsage(ctx.dataDir);
415
+ }
410
416
  return { record, folder: args.folder, storedBytes: sealed.storedBytes, chunkCount: envelope.chunks.length, reusedChunks: args.reusedChunks, heldAtMostBytes, indexed };
411
417
  }
412
418
  /**
package/dist/src/cli.js CHANGED
@@ -36,6 +36,7 @@ import { setupPlan } from "./storage-setup.js";
36
36
  import { connectGdrive } from "./providers/gdrive.js";
37
37
  import { resolveRecoveryPhrase } from "./secrets.js";
38
38
  import { amber, bold, blue, bytes, callout, command as cmd, dim, green, heading, hint, keyValue, mark, relativeTime, shortPath, steps, table, vaultline } from "./ui.js";
39
+ import { envVar } from "./env.js";
39
40
  const VALUE_FLAGS = new Set(["--data-dir", "--recovery-phrase", "--agent", "--home", "--limit", "--executable", "--provider", "--bucket", "--prefix", "--region", "--older-than-days", "--status", "--max", "--port", "--overwrite", "--label", "--public-key", "--config-id", "--backend", "--endpoint", "--policy", "--grace-days", "--interval", "--manifest", "--artifact", "--key", "--current", "--api", "--token", "--group", "--cli-path", "--account-id", "--project", "--out", "--password"]);
40
41
  function take(args, flag, fallback) { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : fallback; }
41
42
  function positionals(args) { return args.filter((value, index) => !value.startsWith("-") && !VALUE_FLAGS.has(args[index - 1] ?? "")); }
@@ -47,7 +48,7 @@ function agentId(value) { const agent = required(value, "Agent must be codex or
47
48
  async function unlock(dataDir, explicit) {
48
49
  const config = await readConfig(dataDir).catch(() => null);
49
50
  if (!config)
50
- return explicit ?? process.env.VAULTLINE_RECOVERY_PHRASE;
51
+ return explicit ?? envVar("RECOVERY_PHRASE");
51
52
  const resolved = await resolveRecoveryPhrase(dataDir, config.vaultId, explicit);
52
53
  if (resolved)
53
54
  return resolved;
@@ -250,7 +251,12 @@ function usage() {
250
251
  }
251
252
  async function main() {
252
253
  const [command, ...args] = process.argv.slice(2);
253
- const dataDir = take(args, "--data-dir", defaultDataDir());
254
+ // The product itself writes SEALKEEP_DATA_DIR into the agent configs that
255
+ // `mcp install` generates, and the MCP server honours it — so a person who
256
+ // exports it and then runs the CLI reasonably expects the same vault. It used
257
+ // to be ignored here, which meant the CLI quietly worked on a different vault
258
+ // than the agent did. An explicit --data-dir still wins over the variable.
259
+ const dataDir = take(args, "--data-dir", envVar("DATA_DIR") ?? defaultDataDir());
254
260
  const json = args.includes("--json");
255
261
  // --version is the first thing anyone types at an unfamiliar CLI, and it was
256
262
  // answering "Unknown command". Read it from the manifest rather than hardcoding
@@ -371,7 +377,7 @@ async function main() {
371
377
  ? `automatically ${dim(`· phrase kept in ${result.remembered.backend}`)}`
372
378
  : dryRun && !args.includes("--no-remember")
373
379
  ? `automatically ${dim("· the phrase would be kept in this machine's keystore")}`
374
- : `${mark.warn()} needs VAULTLINE_RECOVERY_PHRASE`],
380
+ : `${mark.warn()} needs SEALKEEP_RECOVERY_PHRASE`],
375
381
  ["Reclaims", result.reclaimEnabled ? `after ${result.retention.olderThanDays}d ${dim(`+ ${result.retention.graceDays}d grace, once a remote copy is verified`)}` : dim("never — sync only")],
376
382
  ["Starts", result.service.installed ? `at login ${dim(`· ${result.service.kind}`)}` : dryRun ? dim(`${result.service.kind} service at ${shortPath(result.service.path, 44)}`) : `${mark.warn()} not installed`]
377
383
  ]));
@@ -424,7 +430,7 @@ async function main() {
424
430
  print(` ${dim("Could not reach your account, so the last known sync rules are in force.")}`);
425
431
  print(heading("Next"));
426
432
  print(steps([
427
- `Encrypt what is queued: ${cmd(`VAULTLINE_RECOVERY_PHRASE="…" sealkeep queue run`)}`,
433
+ `Encrypt what is queued: ${cmd(`SEALKEEP_RECOVERY_PHRASE="…" sealkeep queue run`)}`,
428
434
  `Archive automatically: ${cmd("vaultline agents hook-config codex")} ${dim("(review, then merge)")}`,
429
435
  `See it: ${cmd("sealkeep desktop")}`
430
436
  ]));
@@ -441,7 +447,7 @@ async function main() {
441
447
  print(`\n${BRAND}\n`);
442
448
  const choice = await onboard();
443
449
  const filled = choice.mode === "paid"
444
- ? { ...choice, password: process.env.VAULTLINE_CLOUD_PASSWORD ?? await promptForPhrase("Choose a password (12+ characters): ") }
450
+ ? { ...choice, password: envVar("CLOUD_PASSWORD") ?? await promptForPhrase("Choose a password (12+ characters): ") }
445
451
  : choice;
446
452
  const result = await start(dataDir, filled);
447
453
  print(`\n${recoveryPhraseScreen(result.phrase)}`);
@@ -471,7 +477,7 @@ async function main() {
471
477
  ? await start(dataDir, {
472
478
  mode: "paid",
473
479
  email: take(args, "--email") ?? fail("invalid_argument", "--email is required for a paid account"),
474
- password: process.env.VAULTLINE_CLOUD_PASSWORD ?? await promptForPhrase("Choose a password (12+ characters): "),
480
+ password: envVar("CLOUD_PASSWORD") ?? await promptForPhrase("Choose a password (12+ characters): "),
475
481
  label: take(args, "--label", "this machine")
476
482
  })
477
483
  : await start(dataDir, {
@@ -558,12 +564,12 @@ async function main() {
558
564
  const email = take(args, "--email") ?? fail("invalid_argument", "--email is required");
559
565
  // A one-time code from the panel is the only way in for an account that
560
566
  // signed up with Google, and it works for password accounts too.
561
- const code = take(args, "--code") ?? process.env.VAULTLINE_CLOUD_CODE;
567
+ const code = take(args, "--code") ?? envVar("CLOUD_CODE");
562
568
  const account = code
563
569
  ? await cloud.loginWithCode(dataDir, { email: email, token: code })
564
570
  : await cloud.login(dataDir, {
565
571
  email: email,
566
- password: process.env.VAULTLINE_CLOUD_PASSWORD ?? await promptForPhrase("Password: ")
572
+ password: envVar("CLOUD_PASSWORD") ?? await promptForPhrase("Password: ")
567
573
  });
568
574
  if (json) {
569
575
  print(JSON.stringify(account, null, 2));
@@ -1155,7 +1161,7 @@ async function main() {
1155
1161
  return;
1156
1162
  }
1157
1163
  if (action === "run") {
1158
- const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "No recovery phrase available. Pass --recovery-phrase, set VAULTLINE_RECOVERY_PHRASE, or run `sealkeep autopilot` so this machine remembers it.");
1164
+ const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "No recovery phrase available. Pass --recovery-phrase, set SEALKEEP_RECOVERY_PHRASE, or run `sealkeep autopilot` so this machine remembers it.");
1159
1165
  const processed = await drainQueue(dataDir, phrase, { max: Number(take(args, "--max", "25")) });
1160
1166
  if (json) {
1161
1167
  print(JSON.stringify(processed, null, 2));
@@ -1563,7 +1569,7 @@ async function main() {
1563
1569
  return;
1564
1570
  }
1565
1571
  if (command === "migrate") {
1566
- const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "--recovery-phrase is required (or set VAULTLINE_RECOVERY_PHRASE)");
1572
+ const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "--recovery-phrase is required (or set SEALKEEP_RECOVERY_PHRASE)");
1567
1573
  const result = await migrateVault(dataDir, phrase);
1568
1574
  if (json) {
1569
1575
  print(JSON.stringify(result, null, 2));
@@ -1573,7 +1579,7 @@ async function main() {
1573
1579
  return;
1574
1580
  }
1575
1581
  if (command === "rewrap") {
1576
- const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "--recovery-phrase is required (or set VAULTLINE_RECOVERY_PHRASE)");
1582
+ const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "--recovery-phrase is required (or set SEALKEEP_RECOVERY_PHRASE)");
1577
1583
  const result = await rewrapVault(dataDir, phrase, { group: take(args, "--group") });
1578
1584
  if (json) {
1579
1585
  print(JSON.stringify(result, null, 2));
@@ -1757,8 +1763,8 @@ async function main() {
1757
1763
  const backend = take(args, "--backend");
1758
1764
  // The account's shared Google client first: no console visit, no env
1759
1765
  // var — the consent screen is the whole setup. The PKCE desktop path
1760
- // stays for self-hosters who set VAULTLINE_GDRIVE_CLIENT_ID.
1761
- const viaCloud = !process.env.VAULTLINE_GDRIVE_CLIENT_ID;
1766
+ // stays for self-hosters who set SEALKEEP_GDRIVE_CLIENT_ID.
1767
+ const viaCloud = !envVar("GDRIVE_CLIENT_ID");
1762
1768
  const credentials = viaCloud ? await (await import("./cloud.js")).connectGdriveViaCloud(dataDir, {
1763
1769
  onConsentUrl: (url) => {
1764
1770
  print(`\n ${dim("Approve Sealkeep in the browser window. If none opened, use this link:")}`);
@@ -1982,7 +1988,7 @@ async function main() {
1982
1988
  fail("invalid_argument", "Usage: sealkeep retention <preview|apply|prune|offload|approve|policy> [options]");
1983
1989
  }
1984
1990
  if (command === "daemon") {
1985
- const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "No recovery phrase available. Run `sealkeep autopilot` so this machine can unlock itself, or set VAULTLINE_RECOVERY_PHRASE.");
1991
+ const phrase = required(await unlock(dataDir, take(args, "--recovery-phrase")), "No recovery phrase available. Run `sealkeep autopilot` so this machine can unlock itself, or set SEALKEEP_RECOVERY_PHRASE.");
1986
1992
  const reclaim = args.includes("--reclaim");
1987
1993
  const daemon = await startDaemon(dataDir, {
1988
1994
  phrase, intervalMs: Number(take(args, "--interval", "30")) * 1000, home: take(args, "--home"),
@@ -2012,10 +2018,10 @@ async function main() {
2012
2018
  }
2013
2019
  const HINTS = {
2014
2020
  vault_not_initialized: "run `sealkeep quickstart`",
2015
- recovery_phrase_missing: "pass --recovery-phrase, or set VAULTLINE_RECOVERY_PHRASE",
2021
+ recovery_phrase_missing: "pass --recovery-phrase, or set SEALKEEP_RECOVERY_PHRASE",
2016
2022
  recovery_phrase_mismatch: "check the phrase from your recovery kit",
2017
2023
  storage_not_configured: "run `sealkeep storage configure --provider … --bucket … --prefix …`",
2018
- signer_not_configured: "set VAULTLINE_ENABLE_SIGNER=1 and store credentials",
2024
+ signer_not_configured: "set SEALKEEP_ENABLE_SIGNER=1 and store credentials",
2019
2025
  destination_exists: "add --overwrite backup to keep the existing file",
2020
2026
  destination_unwritable: "free some space, or restore to a different <destination> on another volume",
2021
2027
  archive_not_found: "run `sealkeep list` to see archive ids",
package/dist/src/cloud.js CHANGED
@@ -6,6 +6,7 @@ import { canonicalPhrase } from "./mnemonic.js";
6
6
  import { fail, isVaultlineError, VaultlineError } from "./errors.js";
7
7
  import { sha256 } from "./crypto.js";
8
8
  import { listArchives, readConfig, writeRecord } from "./vault.js";
9
+ import { envVar } from "./env.js";
9
10
  /**
10
11
  * Sealkeep Cloud: the managed tier.
11
12
  *
@@ -16,7 +17,7 @@ import { listArchives, readConfig, writeRecord } from "./vault.js";
16
17
  * control plane is never sent a key that opens it.
17
18
  */
18
19
  export const DEFAULT_CLOUD_URL = "https://shared.spala.ai/p04946/api";
19
- const cloudUrl = (env = process.env) => (env.VAULTLINE_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
20
+ const cloudUrl = (env = process.env) => (envVar("CLOUD_URL", env) ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
20
21
  /**
21
22
  * Where this vault's cloud token lives in the keystore.
22
23
  *
@@ -4,18 +4,19 @@ import { join } from "node:path";
4
4
  import { createControlPlaneServer } from "./control-plane/server.js";
5
5
  import { defaultDataDir } from "./vault.js";
6
6
  import { freePort } from "./net.js";
7
+ import { envVar } from "./env.js";
7
8
  /**
8
9
  * Runs the control plane.
9
10
  *
10
- * Authentication is on by default. `VAULTLINE_CONTROL_PLANE_DEV=1` turns it off for
11
+ * Authentication is on by default. `SEALKEEP_CONTROL_PLANE_DEV=1` turns it off for
11
12
  * local interface work and the banner says so loudly, because an unauthenticated
12
13
  * instance reachable from a network would let anyone enumerate account metadata.
13
14
  */
14
- const development = process.env.VAULTLINE_CONTROL_PLANE_DEV === "1";
15
- const storePath = process.env.VAULTLINE_CONTROL_PLANE_STORE ?? join(defaultDataDir(), "control-plane", "state.json");
16
- const tls = process.env.VAULTLINE_TLS_CERT && process.env.VAULTLINE_TLS_KEY
17
- ? { cert: readFileSync(process.env.VAULTLINE_TLS_CERT), key: readFileSync(process.env.VAULTLINE_TLS_KEY) }
18
- : undefined;
15
+ const development = envVar("CONTROL_PLANE_DEV") === "1";
16
+ const storePath = envVar("CONTROL_PLANE_STORE") ?? join(defaultDataDir(), "control-plane", "state.json");
17
+ const tlsCert = envVar("TLS_CERT");
18
+ const tlsKey = envVar("TLS_KEY");
19
+ const tls = tlsCert && tlsKey ? { cert: readFileSync(tlsCert), key: readFileSync(tlsKey) } : undefined;
19
20
  const server = createControlPlaneServer({
20
21
  storePath: development ? undefined : storePath,
21
22
  requireAuth: !development,
@@ -31,7 +32,7 @@ async function listen() {
31
32
  if (development)
32
33
  console.log("MODE: development — device authentication is DISABLED and state is in memory. Do not expose this.");
33
34
  else
34
- console.log(`Durable state: ${storePath}${tls ? "" : "\nWARNING: no TLS configured. Set VAULTLINE_TLS_CERT and VAULTLINE_TLS_KEY before accepting non-loopback traffic."}`);
35
+ console.log(`Durable state: ${storePath}${tls ? "" : "\nWARNING: no TLS configured. Set SEALKEEP_TLS_CERT and SEALKEEP_TLS_KEY before accepting non-loopback traffic."}`);
35
36
  });
36
37
  }
37
38
  void listen();
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { fail } from "./errors.js";
3
+ import { envVar } from "./env.js";
3
4
  export const DEFAULT_LEASE_TTL_MS = 15 * 60_000;
4
5
  class PlannedProvider {
5
6
  provider;
@@ -44,13 +45,13 @@ export function assertLeaseUsable(lease, now = Date.now()) {
44
45
  }
45
46
  /** Feature flag for provider work in progress. Off by default and read per call so tests and operators can flip it. */
46
47
  export function signerEnabled(env = process.env) {
47
- return env.VAULTLINE_ENABLE_SIGNER === "1";
48
+ return envVar("ENABLE_SIGNER", env) === "1";
48
49
  }
49
50
  const uploadClients = new Map();
50
51
  /** Registration is refused unless the operator explicitly enabled signer work. */
51
52
  export function registerUploadClient(client, env = process.env) {
52
53
  if (!signerEnabled(env))
53
- fail("signer_not_configured", "Set VAULTLINE_ENABLE_SIGNER=1 to register a provider upload client", { provider: client.kind });
54
+ fail("signer_not_configured", "Set SEALKEEP_ENABLE_SIGNER=1 to register a provider upload client", { provider: client.kind });
54
55
  uploadClients.set(client.kind, client);
55
56
  }
56
57
  export function uploadClientFor(kind, env = process.env) {
@@ -5,7 +5,8 @@
5
5
  import { defaultDataDir } from "./vault.js";
6
6
  import { createLocalApiServer, localApiToken, localApiTokenPath } from "./local-api.js";
7
7
  import { freePort } from "./net.js";
8
- const dataDir = process.env.VAULTLINE_DATA_DIR ?? defaultDataDir();
8
+ import { envVar } from "./env.js";
9
+ const dataDir = envVar("DATA_DIR") ?? defaultDataDir();
9
10
  async function listen() {
10
11
  const token = await localApiToken(dataDir);
11
12
  const server = createLocalApiServer(dataDir, token);
@@ -10,6 +10,7 @@ import { quotaMessage, quotaState } from "./packages.js";
10
10
  import { loadProviderCredentials, resolveRecoveryPhrase } from "./secrets.js";
11
11
  import { ENVELOPE_VERSION, SUITES } from "../packages/vaultline-crypto/src/index.js";
12
12
  import { readConfig } from "./vault.js";
13
+ import { envVar } from "./env.js";
13
14
  async function writable(directory) {
14
15
  const probe = join(directory, `.vaultline-write-probe-${randomUUID()}`);
15
16
  try {
@@ -175,7 +176,7 @@ export async function runDoctor(dataDir, env = process.env) {
175
176
  // missing and named an environment variable as the cure.
176
177
  const phraseAvailable = config
177
178
  ? await resolveRecoveryPhrase(dataDir, config.vaultId, undefined, env).then((phrase) => Boolean(phrase)).catch(() => false)
178
- : Boolean(env.VAULTLINE_RECOVERY_PHRASE);
179
+ : Boolean(envVar("RECOVERY_PHRASE", env));
179
180
  checks.push(phraseAvailable
180
181
  ? { name: "recovery-phrase", status: "pass", detail: "This machine can unlock the vault on its own, so archiving runs unattended." }
181
182
  : { name: "recovery-phrase", status: "warn", detail: "This machine doesn't hold your recovery phrase, so it can't archive on its own. Add it under Settings › Unlocking to let archiving run unattended. Your vault and archives are unaffected." });
@@ -18,7 +18,7 @@ import { setRetentionPolicy, retentionSettings, DEFAULT_RETENTION } from "./rete
18
18
  * injectable — this module has to be testable against a fake today and
19
19
  * work unchanged against the real thing later.
20
20
  */
21
- const cloudUrl = (env = process.env) => (env.VAULTLINE_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
21
+ const cloudUrl = (env = process.env) => (env.SEALKEEP_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
22
22
  /**
23
23
  * The token location comes from cloud.ts rather than being restated here.
24
24
  *
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Environment variables, under the product's own name.
3
+ *
4
+ * The product was called Vaultline until August 2026, and its variables carried
5
+ * that name. They still work — someone's launchd plist or CI file should not
6
+ * break because we renamed a brand — but everything the product prints, writes,
7
+ * and documents now says SEALKEEP_. When both are set the new name wins, so a
8
+ * machine can be migrated one variable at a time.
9
+ */
10
+ /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era name. */
11
+ export declare function envVar(name: string, env?: NodeJS.ProcessEnv): string | undefined;
12
+ /** True when either spelling is set to something truthy. */
13
+ export declare function envFlag(name: string, env?: NodeJS.ProcessEnv): boolean;
14
+ /** The name to print or write into a config file — always the current one. */
15
+ export declare const envName: (name: string) => string;
16
+ /** Both spellings, for messages that tell someone what to set. */
17
+ export declare const envNames: (name: string) => string[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Environment variables, under the product's own name.
3
+ *
4
+ * The product was called Vaultline until August 2026, and its variables carried
5
+ * that name. They still work — someone's launchd plist or CI file should not
6
+ * break because we renamed a brand — but everything the product prints, writes,
7
+ * and documents now says SEALKEEP_. When both are set the new name wins, so a
8
+ * machine can be migrated one variable at a time.
9
+ */
10
+ const LEGACY_PREFIX = "VAULTLINE_";
11
+ const PREFIX = "SEALKEEP_";
12
+ /** Reads `SEALKEEP_<name>`, falling back to the Vaultline-era name. */
13
+ export function envVar(name, env = process.env) {
14
+ const current = env[PREFIX + name];
15
+ if (current !== undefined && current !== "")
16
+ return current;
17
+ const legacy = env[LEGACY_PREFIX + name];
18
+ return legacy === "" ? undefined : legacy;
19
+ }
20
+ /** True when either spelling is set to something truthy. */
21
+ export function envFlag(name, env = process.env) {
22
+ const value = envVar(name, env);
23
+ return value === "1" || value === "true" || value === "yes";
24
+ }
25
+ /** The name to print or write into a config file — always the current one. */
26
+ export const envName = (name) => PREFIX + name;
27
+ /** Both spellings, for messages that tell someone what to set. */
28
+ export const envNames = (name) => [PREFIX + name, LEGACY_PREFIX + name];
@@ -24,6 +24,7 @@ import { isV2 } from "./types.js";
24
24
  import * as cloud from "./cloud.js";
25
25
  import { configureRemoteStorage, initialize, listArchives, previewRetention, readConfig, vaultStatus, writeRecord } from "./vault.js";
26
26
  import { sha256 } from "./crypto.js";
27
+ import { envVar } from "./env.js";
27
28
  const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
28
29
  /**
29
30
  * `vault.line` and `vault.localhost` join the list because the address bar is
@@ -303,7 +304,7 @@ export async function testStorageTarget(input) {
303
304
  const payload = Buffer.from(`sealkeep write test ${randomBytes(16).toString("hex")}\n`);
304
305
  const digest = sha256(payload);
305
306
  try {
306
- // VAULTLINE_ENABLE_SIGNER gates *uploading archives* through a provider
307
+ // SEALKEEP_ENABLE_SIGNER gates *uploading archives* through a provider
307
308
  // client, and it is off unless the installed service turns it on. That gate
308
309
  // is right for archives and wrong for this: refusing to check a bucket
309
310
  // because archives are not enabled yet would mean the only way to find out
@@ -312,7 +313,7 @@ export async function testStorageTarget(input) {
312
313
  // fifty bytes the person explicitly asked to have written, to a bucket they
313
314
  // just named, with a credential they just typed. Nothing else in this
314
315
  // process gains the flag and the archive path is untouched.
315
- const probeEnv = { ...process.env, VAULTLINE_ENABLE_SIGNER: "1" };
316
+ const probeEnv = { ...process.env, SEALKEEP_ENABLE_SIGNER: "1" };
316
317
  const target = { provider, bucket, prefix, ...(region ? { region } : {}) };
317
318
  const client = createUploadClient(target, credentials, endpoint ? endpointOverrides(endpoint) : {}, probeEnv);
318
319
  const lease = createActiveLease(target, {
@@ -648,12 +649,13 @@ async function cloudPlanAndQuota(dataDir) {
648
649
  /**
649
650
  * The account panel lives on the product domain, which is no longer the API's
650
651
  * origin — deriving it from the cloud URL sent people to the old address after
651
- * the rename. A self-hosted plane (VAULTLINE_CLOUD_URL set) still derives from
652
+ * the rename. A self-hosted plane (SEALKEEP_CLOUD_URL set) still derives from
652
653
  * its own origin, because there is no sealkeep.spala.ai panel for it.
653
654
  */
654
655
  async function panelUrl() {
655
- if (process.env.VAULTLINE_CLOUD_URL) {
656
- const origin = process.env.VAULTLINE_CLOUD_URL.replace(/\/+$/, "").replace(/\/api$/, "");
656
+ const selfHosted = envVar("CLOUD_URL");
657
+ if (selfHosted) {
658
+ const origin = selfHosted.replace(/\/+$/, "").replace(/\/api$/, "");
657
659
  return `${origin}/#account`;
658
660
  }
659
661
  const { PRODUCT_URL } = await import("./branding.js");
@@ -950,7 +952,7 @@ export function createLocalApiServer(dataDir, token, options = {}) {
950
952
  if (offerCache && Date.now() - offerCache.at < 10 * 60_000)
951
953
  return offerCache.value;
952
954
  try {
953
- const base = (process.env.VAULTLINE_CLOUD_URL ?? cloud.DEFAULT_CLOUD_URL).replace(/\/$/, "");
955
+ const base = (envVar("CLOUD_URL") ?? cloud.DEFAULT_CLOUD_URL).replace(/\/$/, "");
954
956
  const response = await fetch(`${base}/v1/public/offer`, { signal: AbortSignal.timeout(4000) });
955
957
  const body = response.ok ? await response.json() : null;
956
958
  offerCache = { at: Date.now(), value: body?.offer ?? null };
@@ -1500,7 +1502,7 @@ export function createLocalApiServer(dataDir, token, options = {}) {
1500
1502
  const { capGb } = parseBody(rollingCapSchema, await readJsonBody(request));
1501
1503
  const { cloudToken, DEFAULT_CLOUD_URL } = await import("./cloud.js");
1502
1504
  const token = await cloudToken(dataDir);
1503
- const base = (process.env.VAULTLINE_CLOUD_URL ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
1505
+ const base = (envVar("CLOUD_URL") ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
1504
1506
  const pushed = await fetch(`${base}/v1/cloud/rolling`, {
1505
1507
  method: "PUT",
1506
1508
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
@@ -1596,11 +1598,11 @@ export function createLocalApiServer(dataDir, token, options = {}) {
1596
1598
  remembered = await rememberRecoveryPhrase(dataDir, config.vaultId, phrase);
1597
1599
  }
1598
1600
  catch (error) {
1599
- notes.push(`This machine's keystore refused the phrase (${error instanceof Error ? error.message.split("\n")[0] : "unknown error"}). The background service will need VAULTLINE_RECOVERY_PHRASE in its environment.`);
1601
+ notes.push(`This machine's keystore refused the phrase (${error instanceof Error ? error.message.split("\n")[0] : "unknown error"}). The background service will need SEALKEEP_RECOVERY_PHRASE in its environment.`);
1600
1602
  }
1601
1603
  }
1602
1604
  else {
1603
- notes.push("The phrase was not stored on this machine, so the background service cannot seal sessions until VAULTLINE_RECOVERY_PHRASE is in its environment.");
1605
+ notes.push("The phrase was not stored on this machine, so the background service cannot seal sessions until SEALKEEP_RECOVERY_PHRASE is in its environment.");
1604
1606
  }
1605
1607
  if (password) {
1606
1608
  // Failure may not cost the person this response: the vault already
@@ -1748,7 +1750,7 @@ export function createLocalApiServer(dataDir, token, options = {}) {
1748
1750
  // verified remote copy it would never free a byte either — see the
1749
1751
  // note on `environment` in service.ts. serviceUnitEnvironment adds
1750
1752
  // the secret-backend choice, without which the daemon cannot unlock.
1751
- environment: serviceUnitEnvironment(config.remoteStorage ? { VAULTLINE_ENABLE_SIGNER: "1" } : {})
1753
+ environment: serviceUnitEnvironment(config.remoteStorage ? { SEALKEEP_ENABLE_SIGNER: "1" } : {})
1752
1754
  }));
1753
1755
  return json(response, outcome.installed ? 201 : 200, {
1754
1756
  service: { kind: outcome.kind, installed: outcome.installed, alreadyInstalled: false, path: outcome.path, ranCommands: outcome.ranCommands, note: outcome.note ?? null },
@@ -30,7 +30,7 @@ export declare function mcpServerSpec(dataDir: string, executable?: string): {
30
30
  command: string;
31
31
  args: string[];
32
32
  env: {
33
- VAULTLINE_DATA_DIR: string;
33
+ SEALKEEP_DATA_DIR: string;
34
34
  };
35
35
  };
36
36
  /**
@@ -10,7 +10,7 @@ export function mcpServerSpec(dataDir, executable = "vaultline") {
10
10
  return {
11
11
  command: executable,
12
12
  args: ["mcp"],
13
- env: { VAULTLINE_DATA_DIR: dataDir }
13
+ env: { SEALKEEP_DATA_DIR: dataDir }
14
14
  };
15
15
  }
16
16
  /**
@@ -44,7 +44,7 @@ async function installForClaude(dataDir, exec = run) {
44
44
  try {
45
45
  await exec("claude", [
46
46
  "mcp", "add", "--transport", "stdio", "--scope", "user", "vaultline",
47
- "--env", `VAULTLINE_DATA_DIR=${dataDir}`,
47
+ "--env", `SEALKEEP_DATA_DIR=${dataDir}`,
48
48
  "--", spec.command, ...spec.args
49
49
  ]);
50
50
  return { installed: true, detail: "registered with Claude Code — ask it to search your sessions" };
@@ -74,7 +74,7 @@ async function installForCodex(dataDir, home) {
74
74
  `args = ${JSON.stringify(spec.args)}`,
75
75
  "",
76
76
  "[mcp_servers.vaultline.env]",
77
- `VAULTLINE_DATA_DIR = ${JSON.stringify(dataDir)}`,
77
+ `SEALKEEP_DATA_DIR = ${JSON.stringify(dataDir)}`,
78
78
  ""
79
79
  ].join("\n");
80
80
  await mkdir(dirname(fragmentPath), { recursive: true });
package/dist/src/mcp.js CHANGED
@@ -4,12 +4,13 @@ import { z } from "zod";
4
4
  import { archiveFile, defaultDataDir, listArchives, previewRetention, vaultStatus } from "./vault.js";
5
5
  import { restoreArchive } from "./restore.js";
6
6
  import { providers } from "./control-plane.js";
7
- const dataDir = process.env.VAULTLINE_DATA_DIR ?? defaultDataDir();
8
- const phrase = process.env.VAULTLINE_RECOVERY_PHRASE;
7
+ import { envVar } from "./env.js";
8
+ const dataDir = envVar("DATA_DIR") ?? defaultDataDir();
9
+ const phrase = envVar("RECOVERY_PHRASE");
9
10
  const server = new McpServer({ name: "vaultline", version: "0.1.0" });
10
11
  function text(value) { return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] }; }
11
12
  function requirePhrase() { if (!phrase)
12
- throw new Error("VAULTLINE_RECOVERY_PHRASE is required for archive and recovery actions"); return phrase; }
13
+ throw new Error("SEALKEEP_RECOVERY_PHRASE is required for archive and recovery actions"); return phrase; }
13
14
  server.registerTool("vaultline_status", {
14
15
  title: "Sealkeep status",
15
16
  description: "Read local Sealkeep health, archive count, and encrypted storage usage. No session content is returned.",
@@ -1,9 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { platform } from "node:os";
3
+ import { envVar } from "./env.js";
3
4
  const run = (file, args) => new Promise((resolve) => execFile(file, args, () => resolve()));
4
5
  /** Off by an explicit opt-out, and off automatically where there is no desktop session. */
5
6
  export function notificationsEnabled(env = process.env, target = platform()) {
6
- if (env.VAULTLINE_NOTIFICATIONS === "off")
7
+ if (envVar("NOTIFICATIONS", env) === "off")
7
8
  return false;
8
9
  if (target === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
9
10
  return false;
@@ -116,7 +116,7 @@ export async function offloadArchives(dataDir, options = {}) {
116
116
  /**
117
117
  * Downloads an archive's ciphertext from the bucket it was offloaded to.
118
118
  *
119
- * VAULTLINE_ENABLE_SIGNER gates *uploading*, and it is off unless the installed
119
+ * SEALKEEP_ENABLE_SIGNER gates *uploading*, and it is off unless the installed
120
120
  * service turns it on. Reading back an archive in order to restore it is not
121
121
  * uploading, and refusing it on that flag would mean an offloaded archive could
122
122
  * not be recovered by the person who owns it — which would turn a space-saving
@@ -191,7 +191,7 @@ async function fetchCiphertext(dataDir, record, client) {
191
191
  const resolved = client ?? await uploadClientFromStore(dataDir, config.vaultId, target, {}, undefined);
192
192
  const lease = createActiveLease(target, {
193
193
  archiveId: record.id, ciphertextSha256: record.cipher.ciphertextSha256, bytes: record.cipher.storedBytes
194
- }, { ...process.env, VAULTLINE_ENABLE_SIGNER: "1" });
194
+ }, { ...process.env, SEALKEEP_ENABLE_SIGNER: "1" });
195
195
  const readable = resolved;
196
196
  if (typeof readable.download !== "function") {
197
197
  fail("provider_unsupported", `${target.provider} cannot be read back by this build, so this archive cannot be fetched`);
@@ -257,7 +257,7 @@ export async function checkRemoteCopy(dataDir, archiveId, client) {
257
257
  const resolved = client ?? await uploadClientFromStore(dataDir, config.vaultId, target, {}, undefined);
258
258
  const lease = createActiveLease(target, {
259
259
  archiveId: record.id, ciphertextSha256: record.cipher.ciphertextSha256, bytes: record.cipher.storedBytes
260
- }, { ...process.env, VAULTLINE_ENABLE_SIGNER: "1" });
260
+ }, { ...process.env, SEALKEEP_ENABLE_SIGNER: "1" });
261
261
  const probing = resolved;
262
262
  if (typeof probing.head !== "function")
263
263
  fail("provider_unsupported", `${target.provider} cannot be probed by this build`);
@@ -16,7 +16,7 @@ import type { ProviderKind, ProviderUploadClient, UploadLease } from "../control
16
16
  * into the OS keystore via secrets.ts and never into a config file.
17
17
  */
18
18
  export declare const GDRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
19
- /** Placeholder until a first-party client id ships. Point VAULTLINE_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
19
+ /** Placeholder until a first-party client id ships. Point SEALKEEP_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
20
20
  export declare const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-vaultline-placeholder.apps.googleusercontent.com";
21
21
  export declare function gdriveClientId(env?: NodeJS.ProcessEnv): string;
22
22
  export declare const GDRIVE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createServer } from "node:http";
4
4
  import { fail, VaultlineError } from "../errors.js";
5
+ import { envVar } from "../env.js";
5
6
  /**
6
7
  * Google Drive as a bring-your-own storage provider (Drive API v3).
7
8
  *
@@ -19,10 +20,10 @@ import { fail, VaultlineError } from "../errors.js";
19
20
  * into the OS keystore via secrets.ts and never into a config file.
20
21
  */
21
22
  export const GDRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
22
- /** Placeholder until a first-party client id ships. Point VAULTLINE_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
23
+ /** Placeholder until a first-party client id ships. Point SEALKEEP_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
23
24
  export const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-vaultline-placeholder.apps.googleusercontent.com";
24
25
  export function gdriveClientId(env = process.env) {
25
- return env.VAULTLINE_GDRIVE_CLIENT_ID?.trim() || GDRIVE_CLIENT_ID_PLACEHOLDER;
26
+ return envVar("GDRIVE_CLIENT_ID", env)?.trim() || GDRIVE_CLIENT_ID_PLACEHOLDER;
26
27
  }
27
28
  export const GDRIVE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
28
29
  export const GDRIVE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -18,7 +18,7 @@ function isGdriveCredentials(credentials) {
18
18
  */
19
19
  export function createUploadClient(config, credentials, overrides = {}, env = process.env) {
20
20
  if (!signerEnabled(env))
21
- fail("signer_not_configured", "Uploads are disabled. Set VAULTLINE_ENABLE_SIGNER=1 to enable a configured provider client.", { provider: config.provider });
21
+ fail("signer_not_configured", "Uploads are disabled. Set SEALKEEP_ENABLE_SIGNER=1 to enable a configured provider client.", { provider: config.provider });
22
22
  if (config.provider === "gdrive") {
23
23
  if (!isGdriveCredentials(credentials))
24
24
  fail("invalid_argument", "Google Drive needs an OAuth credential. Run: sealkeep storage connect gdrive");
@@ -29,7 +29,7 @@ export async function rehydrateSession(dataDir, target, phrase, options = {}) {
29
29
  return { rehydrated: false, reason: "already-present" };
30
30
  }
31
31
  if (!phrase)
32
- return { rehydrated: false, reason: "no-phrase", note: "No recovery phrase is available to this machine (keystore or VAULTLINE_RECOVERY_PHRASE), so the sealed copy stays sealed." };
32
+ return { rehydrated: false, reason: "no-phrase", note: "No recovery phrase is available to this machine (keystore or SEALKEEP_RECOVERY_PHRASE), so the sealed copy stays sealed." };
33
33
  try {
34
34
  const outcome = await restoreArchive(dataDir, newest.id, phrase, { native: true, overwrite: "refuse", home: options.home });
35
35
  await recordAudit(dataDir, "archive.restore", "allowed", { archiveId: newest.id, path: outcome.output, rehydrated: true });