@seekrit/cli 2.0.0 → 2.1.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/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from "node:child_process";
3
2
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
3
  import { z } from "zod";
5
4
  import { Command } from "commander";
@@ -7,9 +6,10 @@ import { arch, homedir, hostname, platform, tmpdir, userInfo } from "node:os";
7
6
  import { basename, dirname, join, parse, resolve } from "node:path";
8
7
  import { createInterface } from "node:readline";
9
8
  import { Writable } from "node:stream";
10
- import { createHash } from "node:crypto";
9
+ import { createHash, randomBytes } from "node:crypto";
11
10
  import { connect } from "node:net";
12
11
  import { fileURLToPath } from "node:url";
12
+ import { spawn, spawnSync } from "node:child_process";
13
13
  import { request } from "node:http";
14
14
  /** Default lifetime of a published bundle (7 days), in seconds. */
15
15
  const POLICY_DEFAULT_TTL_SECONDS = 604800;
@@ -5700,7 +5700,7 @@ async function createAgentTaskToken() {
5700
5700
  }
5701
5701
  //#endregion
5702
5702
  //#region package.json
5703
- var version = "2.0.0";
5703
+ var version = "2.1.0";
5704
5704
  //#endregion
5705
5705
  //#region ../../packages/api-client/src/index.ts
5706
5706
  /**
@@ -15102,6 +15102,303 @@ function registerRotationCommands(program) {
15102
15102
  });
15103
15103
  }
15104
15104
  //#endregion
15105
+ //#region src/run-command.ts
15106
+ /** Keep signal handlers installed from provisioning through asynchronous cleanup. */
15107
+ async function runCommand(cmd, args, prepare, cleanup) {
15108
+ let child;
15109
+ let forwarded;
15110
+ let result = { code: 1 };
15111
+ const stragglers = /* @__PURE__ */ new Set();
15112
+ const collect = () => {
15113
+ if (process.platform === "win32" || !child?.pid) return;
15114
+ for (const pid of teardownTargets(child.pid)) stragglers.add(pid);
15115
+ };
15116
+ const forward = (signal) => {
15117
+ forwarded ??= signal;
15118
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
15119
+ collect();
15120
+ child.kill(signal);
15121
+ for (const pid of stragglers) try {
15122
+ process.kill(pid, signal);
15123
+ } catch {}
15124
+ };
15125
+ const signals = [
15126
+ "SIGINT",
15127
+ "SIGTERM",
15128
+ "SIGHUP",
15129
+ "SIGQUIT"
15130
+ ];
15131
+ for (const signal of signals) process.on(signal, forward);
15132
+ try {
15133
+ const env = await prepare(() => forwarded !== void 0);
15134
+ if (!forwarded) {
15135
+ const running = spawn(cmd, args, {
15136
+ stdio: "inherit",
15137
+ env
15138
+ });
15139
+ child = running;
15140
+ result = await new Promise((resolve, reject) => {
15141
+ running.once("exit", (code, signal) => resolve({
15142
+ code: code ?? 1,
15143
+ signal: signal ?? void 0
15144
+ }));
15145
+ running.once("error", (err) => reject(/* @__PURE__ */ new Error(`failed to start ${cmd}: ${err.message}`)));
15146
+ });
15147
+ }
15148
+ } finally {
15149
+ try {
15150
+ if (forwarded) {
15151
+ collect();
15152
+ await reapStragglers(stragglers, forwarded);
15153
+ }
15154
+ } finally {
15155
+ try {
15156
+ if (!await cleanup() && result.code === 0) result.code = 1;
15157
+ } finally {
15158
+ for (const signal of signals) process.off(signal, forward);
15159
+ }
15160
+ }
15161
+ }
15162
+ if (!child && forwarded) result.signal = forwarded;
15163
+ return result;
15164
+ }
15165
+ /**
15166
+ * Every live process `seekrit run` is responsible for tearing down, from a
15167
+ * single `ps(1)` snapshot: each descendant of `root`, plus — when we lead our
15168
+ * own process group — every other member of that group.
15169
+ *
15170
+ * Two mechanisms, because each covers the other's blind spot:
15171
+ *
15172
+ * - The parent-chain walk reaches a process that left our group via
15173
+ * setsid(2)/setpgid(2), which no group-wide kill and no tty-generated signal
15174
+ * can touch. But it loses anything whose chain up to `root` has already
15175
+ * broken — teardown kills the middle layers first, and an orphan reparents to
15176
+ * init, out of reach of any walk down from `root`.
15177
+ * - Group membership survives exactly that: a pgid outlives the parent that
15178
+ * passed it down. But it misses the ones that left the group.
15179
+ *
15180
+ * The group half applies only when our pid *is* our pgid, which is the shell-job
15181
+ * case: the shell put this command in a new group, so every other member of it
15182
+ * descends from us. Nested inside a shell script or a CI runner we are not the
15183
+ * leader and the group holds processes we did not start, so there we use the
15184
+ * walk alone.
15185
+ *
15186
+ * Zombies are skipped: they are already dead and waiting to be reaped, so
15187
+ * counting them would stall teardown for the full grace period and then report
15188
+ * a kill that did nothing. Returns nothing if `ps` is unavailable; callers fall
15189
+ * back to signalling the immediate child.
15190
+ */
15191
+ function teardownTargets(root) {
15192
+ const ps = spawnSync("ps", [
15193
+ "-A",
15194
+ "-o",
15195
+ "pid=,ppid=,pgid=,stat="
15196
+ ], { encoding: "utf8" });
15197
+ if (ps.status !== 0 || typeof ps.stdout !== "string") return [];
15198
+ const childrenOf = /* @__PURE__ */ new Map();
15199
+ const groupOf = /* @__PURE__ */ new Map();
15200
+ const isId = (value) => value !== void 0 && Number.isInteger(value);
15201
+ for (const line of ps.stdout.split("\n")) {
15202
+ const [pid, ppid, pgid, stat] = line.trim().split(/\s+/, 4);
15203
+ const [id, parent, group] = [
15204
+ pid,
15205
+ ppid,
15206
+ pgid
15207
+ ].map(Number);
15208
+ if (!isId(id) || !isId(parent) || !isId(group)) continue;
15209
+ if (stat?.startsWith("Z")) continue;
15210
+ const siblings = childrenOf.get(parent);
15211
+ if (siblings) siblings.push(id);
15212
+ else childrenOf.set(parent, [id]);
15213
+ groupOf.set(id, group);
15214
+ }
15215
+ const found = /* @__PURE__ */ new Set();
15216
+ const queue = [root];
15217
+ const seen = new Set(queue);
15218
+ for (let pid = queue.shift(); pid !== void 0; pid = queue.shift()) for (const kid of childrenOf.get(pid) ?? []) {
15219
+ if (seen.has(kid)) continue;
15220
+ seen.add(kid);
15221
+ found.add(kid);
15222
+ queue.push(kid);
15223
+ }
15224
+ if (groupOf.get(process.pid) === process.pid) {
15225
+ for (const [pid, group] of groupOf) if (group === process.pid) found.add(pid);
15226
+ }
15227
+ found.delete(process.pid);
15228
+ found.delete(root);
15229
+ return [...found];
15230
+ }
15231
+ /** Whether `pid` still exists — signal 0 tests reachability, delivers nothing. */
15232
+ function isAlive(pid) {
15233
+ try {
15234
+ process.kill(pid, 0);
15235
+ return true;
15236
+ } catch {
15237
+ return false;
15238
+ }
15239
+ }
15240
+ /** How long a straggler gets to finish shutting down before SIGKILL. */
15241
+ const STRAGGLER_GRACE_MS = 2e3;
15242
+ /**
15243
+ * Once the command has exited, make sure nothing it started outlives us.
15244
+ *
15245
+ * Anything still alive here has ignored the signal we forwarded *and* lost the
15246
+ * parent that launched it, and we are the last process that knows its pid:
15247
+ * the tty won't hang it up (it is no longer in the foreground group, and the
15248
+ * shell has moved on), so it survives until the machine reboots. That is how
15249
+ * `tsx` and the server under it stacked up for days.
15250
+ *
15251
+ * So escalate rather than trust. SIGTERM first — it costs no extra time and is
15252
+ * a rung the SIGINT-only handlers common in dev servers don't catch — then
15253
+ * SIGKILL whatever is left after the grace period. Nothing is killed unless it
15254
+ * was asked to stop first and declined, and this runs only when we forwarded a
15255
+ * termination signal: a command that exits on its own having deliberately left a
15256
+ * daemon behind keeps working.
15257
+ */
15258
+ async function reapStragglers(pids, signal) {
15259
+ let live = [...pids].filter(isAlive);
15260
+ if (live.length === 0) return;
15261
+ for (const pid of live) try {
15262
+ process.kill(pid, "SIGTERM");
15263
+ } catch {}
15264
+ const deadline = Date.now() + STRAGGLER_GRACE_MS;
15265
+ while (live.length > 0 && Date.now() < deadline) {
15266
+ await new Promise((resolve) => setTimeout(resolve, 50));
15267
+ live = live.filter(isAlive);
15268
+ }
15269
+ if (live.length === 0) return;
15270
+ console.error(`seekrit: force-killed ${live.length} leftover process(es) that ignored ${signal} and SIGTERM (${live.join(", ")})`);
15271
+ for (const pid of live) try {
15272
+ process.kill(pid, "SIGKILL");
15273
+ } catch {}
15274
+ }
15275
+ //#endregion
15276
+ //#region src/run-leases.ts
15277
+ /** Validate all flags before contacting the API or creating any database roles. */
15278
+ function parseRunLeaseOptions(bindings = [], ttl) {
15279
+ if (!bindings.length) {
15280
+ if (ttl !== void 0) throw new Error("--lease-ttl requires --lease VAR=target");
15281
+ return;
15282
+ }
15283
+ const names = /* @__PURE__ */ new Set();
15284
+ const parsed = bindings.map((binding) => {
15285
+ const eq = binding.indexOf("=");
15286
+ const variable = binding.slice(0, eq);
15287
+ const target = binding.slice(eq + 1).trim();
15288
+ if (eq < 1 || !secretNameSchema.safeParse(variable).success || !target) throw new Error("--lease requires VAR=target (for example DATABASE_URL=production)");
15289
+ const key = process.platform === "win32" ? variable.toUpperCase() : variable;
15290
+ if (names.has(key)) throw new Error(`duplicate --lease variable ${variable}`);
15291
+ names.add(key);
15292
+ return {
15293
+ variable,
15294
+ target
15295
+ };
15296
+ });
15297
+ const match = /^(\d+)\s*([smhd]?)$/.exec((ttl ?? "30m").trim());
15298
+ const ttlSeconds = match ? Number(match[1]) * ({
15299
+ "": 1,
15300
+ s: 1,
15301
+ m: 60,
15302
+ h: 3600,
15303
+ d: 86400
15304
+ }[match[2] ?? ""] ?? 1) : NaN;
15305
+ if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > 604800) throw new Error("--lease-ttl must be between 1m and 7d (for example 30m)");
15306
+ return {
15307
+ bindings: parsed,
15308
+ ttlSeconds
15309
+ };
15310
+ }
15311
+ async function prepareRunLeases(options, orgSlug, envFiles = [".env"]) {
15312
+ const dotenvVars = {};
15313
+ overlayEnvFiles(dotenvVars, {}, envFiles);
15314
+ const ctx = tryBuildContext(dotenvVars);
15315
+ if (!ctx) throw new Error("--lease requires authentication; run `seekrit login`");
15316
+ const client = new SeekritClient({
15317
+ baseUrl: ctx.apiUrl,
15318
+ auth: ctx.auth,
15319
+ fetch: (input, init) => fetch(input, {
15320
+ ...init,
15321
+ signal: AbortSignal.timeout(15e3)
15322
+ })
15323
+ });
15324
+ return new RunLeases(client, (await resolveOrg({
15325
+ ...ctx,
15326
+ client
15327
+ }, orgSlug)).id, options);
15328
+ }
15329
+ /** Own every successfully minted lease, including partial setup failures. */
15330
+ var RunLeases = class {
15331
+ client;
15332
+ orgId;
15333
+ options;
15334
+ active = [];
15335
+ constructor(client, orgId, options) {
15336
+ this.client = client;
15337
+ this.orgId = orgId;
15338
+ this.options = options;
15339
+ }
15340
+ async mint(interrupted) {
15341
+ const values = Object.create(null);
15342
+ if (interrupted()) return values;
15343
+ const { targets } = await this.client.listLeaseTargets(this.orgId);
15344
+ const selected = this.options.bindings.map((binding) => {
15345
+ const matches = targets.filter((t) => t.id === binding.target || t.name === binding.target);
15346
+ const target = matches[0];
15347
+ if (matches.length !== 1 || !target) throw new Error(`--lease target "${binding.target}" is missing or ambiguous; use its ID`);
15348
+ if (target.provider !== "postgres") throw new Error(`--lease target "${target.name}" must be Postgres`);
15349
+ return {
15350
+ binding,
15351
+ target
15352
+ };
15353
+ });
15354
+ for (const { binding, target } of selected) {
15355
+ if (interrupted()) break;
15356
+ const username = `tmp_${randomBytes(12).toString("hex")}`;
15357
+ const { password, verifier } = await generatePostgresCredential();
15358
+ if (interrupted()) break;
15359
+ let minted;
15360
+ try {
15361
+ minted = await this.client.mintLease(this.orgId, {
15362
+ provider: "postgres",
15363
+ targetId: target.id,
15364
+ roleName: username,
15365
+ verifier,
15366
+ ttlSeconds: this.options.ttlSeconds
15367
+ });
15368
+ } catch {
15369
+ throw new Error(`could not create temporary user ${username} on target ${target.name}; command not started. If provisioning completed, broker expiry will attempt cleanup after ${this.options.ttlSeconds}s; check the lease ledger`);
15370
+ }
15371
+ const { lease, connection } = minted;
15372
+ this.active.push({
15373
+ id: lease.id,
15374
+ username,
15375
+ expiresAt: connection.expiresAt
15376
+ });
15377
+ console.error(`seekrit: created temporary user ${username} on ${target.name} · lease ${lease.id} · expires ${connection.expiresAt}`);
15378
+ const url = new URL("postgresql://localhost");
15379
+ url.hostname = connection.host.includes(":") && !connection.host.startsWith("[") ? `[${connection.host}]` : connection.host;
15380
+ url.port = String(connection.port);
15381
+ url.username = connection.username;
15382
+ url.password = password;
15383
+ url.pathname = `/${encodeURIComponent(connection.database)}`;
15384
+ values[binding.variable] = url.href;
15385
+ console.error(`seekrit: injected ${binding.variable}`);
15386
+ }
15387
+ return values;
15388
+ }
15389
+ async revoke() {
15390
+ let ok = true;
15391
+ for (const lease of this.active.splice(0).reverse()) try {
15392
+ await this.client.revokeLease(this.orgId, lease.id);
15393
+ console.error(`seekrit: revoked temporary user ${lease.username} · lease ${lease.id}`);
15394
+ } catch {
15395
+ ok = false;
15396
+ console.error(`seekrit: FAILED to revoke temporary user ${lease.username} · lease ${lease.id}. Run seekrit pg revoke ${lease.id} --org ${this.orgId}; broker expiry will retry at/after ${lease.expiresAt}`);
15397
+ }
15398
+ return ok;
15399
+ }
15400
+ };
15401
+ //#endregion
15105
15402
  //#region src/ssh.ts
15106
15403
  /**
15107
15404
  * `seekrit ssh` — temporary SSH access via short-lived certificates (Vault-style
@@ -16468,167 +16765,37 @@ async function materializeForRun(options) {
16468
16765
  };
16469
16766
  }
16470
16767
  }
16471
- /**
16472
- * Every live process `seekrit run` is responsible for tearing down, from a
16473
- * single `ps(1)` snapshot: each descendant of `root`, plus — when we lead our
16474
- * own process group — every other member of that group.
16475
- *
16476
- * Two mechanisms, because each covers the other's blind spot:
16477
- *
16478
- * - The parent-chain walk reaches a process that left our group via
16479
- * setsid(2)/setpgid(2), which no group-wide kill and no tty-generated signal
16480
- * can touch. But it loses anything whose chain up to `root` has already
16481
- * broken — teardown kills the middle layers first, and an orphan reparents to
16482
- * init, out of reach of any walk down from `root`.
16483
- * - Group membership survives exactly that: a pgid outlives the parent that
16484
- * passed it down. But it misses the ones that left the group.
16485
- *
16486
- * The group half applies only when our pid *is* our pgid, which is the shell-job
16487
- * case: the shell put this command in a new group, so every other member of it
16488
- * descends from us. Nested inside a shell script or a CI runner we are not the
16489
- * leader and the group holds processes we did not start, so there we use the
16490
- * walk alone.
16491
- *
16492
- * Zombies are skipped: they are already dead and waiting to be reaped, so
16493
- * counting them would stall teardown for the full grace period and then report
16494
- * a kill that did nothing. Returns nothing if `ps` is unavailable; callers fall
16495
- * back to signalling the immediate child.
16496
- */
16497
- function teardownTargets(root) {
16498
- const ps = spawnSync("ps", [
16499
- "-A",
16500
- "-o",
16501
- "pid=,ppid=,pgid=,stat="
16502
- ], { encoding: "utf8" });
16503
- if (ps.status !== 0 || typeof ps.stdout !== "string") return [];
16504
- const childrenOf = /* @__PURE__ */ new Map();
16505
- const groupOf = /* @__PURE__ */ new Map();
16506
- const isId = (value) => value !== void 0 && Number.isInteger(value);
16507
- for (const line of ps.stdout.split("\n")) {
16508
- const [pid, ppid, pgid, stat] = line.trim().split(/\s+/, 4);
16509
- const [id, parent, group] = [
16510
- pid,
16511
- ppid,
16512
- pgid
16513
- ].map(Number);
16514
- if (!isId(id) || !isId(parent) || !isId(group)) continue;
16515
- if (stat?.startsWith("Z")) continue;
16516
- const siblings = childrenOf.get(parent);
16517
- if (siblings) siblings.push(id);
16518
- else childrenOf.set(parent, [id]);
16519
- groupOf.set(id, group);
16520
- }
16521
- const found = /* @__PURE__ */ new Set();
16522
- const queue = [root];
16523
- const seen = new Set(queue);
16524
- for (let pid = queue.shift(); pid !== void 0; pid = queue.shift()) for (const kid of childrenOf.get(pid) ?? []) {
16525
- if (seen.has(kid)) continue;
16526
- seen.add(kid);
16527
- found.add(kid);
16528
- queue.push(kid);
16529
- }
16530
- if (groupOf.get(process.pid) === process.pid) {
16531
- for (const [pid, group] of groupOf) if (group === process.pid) found.add(pid);
16532
- }
16533
- found.delete(process.pid);
16534
- found.delete(root);
16535
- return [...found];
16536
- }
16537
- /** Whether `pid` still exists — signal 0 tests reachability, delivers nothing. */
16538
- function isAlive(pid) {
16539
- try {
16540
- process.kill(pid, 0);
16541
- return true;
16542
- } catch {
16543
- return false;
16544
- }
16545
- }
16546
- /** How long a straggler gets to finish shutting down before SIGKILL. */
16547
- const STRAGGLER_GRACE_MS = 2e3;
16548
- /**
16549
- * Once the command has exited, make sure nothing it started outlives us.
16550
- *
16551
- * Anything still alive here has ignored the signal we forwarded *and* lost the
16552
- * parent that launched it, and we are the last process that knows its pid:
16553
- * the tty won't hang it up (it is no longer in the foreground group, and the
16554
- * shell has moved on), so it survives until the machine reboots. That is how
16555
- * `tsx` and the server under it stacked up for days.
16556
- *
16557
- * So escalate rather than trust. SIGTERM first — it costs no extra time and is
16558
- * a rung the SIGINT-only handlers common in dev servers don't catch — then
16559
- * SIGKILL whatever is left after the grace period. Nothing is killed unless it
16560
- * was asked to stop first and declined, and this runs only when we forwarded a
16561
- * termination signal: a command that exits on its own having deliberately left a
16562
- * daemon behind keeps working.
16563
- */
16564
- async function reapStragglers(pids, signal) {
16565
- let live = [...pids].filter(isAlive);
16566
- if (live.length === 0) return;
16567
- for (const pid of live) try {
16568
- process.kill(pid, "SIGTERM");
16569
- } catch {}
16570
- const deadline = Date.now() + STRAGGLER_GRACE_MS;
16571
- while (live.length > 0 && Date.now() < deadline) {
16572
- await new Promise((resolve) => setTimeout(resolve, 50));
16573
- live = live.filter(isAlive);
16574
- }
16575
- if (live.length === 0) return;
16576
- console.error(`seekrit: force-killed ${live.length} leftover process(es) that ignored ${signal} and SIGTERM (${live.join(", ")})`);
16577
- for (const pid of live) try {
16578
- process.kill(pid, "SIGKILL");
16579
- } catch {}
16580
- }
16581
- program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
16768
+ program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--lease <VAR=target>", "inject a temporary Postgres connection URL (repeatable)", collectList).option("--lease-ttl <duration>", "maximum lease lifetime, 1m–7d (default: 30m)").option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
16582
16769
  const [cmd, ...args] = commandParts;
16583
16770
  if (!cmd) fail("no command given");
16771
+ const leaseOptions = parseRunLeaseOptions(options.lease, options.leaseTtl);
16584
16772
  const { values, provenance, interpolated, unresolvedRefs } = await materializeForRun(options);
16585
- if (options.explain) {
16586
- for (const name of Object.keys(values)) if (process.env[name] !== void 0 && process.env[name] !== values[name]) provenance[name] = "env";
16587
- printExplain(provenance, {
16588
- interpolated,
16589
- unresolved: unresolvedRefs
16590
- });
16591
- }
16592
- const posix = process.platform !== "win32";
16593
- const child = spawn(cmd, args, {
16594
- stdio: "inherit",
16595
- env: {
16773
+ const leases = leaseOptions ? await prepareRunLeases(leaseOptions, options.org, options.envFile) : void 0;
16774
+ const result = await runCommand(cmd, args, async (interrupted) => {
16775
+ const leased = await leases?.mint(interrupted) ?? {};
16776
+ if (options.explain) {
16777
+ for (const name of Object.keys(values)) if (process.env[name] !== void 0 && process.env[name] !== values[name]) provenance[name] = "env";
16778
+ for (const binding of leaseOptions?.bindings ?? []) provenance[binding.variable] = `lease:${binding.target}`;
16779
+ printExplain(provenance, {
16780
+ interpolated,
16781
+ unresolved: unresolvedRefs
16782
+ });
16783
+ }
16784
+ const env = {
16596
16785
  ...values,
16597
16786
  ...process.env
16787
+ };
16788
+ if (process.platform === "win32") {
16789
+ const leasedNames = new Set(Object.keys(leased).map((name) => name.toUpperCase()));
16790
+ for (const name of Object.keys(env)) if (leasedNames.has(name.toUpperCase())) delete env[name];
16598
16791
  }
16599
- });
16600
- const signals = [
16601
- "SIGINT",
16602
- "SIGTERM",
16603
- "SIGHUP",
16604
- "SIGQUIT"
16605
- ];
16606
- const stragglers = /* @__PURE__ */ new Set();
16607
- let forwarded;
16608
- const collect = () => {
16609
- if (!posix || !child.pid) return;
16610
- for (const pid of teardownTargets(child.pid)) stragglers.add(pid);
16611
- };
16612
- const forward = (signal) => {
16613
- if (child.exitCode !== null || child.signalCode !== null) return;
16614
- forwarded ??= signal;
16615
- collect();
16616
- child.kill(signal);
16617
- for (const pid of stragglers) try {
16618
- process.kill(pid, signal);
16619
- } catch {}
16620
- };
16621
- for (const signal of signals) process.on(signal, forward);
16622
- child.on("exit", async (code, signal) => {
16623
- if (forwarded) {
16624
- collect();
16625
- await reapStragglers(stragglers, forwarded);
16626
- }
16627
- for (const s of signals) process.off(s, forward);
16628
- if (signal) process.kill(process.pid, signal);
16629
- else process.exit(code ?? 1);
16630
- });
16631
- child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
16792
+ return {
16793
+ ...env,
16794
+ ...leased
16795
+ };
16796
+ }, () => leases?.revoke() ?? Promise.resolve(true));
16797
+ if (result.signal) process.kill(process.pid, result.signal);
16798
+ else process.exit(result.code);
16632
16799
  });
16633
16800
  program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").option("--with <group=env>", "override one group’s slice", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
16634
16801
  if (![
@@ -16748,7 +16915,7 @@ registerKmsCommands(program);
16748
16915
  registerRotationCommands(program);
16749
16916
  registerRecoveryCommands(program);
16750
16917
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
16751
- const { runMcpServer } = await import("./mcp-D7rsPeBQ.js");
16918
+ const { runMcpServer } = await import("./mcp-DZoBFmDF.js");
16752
16919
  await runMcpServer();
16753
16920
  });
16754
16921
  registerAuditCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "homepage": "https://seekrit.dev/docs/guides/cli",
6
6
  "keywords": [
@@ -46,9 +46,9 @@
46
46
  "@types/node": "^26.6.5",
47
47
  "tsdown": "^0.23.1",
48
48
  "vitest": "^5.0.5",
49
- "@seekrit/crypto": "0.0.1",
50
49
  "@seekrit/api-client": "0.0.1",
51
- "@seekrit/core": "0.0.1"
50
+ "@seekrit/core": "0.0.1",
51
+ "@seekrit/crypto": "0.0.1"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsdown",
@@ -1,6 +1,6 @@
1
1
  import { A as signatureKeyRef, B as generateDek, C as version, D as importSigningKey, E as generateSigningKeyMaterial, F as generateEncryptKeyMaterial, H as parseBranchTtl, I as kmsBlobKeyRef, L as kmsDecrypt, M as generatePostgresCredential, N as generateMysqlCredential, O as importVerifyingKey, P as generateDataKey, R as kmsEncrypt, S as writeProjectConfig, T as parseServiceToken, V as toBase64, _ as getDek, a as kmsResolveRecipient, b as tryBuildContext, c as fetchDecryptedSecrets, d as resolveApp, f as resolveAppEnv, g as resolveOrg, h as resolveGroup, i as kmsResolveKey, j as verifyMessage, k as signMessage, l as fetchDecryptedVersion, m as resolveEnvTarget, n as kmsCallerIdentity, o as syncGrantsForGroupLink, p as resolveBranch, r as kmsRecoverMaterial, s as encryptAndSetSecret, t as ensureM2mAdminToken, u as materializeEnv, v as isTokenAuth, w as createServiceToken, x as setFailThrows, y as principalPublicKeyJwk, z as wrapDek } from "./index.js";
2
- import { spawn } from "node:child_process";
3
2
  import { z } from "zod";
3
+ import { spawn } from "node:child_process";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  //#region src/mcp.ts