@seekrit/cli 1.2.2 → 1.4.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 +148 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { createInterface } from "node:readline";
9
9
  import { Writable } from "node:stream";
10
10
  import { createHash } from "node:crypto";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { request } from "node:http";
12
13
  /** Default lifetime of a published bundle (7 days), in seconds. */
13
14
  const POLICY_DEFAULT_TTL_SECONDS = 10080 * 60;
14
15
  /** Bounds on a bundle's lifetime: an hour at the short end, 90 days at the long. */
@@ -5051,7 +5052,7 @@ async function createAgentTaskToken() {
5051
5052
  }
5052
5053
  //#endregion
5053
5054
  //#region package.json
5054
- var version = "1.2.2";
5055
+ var version = "1.4.0";
5055
5056
  //#endregion
5056
5057
  //#region ../../packages/api-client/src/index.ts
5057
5058
  var SeekritApiError = class extends Error {
@@ -11510,9 +11511,20 @@ function renderProxyConfig(plan) {
11510
11511
  out.push("# agent. Requires SEEKRIT_PROXY_CONTROL_TOKEN in the environment, and that");
11511
11512
  out.push("# token must not be readable by the agents.");
11512
11513
  out.push("[control]");
11513
- out.push(`listen = ${tomlString(plan.control.listen)}`);
11514
+ if (plan.control.socket) out.push(`socket = ${tomlString(plan.control.socket)}`);
11515
+ else if (plan.control.listen) out.push(`listen = ${tomlString(plan.control.listen)}`);
11514
11516
  out.push(`ttl = ${tomlString(plan.control.ttl)}`);
11515
11517
  out.push(`max_ttl = ${tomlString(plan.control.maxTtl)}`);
11518
+ if (plan.control.socket) {
11519
+ out.push("# Only this program may mint tickets and decide held requests. Without it,");
11520
+ out.push("# any process running as your user and holding the control token can.");
11521
+ out.push("# allow_binary = [\"/usr/local/bin/your-orchestrator\"]");
11522
+ out.push("# allow_sha256 = [\"<sha256 of that binary>\"]");
11523
+ } else {
11524
+ out.push("# A TCP control listener carries no peer credentials, so the token is the");
11525
+ out.push("# only gate — and an agent spawned by the orchestrator inherits it. Use a");
11526
+ out.push("# Unix socket (--control /run/seekrit/control.sock) to attest the caller.");
11527
+ }
11516
11528
  out.push("");
11517
11529
  }
11518
11530
  if (plan.tasks) {
@@ -12165,7 +12177,7 @@ function collect$2(value, acc) {
12165
12177
  * release-please-config.json), so the pin follows the crate without anyone
12166
12178
  * remembering to move it.
12167
12179
  */
12168
- const PROXY_VERSION = "0.10.2";
12180
+ const PROXY_VERSION = "0.12.0";
12169
12181
  const BIN = "seekrit-proxy";
12170
12182
  /**
12171
12183
  * Host → Rust target triple.
@@ -12201,7 +12213,7 @@ function versionPrefix(version) {
12201
12213
  return version.startsWith("v") ? version : `v${version}`;
12202
12214
  }
12203
12215
  function resolveVersion(explicit) {
12204
- return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.10.2";
12216
+ return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
12205
12217
  }
12206
12218
  function resolveBaseUrl(explicit) {
12207
12219
  return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
@@ -12328,6 +12340,93 @@ function signalNumber(signal) {
12328
12340
  * — and none of them changes what the proxy does. The generated config is the
12329
12341
  * same config; the fetched binary is the released binary, checksum-verified.
12330
12342
  */
12343
+ /**
12344
+ * Where `seekrit proxy approvals` looks by default.
12345
+ *
12346
+ * The control listener is loopback-bound by design, so the only interesting
12347
+ * variable is the port — and one URL is easier to override than a host/port pair.
12348
+ */
12349
+ const DEFAULT_CONTROL_URL = "http://127.0.0.1:9090";
12350
+ /** Header the control listener authenticates with (mirrors `src/tickets.rs`). */
12351
+ const CONTROL_TOKEN_HEADER = "x-seekrit-control-token";
12352
+ /**
12353
+ * Talk to a running proxy's control listener.
12354
+ *
12355
+ * The token comes from the environment and never from a flag: it is what decides
12356
+ * whether a held request goes through, and a flag would put it in shell history
12357
+ * next to the operation it is guarding.
12358
+ */
12359
+ async function controlRequest(base, path, decision) {
12360
+ const token = process.env.SEEKRIT_PROXY_CONTROL_TOKEN;
12361
+ if (!token) fail("SEEKRIT_PROXY_CONTROL_TOKEN is not set — it is the same token the proxy was started with, and without it the listener will not answer");
12362
+ const headers = {
12363
+ [CONTROL_TOKEN_HEADER]: token,
12364
+ ...decision ? { "content-type": "application/json" } : {}
12365
+ };
12366
+ const method = decision ? "POST" : "GET";
12367
+ const body = decision ? JSON.stringify({ decision }) : void 0;
12368
+ let status;
12369
+ let text;
12370
+ try {
12371
+ ({status, text} = isSocketPath(base) ? await socketRequest(socketPathOf(base), path, method, headers, body) : await fetchRequest(base, path, method, headers, body));
12372
+ } catch (e) {
12373
+ fail(`could not reach the proxy's control listener at ${base}: ${e.message}\nIs the proxy running, and does its config have a [control] block?`);
12374
+ throw e;
12375
+ }
12376
+ if (status < 200 || status >= 300) fail(`${status} from the control listener: ${text.trim()}`);
12377
+ return JSON.parse(text);
12378
+ }
12379
+ /** Is this `--control` value a Unix socket rather than a URL? */
12380
+ function isSocketPath(value) {
12381
+ return value.startsWith("unix:") || value.startsWith("/") || value.startsWith(".");
12382
+ }
12383
+ /** The filesystem path inside a `--control` value. */
12384
+ function socketPathOf(value) {
12385
+ return value.startsWith("unix:") ? value.slice(5).replace(/^\/\//, "") : value;
12386
+ }
12387
+ async function fetchRequest(base, path, method, headers, body) {
12388
+ const resp = await fetch(new URL(path, base), {
12389
+ method,
12390
+ headers,
12391
+ ...body ? { body } : {}
12392
+ });
12393
+ return {
12394
+ status: resp.status,
12395
+ text: await resp.text()
12396
+ };
12397
+ }
12398
+ /**
12399
+ * The same request over a Unix socket.
12400
+ *
12401
+ * `fetch` cannot dial one without a custom dispatcher, and `node:http` can with
12402
+ * no dependency at all — which matters for a CLI shipped over npm.
12403
+ */
12404
+ function socketRequest(socketPath, path, method, headers, body) {
12405
+ return new Promise((resolve, reject) => {
12406
+ const req = request({
12407
+ socketPath,
12408
+ path,
12409
+ method,
12410
+ headers: {
12411
+ ...headers,
12412
+ ...body ? { "content-length": Buffer.byteLength(body) } : {}
12413
+ }
12414
+ }, (res) => {
12415
+ let text = "";
12416
+ res.setEncoding("utf8");
12417
+ res.on("data", (chunk) => {
12418
+ text += chunk;
12419
+ });
12420
+ res.on("end", () => resolve({
12421
+ status: res.statusCode ?? 0,
12422
+ text
12423
+ }));
12424
+ });
12425
+ req.on("error", reject);
12426
+ if (body) req.write(body);
12427
+ req.end();
12428
+ });
12429
+ }
12331
12430
  const DEFAULT_CONFIG = "seekrit-proxy.toml";
12332
12431
  /**
12333
12432
  * The exact grammar `seekrit_cache::parse_duration` accepts: a positive integer,
@@ -12350,7 +12449,8 @@ function planOptions(options) {
12350
12449
  const listen = options.listen ?? PLAN_DEFAULTS.listen;
12351
12450
  const forwardListen = options.forwardListen ?? PLAN_DEFAULTS.forwardListen;
12352
12451
  if (mode === "both" && listen === forwardListen) fail(`--listen and --forward-listen cannot both be ${listen} — the two planes need separate ports`);
12353
- if (options.control && (options.control === listen || options.control === forwardListen)) fail(`--control cannot share an address with a data plane (${options.control})`);
12452
+ const controlIsSocket = options.control !== void 0 && isSocketPath(options.control);
12453
+ if (options.control && !controlIsSocket && (options.control === listen || options.control === forwardListen)) fail(`--control cannot share an address with a data plane (${options.control})`);
12354
12454
  const refresh = duration(options.refresh, "--refresh");
12355
12455
  return {
12356
12456
  mode,
@@ -12362,7 +12462,7 @@ function planOptions(options) {
12362
12462
  ...options.cache || options.cacheMaxAge ? { cacheMaxAge: duration(options.cacheMaxAge, "--cache-max-age") ?? "24h" } : {},
12363
12463
  ...refresh ? { secretsRefresh: refresh } : {},
12364
12464
  ...options.control ? { control: {
12365
- listen: options.control,
12465
+ ...controlIsSocket ? { socket: options.control } : { listen: options.control },
12366
12466
  ttl: "1h",
12367
12467
  maxTtl: "12h"
12368
12468
  } } : {},
@@ -12486,7 +12586,7 @@ async function buildPlan(options) {
12486
12586
  }
12487
12587
  /** Add the generation flags to a command, so `init` and `run` stay in step. */
12488
12588
  function withGenerateOptions(cmd) {
12489
- return cmd.option("--preset <name>", "gateway preset (repeatable; see `seekrit proxy presets`)", (value, acc = []) => [...acc, value]).option("--host <host[=SECRET,…]>", "ad-hoc rule: bare hostname, optionally the secrets it may receive (repeatable)", (value, acc = []) => [...acc, value]).option("--base-url <url>", "upstream base URL for an OpenAI-compatible gateway").option("--secret <NAME>", "override a preset's secret name").option("--prefix <path>", "override a preset's route prefix").option("--agent <slug>", "take the rules from published agent policy (server mode)").option("--agents <slug>", "additional identities this proxy may serve", (v, a = []) => [...a, v]).option("--org <slug>", "organization (for --agent)").option("--mode <reverse|forward|both>", "which data plane(s) to configure", "reverse").option("--listen <addr>", `reverse-proxy address (default: ${PLAN_DEFAULTS.listen})`).option("--forward-listen <addr>", `forward-proxy address (default: ${PLAN_DEFAULTS.forwardListen})`).option("--unmatched <tunnel|deny>", "what to do with an unruled host in forward mode").option("--ca-cert <path>", "interception CA certificate path (forward mode)").option("--ca-key <path>", "interception CA key path (forward mode)").option("--cache", "add a [cache] block so the proxy can start during an outage").option("--cache-max-age <dur>", "how stale a cached resolve may be (implies --cache)").option("--refresh <dur>", "re-resolve/re-fetch interval").option("--control <addr>", "add a [control] listener for per-agent session tickets").option("--tasks", "add a [tasks] block so this proxy honours runs dispatched with `seekrit agents dispatch`").option("--tasks-cache-ttl <dur>", "how long an introspected task is reused (implies --tasks)").option("--activity", "add an [activity] block so this proxy reports aggregate decisions for `seekrit agents review`").option("--activity-interval <dur>", "how often counts are flushed (implies --activity)");
12589
+ return cmd.option("--preset <name>", "gateway preset (repeatable; see `seekrit proxy presets`)", (value, acc = []) => [...acc, value]).option("--host <host[=SECRET,…]>", "ad-hoc rule: bare hostname, optionally the secrets it may receive (repeatable)", (value, acc = []) => [...acc, value]).option("--base-url <url>", "upstream base URL for an OpenAI-compatible gateway").option("--secret <NAME>", "override a preset's secret name").option("--prefix <path>", "override a preset's route prefix").option("--agent <slug>", "take the rules from published agent policy (server mode)").option("--agents <slug>", "additional identities this proxy may serve", (v, a = []) => [...a, v]).option("--org <slug>", "organization (for --agent)").option("--mode <reverse|forward|both>", "which data plane(s) to configure", "reverse").option("--listen <addr>", `reverse-proxy address (default: ${PLAN_DEFAULTS.listen})`).option("--forward-listen <addr>", `forward-proxy address (default: ${PLAN_DEFAULTS.forwardListen})`).option("--unmatched <tunnel|deny>", "what to do with an unruled host in forward mode").option("--ca-cert <path>", "interception CA certificate path (forward mode)").option("--ca-key <path>", "interception CA key path (forward mode)").option("--cache", "add a [cache] block so the proxy can start during an outage").option("--cache-max-age <dur>", "how stale a cached resolve may be (implies --cache)").option("--refresh <dur>", "re-resolve/re-fetch interval").option("--control <addr|path>", "add a [control] listener for per-agent session tickets; a path makes it a Unix socket, which is the only transport whose caller can be attested").option("--tasks", "add a [tasks] block so this proxy honours runs dispatched with `seekrit agents dispatch`").option("--tasks-cache-ttl <dur>", "how long an introspected task is reused (implies --tasks)").option("--activity", "add an [activity] block so this proxy reports aggregate decisions for `seekrit agents review`").option("--activity-interval <dur>", "how often counts are flushed (implies --activity)");
12490
12590
  }
12491
12591
  function registerProxyCommands(program) {
12492
12592
  const proxy = program.command("proxy").description("run and configure the agent egress proxy (`seekrit proxy --help`)");
@@ -12553,10 +12653,10 @@ Next:
12553
12653
  });
12554
12654
  console.log(path);
12555
12655
  const { target } = detectTarget();
12556
- process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.10.2")} (${target})\n`);
12656
+ process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.12.0")} (${target})\n`);
12557
12657
  });
12558
12658
  proxy.command("where").description("show which binary `seekrit proxy run` would use, without fetching it").option("--proxy-version <version>", `version to report (default: ${PROXY_VERSION})`).option("--json", "machine-readable output").action((options) => {
12559
- const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.10.2";
12659
+ const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
12560
12660
  const override = process.env.SEEKRIT_PROXY_BIN;
12561
12661
  const { target, exe } = detectTarget();
12562
12662
  const path = override ?? proxyBinaryPath(version, target, exe);
@@ -12572,12 +12672,50 @@ Next:
12572
12672
  process.stderr.write(`${info.version} · ${info.target} · ${info.source}\n`);
12573
12673
  });
12574
12674
  });
12675
+ proxy.command("approvals").description("list requests a running proxy is holding for approval").option("--control <url|path>", "control listener URL, or a Unix socket path", DEFAULT_CONTROL_URL).option("--json", "machine-readable output").action(async (options) => {
12676
+ const { pending } = await controlRequest(options.control, "/approvals");
12677
+ emit(options, { pending }, () => {
12678
+ printTable(pending, [
12679
+ col("ID", (p) => p.id),
12680
+ col("OPERATION", (p) => p.operation),
12681
+ col("REQUEST", (p) => `${p.method} ${p.host}${p.path}`),
12682
+ col("CARRIES", (p) => p.secrets.join(", ") || "no credential"),
12683
+ col("EXPIRES", (p) => `${p.expires_in_seconds}s`)
12684
+ ], "nothing is waiting");
12685
+ });
12686
+ });
12687
+ for (const [verb, decision, summary] of [
12688
+ [
12689
+ "approve",
12690
+ "approve",
12691
+ "let a held request through"
12692
+ ],
12693
+ [
12694
+ "deny",
12695
+ "deny",
12696
+ "refuse a held request"
12697
+ ],
12698
+ [
12699
+ "always",
12700
+ "always",
12701
+ "let it through and stop asking about that host and method for this run"
12702
+ ]
12703
+ ]) proxy.command(`${verb} <id>`).description(summary).option("--control <url|path>", "control listener URL, or a Unix socket path", DEFAULT_CONTROL_URL).option("--json", "machine-readable output").action(async (id, options) => {
12704
+ const result = await controlRequest(options.control, `/approvals/${encodeURIComponent(id)}`, decision);
12705
+ emit(options, {
12706
+ id,
12707
+ decision,
12708
+ ...result
12709
+ }, () => {
12710
+ process.stderr.write(`${id}: ${decision}\n`);
12711
+ });
12712
+ });
12575
12713
  withGenerateOptions(proxy.command("compose").description("print a docker compose sidecar snippet for a generated config")).option("--service <name>", "compose service name for the proxy", COMPOSE_DEFAULTS.service).option("--workload <name>", "compose service name for your workload", COMPOSE_DEFAULTS.workload).option("--image <ref>", "image to pin (default: seekritdev/proxy:<version>)").option("--publish", "also publish the proxy's ports to the host").action(async (options) => {
12576
12714
  const plan = await buildPlan(options);
12577
12715
  process.stdout.write(renderComposeSnippet(plan, {
12578
12716
  service: options.service,
12579
12717
  workload: options.workload,
12580
- image: options.image ?? `seekritdev/proxy:0.10.2`,
12718
+ image: options.image ?? `seekritdev/proxy:0.12.0`,
12581
12719
  publish: Boolean(options.publish)
12582
12720
  }));
12583
12721
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {