@seekrit/cli 1.5.0 → 1.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.
Files changed (2) hide show
  1. package/dist/index.js +579 -194
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5053,7 +5053,7 @@ async function createAgentTaskToken() {
5053
5053
  }
5054
5054
  //#endregion
5055
5055
  //#region package.json
5056
- var version = "1.5.0";
5056
+ var version = "1.6.0";
5057
5057
  //#endregion
5058
5058
  //#region ../../packages/api-client/src/index.ts
5059
5059
  var SeekritApiError = class extends Error {
@@ -7524,7 +7524,7 @@ function registerEnvDiff(env) {
7524
7524
  //#endregion
7525
7525
  //#region src/kms.ts
7526
7526
  /** Collect a repeatable option into a list. */
7527
- function collect$7(value, acc = []) {
7527
+ function collect$8(value, acc = []) {
7528
7528
  acc.push(value);
7529
7529
  return acc;
7530
7530
  }
@@ -7592,7 +7592,7 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
7592
7592
  }
7593
7593
  function registerKmsCommands(program) {
7594
7594
  const kms = program.command("kms").description("managed keys for application-layer encryption & signing (client-side)");
7595
- kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$7, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$7, []).action(async (options) => {
7595
+ kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$8, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$8, []).action(async (options) => {
7596
7596
  if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
7597
7597
  if (options.app && options.group) fail("pass at most one of --app or --group");
7598
7598
  const ctx = buildContext();
@@ -7814,7 +7814,7 @@ function registerKmsCommands(program) {
7814
7814
  //#endregion
7815
7815
  //#region src/recovery.ts
7816
7816
  /** Collect a repeatable option into a list. */
7817
- function collect$6(value, acc = []) {
7817
+ function collect$7(value, acc = []) {
7818
7818
  acc.push(value);
7819
7819
  return acc;
7820
7820
  }
@@ -7911,7 +7911,7 @@ function registerRecoveryCommands(program) {
7911
7911
  for (const cst of status.custodians) console.log(` - ${cst.label ?? cst.principalId} (${cst.principalType}, share #${cst.shareIndex})`);
7912
7912
  if (status.coverage.unprotectedEnvIds.length > 0) console.log(`${status.coverage.unprotectedEnvIds.length} environment(s) not yet protected — run \`seekrit recovery sync\``);
7913
7913
  });
7914
- recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7914
+ recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$7, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7915
7915
  const ctx = buildContext();
7916
7916
  const org = await resolveOrg(ctx, options.org);
7917
7917
  const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
@@ -7929,7 +7929,7 @@ function registerRecoveryCommands(program) {
7929
7929
  const { wrapped, skipped } = await syncRecoveryGrants(ctx, (await resolveOrg(ctx, options.org)).id);
7930
7930
  console.error(`recovery-protected ${wrapped} environment(s); skipped ${skipped} you cannot decrypt`);
7931
7931
  });
7932
- recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7932
+ recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$7, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
7933
7933
  const ctx = buildContext();
7934
7934
  const org = await resolveOrg(ctx, options.org);
7935
7935
  const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
@@ -9070,7 +9070,7 @@ function formatSecrets(values, format) {
9070
9070
  * `buildContext()` in any of them silently breaks the promise the feature makes.
9071
9071
  */
9072
9072
  /** Collect a repeatable option into a list. */
9073
- function collect$5(value, acc = []) {
9073
+ function collect$6(value, acc = []) {
9074
9074
  acc.push(value);
9075
9075
  return acc;
9076
9076
  }
@@ -9343,7 +9343,7 @@ function registerArchiveCommands(program) {
9343
9343
  } catch {}
9344
9344
  fail("none of the recovery shares in this archive unwrap with that key");
9345
9345
  });
9346
- archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$5).option("-y, --yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
9346
+ archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$6).option("-y, --yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
9347
9347
  if (!options.out && !options.stdout) fail("choose a destination: --out <dir> to write files, or --stdout to print plaintext");
9348
9348
  if (![
9349
9349
  "dotenv",
@@ -11263,7 +11263,7 @@ function parseTtlSeconds$4(input) {
11263
11263
  }[m[2] || "s"] ?? 1);
11264
11264
  }
11265
11265
  /** Collect a repeatable option into an array. */
11266
- function collect$4(value, previous) {
11266
+ function collect$5(value, previous) {
11267
11267
  return [...previous, value];
11268
11268
  }
11269
11269
  /** Parse `readWrite@app` → { role, db } for a custom target. */
@@ -11288,7 +11288,7 @@ function resolveAdminUri(uri) {
11288
11288
  function registerMongoCommands(program) {
11289
11289
  const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
11290
11290
  const target = mongo.command("target").description("manage MongoDB targets");
11291
- target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
11291
+ target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$5, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
11292
11292
  const ctx = buildContext();
11293
11293
  const org = await resolveOrg(ctx, options.org);
11294
11294
  const adminUri = resolveAdminUri(options.uri);
@@ -11450,7 +11450,7 @@ function generateUserName$1(prefix = "tmp") {
11450
11450
  function registerMysqlCommands(program) {
11451
11451
  const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
11452
11452
  const target = mysql.command("target").description("manage provisioning targets");
11453
- target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
11453
+ target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$4, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$4, []).action(async (options) => {
11454
11454
  const ctx = buildContext();
11455
11455
  const org = await resolveOrg(ctx, options.org);
11456
11456
  const executor = options.executor === "remote" ? "remote" : "in_do";
@@ -11553,11 +11553,577 @@ function registerMysqlCommands(program) {
11553
11553
  });
11554
11554
  }
11555
11555
  /** Collect a repeatable option into an array. */
11556
- function collect$3(value, acc) {
11556
+ function collect$4(value, acc) {
11557
11557
  acc.push(value);
11558
11558
  return acc;
11559
11559
  }
11560
11560
  //#endregion
11561
+ //#region src/proxy-binary.ts
11562
+ /**
11563
+ * Fetch and run the `seekrit-proxy` binary without a Rust toolchain.
11564
+ *
11565
+ * The proxy is the strongest answer seekrit has for an untrusted workload — the
11566
+ * agent holds `{{seekrit:NAME}}` and never the key — and it was also the hardest
11567
+ * thing here to *try*, because trying it meant `cargo` and a TOML file. This
11568
+ * module removes the first half: it resolves a prebuilt, checksum-verified
11569
+ * binary for the host platform and execs it, so `npx @seekrit/proxy` and
11570
+ * `seekrit proxy run` behave like the proxy was already installed.
11571
+ *
11572
+ * The logic lives in the CLI (and is re-exported as `@seekrit/cli/proxy-launcher`)
11573
+ * for the same reason the MCP server does: `@seekrit/proxy` is a thin npx
11574
+ * entrypoint over it, and the two must not drift.
11575
+ *
11576
+ * Three properties worth stating, since this downloads and executes code:
11577
+ *
11578
+ * - **The checksum is verified before anything is executed**, against a
11579
+ * `.sha256` fetched from the same release. That is integrity, not provenance —
11580
+ * it proves the bytes match what the release published, which is exactly the
11581
+ * guarantee `install.sh` gives and no more.
11582
+ * - **Nothing is fetched when a binary is already available.** `SEEKRIT_PROXY_BIN`
11583
+ * short-circuits entirely, and a cached download for the same version+target is
11584
+ * reused, so this is a one-time cost per version.
11585
+ * - **Version is pinned, not floating.** A default of `latest` would make two
11586
+ * machines run different proxies from the same command; the pinned constant is
11587
+ * what this CLI was built against, overridable when you want otherwise.
11588
+ */
11589
+ /**
11590
+ * The proxy version this CLI was built against.
11591
+ *
11592
+ * Bumped by release-please when `apps/proxy` releases (an `extra-files` entry in
11593
+ * release-please-config.json), so the pin follows the crate without anyone
11594
+ * remembering to move it.
11595
+ */
11596
+ const PROXY_VERSION = "0.12.0";
11597
+ const BIN = "seekrit-proxy";
11598
+ /**
11599
+ * Host → Rust target triple.
11600
+ *
11601
+ * Linux always resolves to **musl**: that build is statically linked, so one
11602
+ * artifact covers glibc, musl, alpine, and distroless, and there is no libc
11603
+ * detection to get wrong on a machine where `ldd` says something unexpected.
11604
+ */
11605
+ function detectTarget(os = platform(), cpu = arch()) {
11606
+ const machine = cpu === "x64" ? "x86_64" : cpu === "arm64" ? "aarch64" : null;
11607
+ if (!machine) throw new Error(`unsupported architecture "${cpu}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
11608
+ switch (os) {
11609
+ case "linux": return {
11610
+ target: `${machine}-unknown-linux-musl`,
11611
+ exe: ""
11612
+ };
11613
+ case "darwin": return {
11614
+ target: `${machine}-apple-darwin`,
11615
+ exe: ""
11616
+ };
11617
+ case "win32":
11618
+ if (machine !== "x86_64") throw new Error(`no prebuilt seekrit-proxy for ${machine} Windows — set SEEKRIT_PROXY_BIN to a binary you built`);
11619
+ return {
11620
+ target: "x86_64-pc-windows-msvc",
11621
+ exe: ".exe"
11622
+ };
11623
+ default: throw new Error(`unsupported platform "${os}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
11624
+ }
11625
+ }
11626
+ /** `latest` stays `latest`; everything else is normalized to `v<x.y.z>`. */
11627
+ function versionPrefix(version) {
11628
+ if (version === "latest") return "latest";
11629
+ return version.startsWith("v") ? version : `v${version}`;
11630
+ }
11631
+ function resolveVersion(explicit) {
11632
+ return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
11633
+ }
11634
+ function resolveBaseUrl(explicit) {
11635
+ return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
11636
+ }
11637
+ /** Where a resolved binary is kept, keyed so versions and targets never collide. */
11638
+ function proxyBinaryPath(version, target, exe) {
11639
+ return join(defaultCacheDir(), "proxy", versionPrefix(version), target, `${BIN}${exe}`);
11640
+ }
11641
+ async function fetchBytes$1(url) {
11642
+ const res = await fetch(url);
11643
+ if (!res.ok) throw new Error(`GET ${url} → ${res.status} ${res.statusText}`);
11644
+ return new Uint8Array(await res.arrayBuffer());
11645
+ }
11646
+ /**
11647
+ * Ensure a `seekrit-proxy` binary exists locally and return its path.
11648
+ *
11649
+ * Order: an explicit `SEEKRIT_PROXY_BIN`, then a cached download for this
11650
+ * version+target, then a fresh download. A binary already on `PATH` is
11651
+ * deliberately *not* used — silently running a different version than the one
11652
+ * this CLI pins is the kind of surprise that costs an afternoon.
11653
+ */
11654
+ async function resolveProxyBinary(options = {}) {
11655
+ const override = process.env.SEEKRIT_PROXY_BIN;
11656
+ if (override) {
11657
+ if (!existsSync(override)) throw new Error(`SEEKRIT_PROXY_BIN points at ${override}, which does not exist`);
11658
+ return override;
11659
+ }
11660
+ const version = resolveVersion(options.version);
11661
+ const { target, exe } = detectTarget();
11662
+ const dest = proxyBinaryPath(version, target, exe);
11663
+ if (!options.force && version !== "latest" && existsSync(dest)) return dest;
11664
+ const baseUrl = resolveBaseUrl(options.baseUrl);
11665
+ const prefix = versionPrefix(version);
11666
+ const name = `${BIN}-${target}${exe}`;
11667
+ const binUrl = `${baseUrl}/${prefix}/bin/${name}`;
11668
+ const sumUrl = `${binUrl}.sha256`;
11669
+ if (!options.quiet) process.stderr.write(`seekrit: fetching ${BIN} ${prefix} (${target})…\n`);
11670
+ let bytes;
11671
+ let expected;
11672
+ try {
11673
+ [bytes, expected] = await Promise.all([fetchBytes$1(binUrl), fetchBytes$1(sumUrl).then((b) => Buffer.from(b).toString("utf8").trim().split(/\s+/)[0] ?? "")]);
11674
+ } catch (err) {
11675
+ const message = err instanceof Error ? err.message : String(err);
11676
+ throw new Error(`could not download ${BIN} ${prefix} for ${target}: ${message}\n Set SEEKRIT_PROXY_BIN to a binary you already have, or build it from apps/proxy.`);
11677
+ }
11678
+ const actual = createHash("sha256").update(bytes).digest("hex");
11679
+ if (!expected || actual !== expected.toLowerCase()) throw new Error(`checksum mismatch for ${name}: expected ${expected || "(none published)"}, got ${actual}. Refusing to run it.`);
11680
+ const dir = dirname(dest);
11681
+ mkdirSync(dir, { recursive: true });
11682
+ const staging = join(dir, `.${BIN}-${process.pid}-${actual.slice(0, 12)}${exe}`);
11683
+ try {
11684
+ writeFileSync(staging, bytes, { mode: 493 });
11685
+ renameSync(staging, dest);
11686
+ } catch (err) {
11687
+ rmSync(staging, { force: true });
11688
+ throw err;
11689
+ }
11690
+ chmodSync(dest, 493);
11691
+ return dest;
11692
+ }
11693
+ /**
11694
+ * Run the proxy, forwarding stdio, signals, and its exit status.
11695
+ *
11696
+ * The proxy is a long-lived foreground process, so this wrapper has to be
11697
+ * transparent: Node cannot exec-replace itself, and without relaying signals
11698
+ * Node's default SIGINT handler would kill *this* process on Ctrl-C and leave
11699
+ * the proxy running, holding decrypted secrets, with the shell prompt back.
11700
+ */
11701
+ async function runProxyBinary(argv, options = {}) {
11702
+ const bin = await resolveProxyBinary(options);
11703
+ const child = spawn(bin, argv, {
11704
+ stdio: "inherit",
11705
+ env: {
11706
+ ...process.env,
11707
+ ...options.env
11708
+ }
11709
+ });
11710
+ const signals = [
11711
+ "SIGINT",
11712
+ "SIGTERM",
11713
+ "SIGHUP",
11714
+ "SIGQUIT"
11715
+ ];
11716
+ const forward = (signal) => {
11717
+ if (child.exitCode !== null || child.signalCode !== null) return;
11718
+ child.kill(signal);
11719
+ };
11720
+ for (const signal of signals) process.on(signal, forward);
11721
+ return new Promise((resolve, reject) => {
11722
+ child.on("error", (err) => {
11723
+ for (const s of signals) process.off(s, forward);
11724
+ reject(/* @__PURE__ */ new Error(`could not start ${bin}: ${err.message}\n If this is a fresh download, the platform may not match — set SEEKRIT_PROXY_BIN.`));
11725
+ });
11726
+ child.on("exit", (code, signal) => {
11727
+ for (const s of signals) process.off(s, forward);
11728
+ resolve(signal ? 128 + signalNumber(signal) : code ?? 0);
11729
+ });
11730
+ });
11731
+ }
11732
+ /** Signal name → number, for the 128+n exit convention. */
11733
+ function signalNumber(signal) {
11734
+ return {
11735
+ SIGHUP: 1,
11736
+ SIGINT: 2,
11737
+ SIGQUIT: 3,
11738
+ SIGKILL: 9,
11739
+ SIGTERM: 15
11740
+ }[signal] ?? 0;
11741
+ }
11742
+ //#endregion
11743
+ //#region src/nomad.ts
11744
+ /**
11745
+ * `seekrit nomad` — wire seekrit into [HashiCorp Nomad](https://developer.hashicorp.com/nomad)
11746
+ * as a **secret provider plugin**.
11747
+ *
11748
+ * Nomad 1.11 grew a `secret` block in the job specification plus an external
11749
+ * plugin framework: drop an executable in `client.common_plugin_dir/secrets/`,
11750
+ * answer `fingerprint` and `fetch` with JSON on stdout, and jobs name
11751
+ * credentials instead of holding them. Two built-in providers ship (Nomad
11752
+ * Variables and Vault); everything else is a community binary.
11753
+ *
11754
+ * The plugin itself is Rust (`apps/nomad-plugin`), not this CLI, because it runs
11755
+ * as the Nomad agent's user on every client node — bare Linux hosts where
11756
+ * requiring a Node runtime would be a real adoption tax. This command covers the
11757
+ * three things around it that are not the protocol: putting the binary in the
11758
+ * right place, printing the configuration, and proving the install works.
11759
+ *
11760
+ * Three facts about Nomad shaped this file, and **all three fail silently**:
11761
+ *
11762
+ * **The `provider` value is the executable's filename.** Nomad looks for an
11763
+ * executable named exactly what the job's `provider` says. So the binary is
11764
+ * installed as `secrets/seekrit`, not as its artifact name — get it wrong and
11765
+ * the plugin is not registered, with no error and no log line pointing here.
11766
+ *
11767
+ * **Plugins are fingerprinted once, at client startup.** Installing a plugin
11768
+ * into a running agent does nothing until it restarts. So every path here says
11769
+ * so, rather than letting someone conclude the plugin is broken.
11770
+ *
11771
+ * **`common_plugin_dir` is opt-in.** An agent with no `common_plugin_dir` set
11772
+ * scans nothing. `init` prints the stanza because the most likely reason a
11773
+ * correct install does not work is that the agent was never told where to look.
11774
+ */
11775
+ /**
11776
+ * The name a job's `secret` block puts in `provider`, **and** the filename the
11777
+ * binary must have on disk. Nomad ties the two together; they are one constant
11778
+ * here so they cannot drift.
11779
+ */
11780
+ const PROVIDER_NAME = "seekrit";
11781
+ /** The artifact's own name — what the release publishes and install.sh fetches. */
11782
+ const PLUGIN_BIN = "seekrit-nomad-secrets";
11783
+ /**
11784
+ * The plugin version this CLI installs by default.
11785
+ *
11786
+ * Bumped by release-please when `apps/nomad-plugin` releases (an `extra-files`
11787
+ * entry in release-please-config.json), so the pin follows the crate without
11788
+ * anyone remembering to move it — the same arrangement `PROXY_VERSION` has.
11789
+ */
11790
+ const NOMAD_PLUGIN_VERSION = "0.2.0";
11791
+ /** Public artifact host. Mirrors GitHub Releases; public, no egress fees. */
11792
+ const DEFAULT_BASE_URL = "https://nomad.seekrit.dev";
11793
+ /** Where the plugin reads its node config, matching `config::DEFAULT_CONFIG_PATH`. */
11794
+ const DEFAULT_CONFIG_PATH = "/etc/seekrit/nomad.json";
11795
+ /**
11796
+ * Plugin directories to look in when `--plugin-dir` is not given.
11797
+ *
11798
+ * Deliberately a short list of conventional paths rather than a guess: an agent
11799
+ * can put `common_plugin_dir` anywhere, and installing into the wrong directory
11800
+ * produces a plugin that does not exist as far as Nomad is concerned.
11801
+ */
11802
+ const PLUGIN_DIR_CANDIDATES = ["/opt/nomad/plugins", "/var/lib/nomad/plugins"];
11803
+ /** The first candidate that exists, or undefined — never a blind default. */
11804
+ function detectPluginDir(candidates = PLUGIN_DIR_CANDIDATES) {
11805
+ return candidates.find((dir) => existsSync(dir));
11806
+ }
11807
+ /** Where the executable has to end up for `provider = "seekrit"` to find it. */
11808
+ function pluginPath(pluginDir) {
11809
+ return join(pluginDir, "secrets", PROVIDER_NAME);
11810
+ }
11811
+ /** The agent stanza that makes Nomad scan for plugins at all. */
11812
+ function agentStanza(pluginDir) {
11813
+ return [
11814
+ "client {",
11815
+ " enabled = true",
11816
+ ` common_plugin_dir = ${JSON.stringify(pluginDir)}`,
11817
+ "}"
11818
+ ].join("\n");
11819
+ }
11820
+ /** The node-side config file: which credential, and who may use it. */
11821
+ function pluginConfig(opts) {
11822
+ const profile = { token_file: opts.tokenFile };
11823
+ if (opts.namespaces?.length) profile.namespaces = opts.namespaces;
11824
+ if (opts.jobs?.length) profile.jobs = opts.jobs;
11825
+ return `${JSON.stringify({ profiles: { [opts.profile]: profile } }, null, 2)}\n`;
11826
+ }
11827
+ /**
11828
+ * A job snippet.
11829
+ *
11830
+ * `path` is the profile name, because the first segment of a path is always a
11831
+ * profile — never a secret name. seekrit names and slugs overlap (`api_key` is
11832
+ * a legal spelling of both), so a single-segment path that could mean either
11833
+ * would resolve differently depending on what a node happened to have
11834
+ * configured, which is the worst possible failure mode for an access decision.
11835
+ */
11836
+ function jobSnippet(profile, names) {
11837
+ const vars = names.length > 0 ? names : ["DATABASE_URL", "STRIPE_SECRET_KEY"];
11838
+ const width = Math.max(...vars.map((n) => n.length));
11839
+ const INTERP = "${secret.seekrit.";
11840
+ const env = vars.map((n) => ` ${n.padEnd(width)} = "${INTERP}${n}}"`).join("\n");
11841
+ return [
11842
+ "job \"example\" {",
11843
+ " secret \"seekrit\" {",
11844
+ ` provider = ${JSON.stringify(PROVIDER_NAME)}`,
11845
+ ` path = ${JSON.stringify(profile)}`,
11846
+ " }",
11847
+ "",
11848
+ " group \"app\" {",
11849
+ " task \"server\" {",
11850
+ " driver = \"docker\"",
11851
+ " env {",
11852
+ env,
11853
+ " }",
11854
+ " }",
11855
+ " }",
11856
+ "}"
11857
+ ].join("\n");
11858
+ }
11859
+ async function fetchBytes(url) {
11860
+ const res = await fetch(url);
11861
+ if (!res.ok) throw new Error(`GET ${url} → ${res.status} ${res.statusText}`);
11862
+ return new Uint8Array(await res.arrayBuffer());
11863
+ }
11864
+ /**
11865
+ * Download, verify, and place the plugin.
11866
+ *
11867
+ * Same integrity story as the proxy launcher: the SHA-256 is checked against a
11868
+ * `.sha256` published beside the binary *before* anything is written to the
11869
+ * plugin directory. That proves the bytes match what the release published —
11870
+ * integrity, not provenance.
11871
+ *
11872
+ * The staging file lives in the destination directory rather than `TMPDIR` so
11873
+ * the rename is atomic: a cross-device rename degrades to a copy, which would
11874
+ * let Nomad fingerprint a half-written executable.
11875
+ */
11876
+ async function installPlugin(pluginDir, options = {}) {
11877
+ const version = options.version ?? "0.2.0";
11878
+ const baseUrl = (options.baseUrl ?? "https://nomad.seekrit.dev").replace(/\/+$/, "");
11879
+ const { target, exe } = detectTarget();
11880
+ if (exe !== "") fail("Nomad secret provider plugins are not supported on Windows clients");
11881
+ const prefix = versionPrefix(version);
11882
+ const name = `${PLUGIN_BIN}-${target}`;
11883
+ const binUrl = `${baseUrl}/${prefix}/bin/${name}`;
11884
+ if (!options.quiet) process.stderr.write(`seekrit: fetching ${PLUGIN_BIN} ${prefix} (${target})…\n`);
11885
+ let bytes;
11886
+ let expected;
11887
+ try {
11888
+ [bytes, expected] = await Promise.all([fetchBytes(binUrl), fetchBytes(`${binUrl}.sha256`).then((b) => Buffer.from(b).toString("utf8").trim().split(/\s+/)[0] ?? "")]);
11889
+ } catch (err) {
11890
+ const message = err instanceof Error ? err.message : String(err);
11891
+ fail(`could not download ${PLUGIN_BIN} ${prefix} for ${target}: ${message}\n Build it from apps/nomad-plugin, or use https://nomad.seekrit.dev/install.sh`);
11892
+ }
11893
+ const actual = createHash("sha256").update(bytes).digest("hex");
11894
+ if (!expected || actual !== expected.toLowerCase()) fail(`checksum mismatch for ${name}: expected ${expected || "(none published)"}, got ${actual}. Refusing to install it.`);
11895
+ const dest = pluginPath(pluginDir);
11896
+ const dir = dirname(dest);
11897
+ try {
11898
+ mkdirSync(dir, { recursive: true });
11899
+ } catch (err) {
11900
+ fail(`could not create ${dir}: ${err instanceof Error ? err.message : String(err)}\n The plugin directory usually belongs to root — try again with sudo.`);
11901
+ }
11902
+ const staging = join(dir, `.${PROVIDER_NAME}-${process.pid}-${actual.slice(0, 12)}`);
11903
+ try {
11904
+ writeFileSync(staging, bytes, { mode: 493 });
11905
+ renameSync(staging, dest);
11906
+ } catch (err) {
11907
+ rmSync(staging, { force: true });
11908
+ fail(`could not install to ${dest}: ${err instanceof Error ? err.message : String(err)}\n The plugin directory usually belongs to root — try again with sudo.`);
11909
+ }
11910
+ chmodSync(dest, 493);
11911
+ return {
11912
+ path: dest,
11913
+ version: prefix,
11914
+ target,
11915
+ sha256: actual
11916
+ };
11917
+ }
11918
+ /**
11919
+ * Exercise an installed plugin exactly the way Nomad will.
11920
+ *
11921
+ * This exists because every way to get a Nomad plugin wrong fails *silently* —
11922
+ * a misnamed file, an unset `common_plugin_dir`, a plugin installed but never
11923
+ * fingerprinted because the agent was not restarted. None of those produce a
11924
+ * diagnostic, so the only reliable check is to run the thing yourself.
11925
+ */
11926
+ function checkPlugin(pluginDir, opts = {}) {
11927
+ const steps = [];
11928
+ const bin = pluginPath(pluginDir);
11929
+ if (!existsSync(bin)) {
11930
+ steps.push({
11931
+ name: "installed",
11932
+ ok: false,
11933
+ detail: `${bin} is missing — run \`seekrit nomad install\``
11934
+ });
11935
+ return steps;
11936
+ }
11937
+ steps.push({
11938
+ name: "installed",
11939
+ ok: true,
11940
+ detail: bin
11941
+ });
11942
+ const fingerprint = spawnSync(bin, ["fingerprint"], {
11943
+ encoding: "utf8",
11944
+ timeout: 6e4
11945
+ });
11946
+ const fingerprintBody = safeJson(fingerprint.stdout);
11947
+ const fingerprintOk = fingerprint.status === 0 && fingerprintBody?.type === "secrets";
11948
+ steps.push({
11949
+ name: "fingerprint",
11950
+ ok: fingerprintOk,
11951
+ detail: fingerprintOk ? String(fingerprintBody?.version ?? "") : (fingerprint.stderr || fingerprint.stdout || "no output").trim()
11952
+ });
11953
+ if (!fingerprintOk) return steps;
11954
+ const fetch = spawnSync(bin, [...opts.config ? [
11955
+ "--config",
11956
+ opts.config,
11957
+ "fetch"
11958
+ ] : ["fetch"], opts.path ?? ""], {
11959
+ encoding: "utf8",
11960
+ timeout: 6e4,
11961
+ env: {
11962
+ ...process.env,
11963
+ ...opts.namespace ? { NOMAD_NAMESPACE: opts.namespace } : {},
11964
+ ...opts.job ? { NOMAD_JOB_ID: opts.job } : {}
11965
+ }
11966
+ });
11967
+ const body = safeJson(fetch.stdout);
11968
+ if (!body) {
11969
+ steps.push({
11970
+ name: "fetch",
11971
+ ok: false,
11972
+ detail: (fetch.stderr || "no JSON on stdout").trim()
11973
+ });
11974
+ return steps;
11975
+ }
11976
+ if (typeof body.error === "string") {
11977
+ steps.push({
11978
+ name: "fetch",
11979
+ ok: false,
11980
+ detail: body.error
11981
+ });
11982
+ return steps;
11983
+ }
11984
+ const count = Object.keys(body.result ?? {}).length;
11985
+ steps.push({
11986
+ name: "fetch",
11987
+ ok: true,
11988
+ detail: `${count} ${count === 1 ? "secret" : "secrets"}`
11989
+ });
11990
+ return steps;
11991
+ }
11992
+ function safeJson(text) {
11993
+ if (!text) return void 0;
11994
+ try {
11995
+ const parsed = JSON.parse(text.trim());
11996
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
11997
+ } catch {
11998
+ return;
11999
+ }
12000
+ }
12001
+ function requirePluginDir(explicit) {
12002
+ if (explicit) return resolve(explicit);
12003
+ const found = detectPluginDir();
12004
+ if (found) return found;
12005
+ fail("could not find the Nomad plugin directory — pass --plugin-dir <path> matching your agent's client.common_plugin_dir");
12006
+ }
12007
+ function collect$3(value, previous = []) {
12008
+ return [...previous, value];
12009
+ }
12010
+ function registerNomadCommands(program) {
12011
+ const nomad = program.command("nomad").description("wire seekrit into Nomad as a secret provider (`seekrit nomad --help`)");
12012
+ nomad.command("init").description("print the agent stanza, plugin config, and job snippet for this client").option("--plugin-dir <path>", "the agent's client.common_plugin_dir").option("--profile <name>", "profile name jobs will reference in `path`", "default").option("--token-file <path>", "where the profile's service token lives").option("--namespace <ns>", "restrict the profile to a Nomad namespace (repeatable)", collect$3).option("--job <id>", "restrict the profile to a Nomad job id (repeatable)", collect$3).option("--config <path>", "plugin config path", DEFAULT_CONFIG_PATH).option("--write", "write the plugin config file instead of printing it").option("--json", "machine-readable output").action((options) => {
12013
+ const pluginDir = options.pluginDir ? resolve(options.pluginDir) : detectPluginDir() ?? PLUGIN_DIR_CANDIDATES[0];
12014
+ const profile = options.profile ?? "default";
12015
+ const tokenFile = options.tokenFile ?? `/etc/seekrit/tokens/${profile}`;
12016
+ const configPath = resolve(options.config ?? "/etc/seekrit/nomad.json");
12017
+ const config = pluginConfig({
12018
+ profile,
12019
+ tokenFile,
12020
+ namespaces: options.namespace,
12021
+ jobs: options.job
12022
+ });
12023
+ let wrote = false;
12024
+ if (options.write) try {
12025
+ mkdirSync(dirname(configPath), { recursive: true });
12026
+ writeFileSync(configPath, config, { mode: 384 });
12027
+ wrote = true;
12028
+ } catch (err) {
12029
+ fail(`could not write ${configPath}: ${err instanceof Error ? err.message : String(err)}\n This path usually belongs to root — try again with sudo.`);
12030
+ }
12031
+ emit(options, {
12032
+ pluginDir,
12033
+ pluginPath: pluginPath(pluginDir),
12034
+ configPath,
12035
+ profile,
12036
+ wrote
12037
+ }, () => {
12038
+ section("nomad secret provider");
12039
+ printFields([
12040
+ ["plugin dir", pluginDir],
12041
+ ["plugin path", pluginPath(pluginDir)],
12042
+ ["provider", PROVIDER_NAME],
12043
+ ["config", configPath],
12044
+ ["profile", profile],
12045
+ ["config written", wrote ? "yes" : "no"]
12046
+ ]);
12047
+ console.log();
12048
+ console.log("1. Install the plugin on every Nomad client:");
12049
+ console.log(" seekrit nomad install");
12050
+ console.log();
12051
+ console.log(" The file must be named `seekrit` — Nomad matches a job's `provider`");
12052
+ console.log(" against the executable's filename, and a mismatch registers nothing.");
12053
+ console.log();
12054
+ console.log("2. Point the agent at the plugin directory (/etc/nomad.d/client.hcl):");
12055
+ console.log();
12056
+ console.log(indent(agentStanza(pluginDir)));
12057
+ console.log();
12058
+ console.log(`3. ${wrote ? `Written to ${configPath}:` : `Put this in ${configPath} (mode 0600):`}`);
12059
+ console.log();
12060
+ console.log(indent(config.trimEnd()));
12061
+ console.log();
12062
+ console.log(` Then write the token itself to ${tokenFile} (mode 0600):`);
12063
+ console.log(` seekrit token create --name nomad-${profile} --app <app> --env <env>`);
12064
+ console.log(` # then: echo "skt_…" | sudo tee ${tokenFile} && sudo chmod 600 ${tokenFile}`);
12065
+ console.log();
12066
+ console.log("4. Restart the agent — plugins are fingerprinted at client startup:");
12067
+ console.log(" sudo systemctl restart nomad");
12068
+ console.log();
12069
+ console.log("5. Reference it from a job:");
12070
+ console.log();
12071
+ console.log(indent(jobSnippet(profile, [])));
12072
+ console.log();
12073
+ console.log("Check your work with: seekrit nomad check");
12074
+ });
12075
+ });
12076
+ nomad.command("install").description("download the secret provider plugin onto this Nomad client").option("--plugin-dir <path>", "the agent's client.common_plugin_dir").option("--version <version>", "plugin version to install", NOMAD_PLUGIN_VERSION).option("--base-url <url>", "artifact host", DEFAULT_BASE_URL).option("--json", "machine-readable output").action(async (options) => {
12077
+ const result = await installPlugin(requirePluginDir(options.pluginDir), {
12078
+ version: options.version,
12079
+ baseUrl: options.baseUrl,
12080
+ quiet: options.json === true
12081
+ });
12082
+ emit(options, result, () => {
12083
+ section("nomad plugin installed");
12084
+ printFields([
12085
+ ["path", result.path],
12086
+ ["version", result.version],
12087
+ ["target", result.target],
12088
+ ["sha256", result.sha256],
12089
+ ["provider", PROVIDER_NAME]
12090
+ ]);
12091
+ console.log();
12092
+ console.log("Nomad fingerprints plugins at client startup — restart the agent:");
12093
+ console.log(" sudo systemctl restart nomad");
12094
+ console.log();
12095
+ console.log("Then: seekrit nomad check");
12096
+ });
12097
+ });
12098
+ nomad.command("check").description("run the installed plugin the way Nomad does, and report what happened").option("--plugin-dir <path>", "the agent's client.common_plugin_dir").option("--path <path>", "the `path` a job would use (profile, or profile/NAME)", "").option("--namespace <ns>", "pretend to be a job in this Nomad namespace").option("--job <id>", "pretend to be this Nomad job id").option("--config <path>", "check against this plugin config instead of the installed one").option("--json", "machine-readable output").action((options) => {
12099
+ const pluginDir = requirePluginDir(options.pluginDir);
12100
+ const steps = checkPlugin(pluginDir, {
12101
+ path: options.path,
12102
+ namespace: options.namespace,
12103
+ job: options.job,
12104
+ config: options.config
12105
+ });
12106
+ const ok = steps.every((s) => s.ok);
12107
+ emit(options, {
12108
+ pluginDir,
12109
+ ok,
12110
+ steps
12111
+ }, () => {
12112
+ section("nomad plugin check");
12113
+ printFields(steps.map((s) => [s.name, `${s.ok ? "ok" : "FAILED"} — ${s.detail}`]));
12114
+ console.log();
12115
+ if (ok) {
12116
+ console.log("The plugin resolves. If a job still cannot, the agent has not been");
12117
+ console.log("restarted since the install, or client.common_plugin_dir is unset.");
12118
+ } else console.log("See `seekrit nomad init` for the configuration this expects.");
12119
+ });
12120
+ if (!ok) process.exitCode = 1;
12121
+ });
12122
+ }
12123
+ function indent(text) {
12124
+ return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
12125
+ }
12126
+ //#endregion
11561
12127
  //#region src/orgs.ts
11562
12128
  /**
11563
12129
  * Counts for `org show`. Each list is admin-gated to a different degree, so a
@@ -12683,188 +13249,6 @@ function collect$2(value, acc) {
12683
13249
  return acc;
12684
13250
  }
12685
13251
  //#endregion
12686
- //#region src/proxy-binary.ts
12687
- /**
12688
- * Fetch and run the `seekrit-proxy` binary without a Rust toolchain.
12689
- *
12690
- * The proxy is the strongest answer seekrit has for an untrusted workload — the
12691
- * agent holds `{{seekrit:NAME}}` and never the key — and it was also the hardest
12692
- * thing here to *try*, because trying it meant `cargo` and a TOML file. This
12693
- * module removes the first half: it resolves a prebuilt, checksum-verified
12694
- * binary for the host platform and execs it, so `npx @seekrit/proxy` and
12695
- * `seekrit proxy run` behave like the proxy was already installed.
12696
- *
12697
- * The logic lives in the CLI (and is re-exported as `@seekrit/cli/proxy-launcher`)
12698
- * for the same reason the MCP server does: `@seekrit/proxy` is a thin npx
12699
- * entrypoint over it, and the two must not drift.
12700
- *
12701
- * Three properties worth stating, since this downloads and executes code:
12702
- *
12703
- * - **The checksum is verified before anything is executed**, against a
12704
- * `.sha256` fetched from the same release. That is integrity, not provenance —
12705
- * it proves the bytes match what the release published, which is exactly the
12706
- * guarantee `install.sh` gives and no more.
12707
- * - **Nothing is fetched when a binary is already available.** `SEEKRIT_PROXY_BIN`
12708
- * short-circuits entirely, and a cached download for the same version+target is
12709
- * reused, so this is a one-time cost per version.
12710
- * - **Version is pinned, not floating.** A default of `latest` would make two
12711
- * machines run different proxies from the same command; the pinned constant is
12712
- * what this CLI was built against, overridable when you want otherwise.
12713
- */
12714
- /**
12715
- * The proxy version this CLI was built against.
12716
- *
12717
- * Bumped by release-please when `apps/proxy` releases (an `extra-files` entry in
12718
- * release-please-config.json), so the pin follows the crate without anyone
12719
- * remembering to move it.
12720
- */
12721
- const PROXY_VERSION = "0.12.0";
12722
- const BIN = "seekrit-proxy";
12723
- /**
12724
- * Host → Rust target triple.
12725
- *
12726
- * Linux always resolves to **musl**: that build is statically linked, so one
12727
- * artifact covers glibc, musl, alpine, and distroless, and there is no libc
12728
- * detection to get wrong on a machine where `ldd` says something unexpected.
12729
- */
12730
- function detectTarget(os = platform(), cpu = arch()) {
12731
- const machine = cpu === "x64" ? "x86_64" : cpu === "arm64" ? "aarch64" : null;
12732
- if (!machine) throw new Error(`unsupported architecture "${cpu}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
12733
- switch (os) {
12734
- case "linux": return {
12735
- target: `${machine}-unknown-linux-musl`,
12736
- exe: ""
12737
- };
12738
- case "darwin": return {
12739
- target: `${machine}-apple-darwin`,
12740
- exe: ""
12741
- };
12742
- case "win32":
12743
- if (machine !== "x86_64") throw new Error(`no prebuilt seekrit-proxy for ${machine} Windows — set SEEKRIT_PROXY_BIN to a binary you built`);
12744
- return {
12745
- target: "x86_64-pc-windows-msvc",
12746
- exe: ".exe"
12747
- };
12748
- default: throw new Error(`unsupported platform "${os}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
12749
- }
12750
- }
12751
- /** `latest` stays `latest`; everything else is normalized to `v<x.y.z>`. */
12752
- function versionPrefix(version) {
12753
- if (version === "latest") return "latest";
12754
- return version.startsWith("v") ? version : `v${version}`;
12755
- }
12756
- function resolveVersion(explicit) {
12757
- return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
12758
- }
12759
- function resolveBaseUrl(explicit) {
12760
- return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
12761
- }
12762
- /** Where a resolved binary is kept, keyed so versions and targets never collide. */
12763
- function proxyBinaryPath(version, target, exe) {
12764
- return join(defaultCacheDir(), "proxy", versionPrefix(version), target, `${BIN}${exe}`);
12765
- }
12766
- async function fetchBytes(url) {
12767
- const res = await fetch(url);
12768
- if (!res.ok) throw new Error(`GET ${url} → ${res.status} ${res.statusText}`);
12769
- return new Uint8Array(await res.arrayBuffer());
12770
- }
12771
- /**
12772
- * Ensure a `seekrit-proxy` binary exists locally and return its path.
12773
- *
12774
- * Order: an explicit `SEEKRIT_PROXY_BIN`, then a cached download for this
12775
- * version+target, then a fresh download. A binary already on `PATH` is
12776
- * deliberately *not* used — silently running a different version than the one
12777
- * this CLI pins is the kind of surprise that costs an afternoon.
12778
- */
12779
- async function resolveProxyBinary(options = {}) {
12780
- const override = process.env.SEEKRIT_PROXY_BIN;
12781
- if (override) {
12782
- if (!existsSync(override)) throw new Error(`SEEKRIT_PROXY_BIN points at ${override}, which does not exist`);
12783
- return override;
12784
- }
12785
- const version = resolveVersion(options.version);
12786
- const { target, exe } = detectTarget();
12787
- const dest = proxyBinaryPath(version, target, exe);
12788
- if (!options.force && version !== "latest" && existsSync(dest)) return dest;
12789
- const baseUrl = resolveBaseUrl(options.baseUrl);
12790
- const prefix = versionPrefix(version);
12791
- const name = `${BIN}-${target}${exe}`;
12792
- const binUrl = `${baseUrl}/${prefix}/bin/${name}`;
12793
- const sumUrl = `${binUrl}.sha256`;
12794
- if (!options.quiet) process.stderr.write(`seekrit: fetching ${BIN} ${prefix} (${target})…\n`);
12795
- let bytes;
12796
- let expected;
12797
- try {
12798
- [bytes, expected] = await Promise.all([fetchBytes(binUrl), fetchBytes(sumUrl).then((b) => Buffer.from(b).toString("utf8").trim().split(/\s+/)[0] ?? "")]);
12799
- } catch (err) {
12800
- const message = err instanceof Error ? err.message : String(err);
12801
- throw new Error(`could not download ${BIN} ${prefix} for ${target}: ${message}\n Set SEEKRIT_PROXY_BIN to a binary you already have, or build it from apps/proxy.`);
12802
- }
12803
- const actual = createHash("sha256").update(bytes).digest("hex");
12804
- if (!expected || actual !== expected.toLowerCase()) throw new Error(`checksum mismatch for ${name}: expected ${expected || "(none published)"}, got ${actual}. Refusing to run it.`);
12805
- const dir = dirname(dest);
12806
- mkdirSync(dir, { recursive: true });
12807
- const staging = join(dir, `.${BIN}-${process.pid}-${actual.slice(0, 12)}${exe}`);
12808
- try {
12809
- writeFileSync(staging, bytes, { mode: 493 });
12810
- renameSync(staging, dest);
12811
- } catch (err) {
12812
- rmSync(staging, { force: true });
12813
- throw err;
12814
- }
12815
- chmodSync(dest, 493);
12816
- return dest;
12817
- }
12818
- /**
12819
- * Run the proxy, forwarding stdio, signals, and its exit status.
12820
- *
12821
- * The proxy is a long-lived foreground process, so this wrapper has to be
12822
- * transparent: Node cannot exec-replace itself, and without relaying signals
12823
- * Node's default SIGINT handler would kill *this* process on Ctrl-C and leave
12824
- * the proxy running, holding decrypted secrets, with the shell prompt back.
12825
- */
12826
- async function runProxyBinary(argv, options = {}) {
12827
- const bin = await resolveProxyBinary(options);
12828
- const child = spawn(bin, argv, {
12829
- stdio: "inherit",
12830
- env: {
12831
- ...process.env,
12832
- ...options.env
12833
- }
12834
- });
12835
- const signals = [
12836
- "SIGINT",
12837
- "SIGTERM",
12838
- "SIGHUP",
12839
- "SIGQUIT"
12840
- ];
12841
- const forward = (signal) => {
12842
- if (child.exitCode !== null || child.signalCode !== null) return;
12843
- child.kill(signal);
12844
- };
12845
- for (const signal of signals) process.on(signal, forward);
12846
- return new Promise((resolve, reject) => {
12847
- child.on("error", (err) => {
12848
- for (const s of signals) process.off(s, forward);
12849
- reject(/* @__PURE__ */ new Error(`could not start ${bin}: ${err.message}\n If this is a fresh download, the platform may not match — set SEEKRIT_PROXY_BIN.`));
12850
- });
12851
- child.on("exit", (code, signal) => {
12852
- for (const s of signals) process.off(s, forward);
12853
- resolve(signal ? 128 + signalNumber(signal) : code ?? 0);
12854
- });
12855
- });
12856
- }
12857
- /** Signal name → number, for the 128+n exit convention. */
12858
- function signalNumber(signal) {
12859
- return {
12860
- SIGHUP: 1,
12861
- SIGINT: 2,
12862
- SIGQUIT: 3,
12863
- SIGKILL: 9,
12864
- SIGTERM: 15
12865
- }[signal] ?? 0;
12866
- }
12867
- //#endregion
12868
13252
  //#region src/proxy.ts
12869
13253
  /**
12870
13254
  * `seekrit proxy` — get `seekrit-proxy` running without a Rust toolchain or a
@@ -15130,6 +15514,7 @@ registerProvisionerCommands(program);
15130
15514
  registerProxyCommands(program);
15131
15515
  registerAgentCommands(program);
15132
15516
  registerPaperclipCommands(program);
15517
+ registerNomadCommands(program);
15133
15518
  registerOpenclawCommands(program);
15134
15519
  registerHermesCommands(program);
15135
15520
  registerSshCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {