@seekrit/cli 0.41.0 → 0.43.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 +1280 -11
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,13 +1,87 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  import { Command } from "commander";
6
- import { homedir, hostname, tmpdir, userInfo } from "node:os";
7
- import { dirname, join, parse } from "node:path";
6
+ import { arch, homedir, hostname, platform, tmpdir, userInfo } from "node:os";
7
+ import { dirname, join, parse, resolve } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
9
  import { Writable } from "node:stream";
10
10
  import { createHash } from "node:crypto";
11
+ /** A bare hostname: no scheme, no port, no path, no wildcard. */
12
+ const policyHostSchema = z.string().trim().min(1).max(253).toLowerCase().refine((h) => !/[:/\s*]/.test(h), { message: "host must be a bare hostname (no scheme, port, path, or wildcard)" }).refine((h) => /^[a-z0-9.-]+$/.test(h), { message: "host contains invalid characters" });
13
+ const policyMethodSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3,10}$/, "method must be an HTTP method name");
14
+ /**
15
+ * A path pattern. Must be absolute, because it is matched against a request
16
+ * path — a relative pattern is a mistake that would silently match nothing.
17
+ */
18
+ const policyPathSchema = z.string().trim().min(1).max(512).startsWith("/", "path pattern must start with /").refine((p) => !p.includes("?"), { message: "path patterns match the path only, not the query" });
19
+ const policySecretNameSchema = z.string().trim().regex(/^[A-Za-z0-9_]+$/, "secret names are letters, digits, and underscores");
20
+ z.object({
21
+ host: policyHostSchema,
22
+ methods: z.array(policyMethodSchema).max(16).default([]),
23
+ paths: z.array(policyPathSchema).max(64).default([]),
24
+ allow: z.array(policySecretNameSchema).max(64).default([]),
25
+ label: z.string().trim().max(120).optional()
26
+ });
27
+ z.object({
28
+ /** `ap1.<body>.<signature>` — opaque to the server. */
29
+ bundle: z.string().min(16).max(256 * 1024) });
30
+ z.object({
31
+ name: z.string().trim().min(1).max(120),
32
+ slug: z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "slug is lowercase letters, digits, and dashes"),
33
+ /** The environment whose secrets this agent's policy may name. Optional. */
34
+ environmentId: z.string().trim().min(1).max(64).optional()
35
+ });
36
+ z.object({
37
+ name: z.string().trim().min(1).max(120).optional(),
38
+ enabled: z.boolean().optional(),
39
+ environmentId: z.string().trim().min(1).max(64).nullish()
40
+ });
41
+ z.object({
42
+ host: policyHostSchema,
43
+ method: policyMethodSchema,
44
+ path: z.string().trim().min(1).max(2048),
45
+ secret: policySecretNameSchema.optional()
46
+ });
47
+ /** A structurally invalid, unverifiable, or expired bundle. */
48
+ var AgentPolicyError = class extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "AgentPolicyError";
52
+ }
53
+ };
54
+ /**
55
+ * Read a bundle **without** verifying its signature — for display only.
56
+ *
57
+ * Named to be hard to misuse: anything that acts on policy must go through
58
+ * {@link verifyAgentPolicy} (or the Rust verifier). The dashboard uses this to
59
+ * render version history it fetched from the API, where the trust question is
60
+ * already settled differently: the API is the one showing you the list.
61
+ */
62
+ function parseAgentPolicyUnverified(envelope) {
63
+ const parts = envelope.trim().split(".");
64
+ if (parts.length !== 3 || parts[0] !== "ap1") throw new AgentPolicyError(`not a policy bundle (expected a ap1. envelope)`);
65
+ let body;
66
+ try {
67
+ body = JSON.parse(utf8Decode$1(fromBase64url(parts[1])));
68
+ } catch (e) {
69
+ throw new AgentPolicyError(`policy bundle body is unreadable: ${e.message}`);
70
+ }
71
+ if (body?.v !== 1) throw new AgentPolicyError(`unsupported policy bundle version ${String(body?.v)}`);
72
+ if (!Array.isArray(body.rules) || !body.signer?.jwk) throw new AgentPolicyError("policy bundle is missing rules or signer");
73
+ return body;
74
+ }
75
+ function utf8Decode$1(bytes) {
76
+ return new TextDecoder().decode(bytes);
77
+ }
78
+ function fromBase64url(text) {
79
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/");
80
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
81
+ const out = new Uint8Array(binary.length);
82
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
83
+ return out;
84
+ }
11
85
  /** All catalog keys as a runtime array (for iteration / zod enums). */
12
86
  const ENTITLEMENT_KEYS = Object.keys({
13
87
  "feature.kms": {
@@ -1188,7 +1262,12 @@ const AUDIT_ACTIONS = [
1188
1262
  "sync.binding_updated",
1189
1263
  "sync.binding_deleted",
1190
1264
  "sync.run_succeeded",
1191
- "sync.run_failed"
1265
+ "sync.run_failed",
1266
+ "agent.created",
1267
+ "agent.updated",
1268
+ "agent.deleted",
1269
+ "agent.policy_published",
1270
+ "agent.policy_rolled_back"
1192
1271
  ];
1193
1272
  /**
1194
1273
  * Transactional notification emails seekrit can send. Each id is one
@@ -1667,7 +1746,8 @@ const SYNC_PROVIDER_KINDS = [
1667
1746
  "netlify",
1668
1747
  "bunnyshell",
1669
1748
  "github-actions",
1670
- "gcp-secret-manager"
1749
+ "gcp-secret-manager",
1750
+ "langgraph-platform"
1671
1751
  ];
1672
1752
  z.enum(SYNC_PROVIDER_KINDS);
1673
1753
  /**
@@ -1948,6 +2028,66 @@ const gcpSecretManagerConnectionConfigSchema = z.object({
1948
2028
  /** Project ID (`acme-prod`) or project number. */
1949
2029
  projectId: gcpProjectSchema
1950
2030
  });
2031
+ /**
2032
+ * The four hosts LangChain runs the deployment control plane on. A LangSmith
2033
+ * account lives in exactly one of them, and an API key minted in one is not
2034
+ * accepted by another — so this is the "which account" half of a LangGraph
2035
+ * Platform connection, the way Cloudflare's account id is.
2036
+ *
2037
+ * `us` is the default because it is what `https://smith.langchain.com` signs
2038
+ * into; the other three are chosen at signup and never change afterwards.
2039
+ */
2040
+ const LANGGRAPH_PLATFORM_REGIONS = [
2041
+ "us",
2042
+ "eu",
2043
+ "apac",
2044
+ "aws-us"
2045
+ ];
2046
+ /**
2047
+ * LangSmith workspace/tenant scope for LangGraph Platform.
2048
+ *
2049
+ * The API key is never here — it is wrapped to the connection's public key and
2050
+ * stored as ciphertext, exactly as Vercel's token is.
2051
+ *
2052
+ * Two optional fields, for two different situations, and setting both is
2053
+ * rejected rather than silently resolved:
2054
+ *
2055
+ * - `region` picks one of {@link LANGGRAPH_PLATFORM_HOSTS}. Omitted means
2056
+ * `us`, which is where an account created at `smith.langchain.com` lives.
2057
+ * - `baseUrl` points the connection at a **self-hosted** LangSmith install,
2058
+ * whose control plane is served from the customer's own host under
2059
+ * `/api-host` rather than from `*.api.host.langchain.com`.
2060
+ *
2061
+ * `tenantId` is the workspace a key was minted in. A workspace-scoped key names
2062
+ * its own tenant and does not need it; an organization-scoped key reaches
2063
+ * several workspaces and gets a bare 403 without it, which is the same trap
2064
+ * Vercel's `teamId` sets — so it is passed through as `X-Tenant-Id` whenever
2065
+ * it is present.
2066
+ */
2067
+ const langgraphPlatformConnectionConfigSchema = z.object({
2068
+ provider: z.literal("langgraph-platform"),
2069
+ /** Control-plane region. Omit for `us`. Mutually exclusive with `baseUrl`. */
2070
+ region: z.enum(LANGGRAPH_PLATFORM_REGIONS).optional(),
2071
+ /**
2072
+ * Self-hosted LangSmith control-plane root, e.g.
2073
+ * `https://langsmith.acme.com/api-host`. Omit for LangChain's own hosts.
2074
+ * Must be `https:` — this URL carries the API key.
2075
+ */
2076
+ baseUrl: z.string().trim().max(300).refine((value) => {
2077
+ let parsed;
2078
+ try {
2079
+ parsed = new URL(value);
2080
+ } catch {
2081
+ return false;
2082
+ }
2083
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password;
2084
+ }, "must be an https:// URL — the self-hosted control-plane root, e.g. https://langsmith.acme.com/api-host").optional(),
2085
+ /** LangSmith workspace (tenant) UUID, sent as `X-Tenant-Id`. */
2086
+ tenantId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangSmith workspace UUID").optional()
2087
+ }).refine((c) => !(c.baseUrl !== void 0 && c.region !== void 0), {
2088
+ message: "set region for a LangChain-hosted account or baseUrl for a self-hosted one, not both",
2089
+ path: ["baseUrl"]
2090
+ });
1951
2091
  const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1952
2092
  vercelConnectionConfigSchema,
1953
2093
  cloudflareWorkersConnectionConfigSchema,
@@ -1964,7 +2104,8 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1964
2104
  netlifyConnectionConfigSchema,
1965
2105
  bunnyshellConnectionConfigSchema,
1966
2106
  githubActionsConnectionConfigSchema,
1967
- gcpSecretManagerConnectionConfigSchema
2107
+ gcpSecretManagerConnectionConfigSchema,
2108
+ langgraphPlatformConnectionConfigSchema
1968
2109
  ]);
1969
2110
  /** Vercel's three deployment targets. A binding writes to one or more. */
1970
2111
  const VERCEL_TARGETS = [
@@ -2675,6 +2816,27 @@ const gcpSecretManagerDestinationSchema = z.object({
2675
2816
  message: "a customer-managed key covers one location — use automatic replication, or a single location",
2676
2817
  path: ["kmsKeyName"]
2677
2818
  });
2819
+ /**
2820
+ * One LangGraph Platform (Agent Server) **deployment**, addressed by its id.
2821
+ *
2822
+ * A deployment is the whole unit here: its secrets are a property of the
2823
+ * deployment, delivered to the agent container as environment variables, and
2824
+ * there is nothing finer to point at — no per-revision or per-graph scope, and
2825
+ * no equivalent of Vercel's `production`/`preview` split. A deployment that
2826
+ * needs different values is a different deployment, so it is a different
2827
+ * binding.
2828
+ *
2829
+ * Validated as a UUID because `PATCH /v2/deployments/{deployment_id}` declares
2830
+ * the path parameter as one: a name or a URL slug in the slot fails validation
2831
+ * at the control plane hours later inside an alarm, with nobody watching. It is
2832
+ * the `id` from `GET /v2/deployments`, and the UUID in the deployment's
2833
+ * dashboard URL.
2834
+ */
2835
+ const langgraphPlatformDestinationSchema = z.object({
2836
+ provider: z.literal("langgraph-platform"),
2837
+ /** Deployment UUID, from the dashboard URL or `GET /v2/deployments`. */
2838
+ deploymentId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangGraph Platform deployment UUID")
2839
+ });
2678
2840
  const syncDestinationSchema = z.discriminatedUnion("provider", [
2679
2841
  vercelDestinationSchema,
2680
2842
  cloudflareWorkersDestinationSchema,
@@ -2691,7 +2853,8 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
2691
2853
  netlifyDestinationSchema,
2692
2854
  bunnyshellDestinationSchema,
2693
2855
  githubActionsDestinationSchema,
2694
- gcpSecretManagerDestinationSchema
2856
+ gcpSecretManagerDestinationSchema,
2857
+ langgraphPlatformDestinationSchema
2695
2858
  ]);
2696
2859
  /**
2697
2860
  * How seekrit secret names become destination key names. Applied in order:
@@ -3897,7 +4060,7 @@ function isCliSessionToken(value) {
3897
4060
  }
3898
4061
  //#endregion
3899
4062
  //#region package.json
3900
- var version = "0.41.0";
4063
+ var version = "0.43.0";
3901
4064
  //#endregion
3902
4065
  //#region ../../packages/api-client/src/index.ts
3903
4066
  var SeekritApiError = class extends Error {
@@ -4219,6 +4382,61 @@ var SeekritClient = class {
4219
4382
  deleteHoneyToken(orgId, honeyTokenId) {
4220
4383
  return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
4221
4384
  }
4385
+ listAgents(orgId) {
4386
+ return this.request("GET", `/v1/orgs/${orgId}/agents`);
4387
+ }
4388
+ createAgent(orgId, input) {
4389
+ return this.request("POST", `/v1/orgs/${orgId}/agents`, input);
4390
+ }
4391
+ getAgent(orgId, agentId) {
4392
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}`);
4393
+ }
4394
+ updateAgent(orgId, agentId, input) {
4395
+ return this.request("PATCH", `/v1/orgs/${orgId}/agents/${agentId}`, input);
4396
+ }
4397
+ deleteAgent(orgId, agentId) {
4398
+ return this.request("DELETE", `/v1/orgs/${orgId}/agents/${agentId}`);
4399
+ }
4400
+ /** Published versions, newest first. Append-only; nothing here is rewritten. */
4401
+ listAgentPolicies(orgId, agentId) {
4402
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/policies`);
4403
+ }
4404
+ /**
4405
+ * Publish a bundle signed in the browser.
4406
+ *
4407
+ * The signing happens client-side (`signAgentPolicy` in `@seekrit/core`) with
4408
+ * the publishing admin's own key, so the API receives an opaque envelope it
4409
+ * cannot forge. A version mismatch answers `409`: the version is inside the
4410
+ * signature, so a concurrent publish has to be re-signed, not patched.
4411
+ */
4412
+ publishAgentPolicy(orgId, agentId, bundle) {
4413
+ return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies`, { bundle });
4414
+ }
4415
+ /** Republish an earlier version's bundle as the newest version. */
4416
+ rollbackAgentPolicy(orgId, agentId, version) {
4417
+ return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies/${version}/rollback`);
4418
+ }
4419
+ /** The caller's own signing thumbprint, for the trust-anchor snippet. */
4420
+ getMyPolicySigner(orgId) {
4421
+ return this.request("GET", `/v1/orgs/${orgId}/agents/signers/me`);
4422
+ }
4423
+ /**
4424
+ * The bundle a proxy would see — `GET /v1/agents/:ref/policy`, the same route
4425
+ * `seekrit-proxy` polls, resolved by agent id or slug.
4426
+ *
4427
+ * Not org-scoped, because the caller is not: a proxy holds a service token that
4428
+ * knows an agent slug and nothing about org ids. Reachable with any service
4429
+ * token bound to the agent's org (or a user session), which is what lets
4430
+ * `seekrit proxy init` generate a config on the machine that holds the proxy's
4431
+ * own token rather than requiring an admin credential there.
4432
+ *
4433
+ * The `bundle` is signed and opaque to the API. Anything that *acts* on it must
4434
+ * verify the signature against locally pinned signers; decoding it for display
4435
+ * or to name a route is not acting on it.
4436
+ */
4437
+ getAgentPolicyBundle(agentRef) {
4438
+ return this.request("GET", `/v1/agents/${encodeURIComponent(agentRef)}/policy`);
4439
+ }
4222
4440
  /** Keys the caller can see: all org keys for admins, granted keys otherwise. */
4223
4441
  listKmsKeys(orgId) {
4224
4442
  return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
@@ -7280,6 +7498,1024 @@ function collect$2(value, acc) {
7280
7498
  return acc;
7281
7499
  }
7282
7500
  //#endregion
7501
+ //#region src/proxy-binary.ts
7502
+ /**
7503
+ * Fetch and run the `seekrit-proxy` binary without a Rust toolchain.
7504
+ *
7505
+ * The proxy is the strongest answer seekrit has for an untrusted workload — the
7506
+ * agent holds `{{seekrit:NAME}}` and never the key — and it was also the hardest
7507
+ * thing here to *try*, because trying it meant `cargo` and a TOML file. This
7508
+ * module removes the first half: it resolves a prebuilt, checksum-verified
7509
+ * binary for the host platform and execs it, so `npx @seekrit/proxy` and
7510
+ * `seekrit proxy run` behave like the proxy was already installed.
7511
+ *
7512
+ * The logic lives in the CLI (and is re-exported as `@seekrit/cli/proxy-launcher`)
7513
+ * for the same reason the MCP server does: `@seekrit/proxy` is a thin npx
7514
+ * entrypoint over it, and the two must not drift.
7515
+ *
7516
+ * Three properties worth stating, since this downloads and executes code:
7517
+ *
7518
+ * - **The checksum is verified before anything is executed**, against a
7519
+ * `.sha256` fetched from the same release. That is integrity, not provenance —
7520
+ * it proves the bytes match what the release published, which is exactly the
7521
+ * guarantee `install.sh` gives and no more.
7522
+ * - **Nothing is fetched when a binary is already available.** `SEEKRIT_PROXY_BIN`
7523
+ * short-circuits entirely, and a cached download for the same version+target is
7524
+ * reused, so this is a one-time cost per version.
7525
+ * - **Version is pinned, not floating.** A default of `latest` would make two
7526
+ * machines run different proxies from the same command; the pinned constant is
7527
+ * what this CLI was built against, overridable when you want otherwise.
7528
+ */
7529
+ /**
7530
+ * The proxy version this CLI was built against.
7531
+ *
7532
+ * Bumped by release-please when `apps/proxy` releases (an `extra-files` entry in
7533
+ * release-please-config.json), so the pin follows the crate without anyone
7534
+ * remembering to move it.
7535
+ */
7536
+ const PROXY_VERSION = "0.8.0";
7537
+ const BIN = "seekrit-proxy";
7538
+ /**
7539
+ * Host → Rust target triple.
7540
+ *
7541
+ * Linux always resolves to **musl**: that build is statically linked, so one
7542
+ * artifact covers glibc, musl, alpine, and distroless, and there is no libc
7543
+ * detection to get wrong on a machine where `ldd` says something unexpected.
7544
+ */
7545
+ function detectTarget(os = platform(), cpu = arch()) {
7546
+ const machine = cpu === "x64" ? "x86_64" : cpu === "arm64" ? "aarch64" : null;
7547
+ if (!machine) throw new Error(`unsupported architecture "${cpu}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
7548
+ switch (os) {
7549
+ case "linux": return {
7550
+ target: `${machine}-unknown-linux-musl`,
7551
+ exe: ""
7552
+ };
7553
+ case "darwin": return {
7554
+ target: `${machine}-apple-darwin`,
7555
+ exe: ""
7556
+ };
7557
+ case "win32":
7558
+ if (machine !== "x86_64") throw new Error(`no prebuilt seekrit-proxy for ${machine} Windows — set SEEKRIT_PROXY_BIN to a binary you built`);
7559
+ return {
7560
+ target: "x86_64-pc-windows-msvc",
7561
+ exe: ".exe"
7562
+ };
7563
+ default: throw new Error(`unsupported platform "${os}" — build from source (apps/proxy) or set SEEKRIT_PROXY_BIN`);
7564
+ }
7565
+ }
7566
+ /** `latest` stays `latest`; everything else is normalized to `v<x.y.z>`. */
7567
+ function versionPrefix(version) {
7568
+ if (version === "latest") return "latest";
7569
+ return version.startsWith("v") ? version : `v${version}`;
7570
+ }
7571
+ function resolveVersion(explicit) {
7572
+ return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.8.0";
7573
+ }
7574
+ function resolveBaseUrl(explicit) {
7575
+ return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
7576
+ }
7577
+ /** Where a resolved binary is kept, keyed so versions and targets never collide. */
7578
+ function proxyBinaryPath(version, target, exe) {
7579
+ return join(defaultCacheDir(), "proxy", versionPrefix(version), target, `${BIN}${exe}`);
7580
+ }
7581
+ async function fetchBytes(url) {
7582
+ const res = await fetch(url);
7583
+ if (!res.ok) throw new Error(`GET ${url} → ${res.status} ${res.statusText}`);
7584
+ return new Uint8Array(await res.arrayBuffer());
7585
+ }
7586
+ /**
7587
+ * Ensure a `seekrit-proxy` binary exists locally and return its path.
7588
+ *
7589
+ * Order: an explicit `SEEKRIT_PROXY_BIN`, then a cached download for this
7590
+ * version+target, then a fresh download. A binary already on `PATH` is
7591
+ * deliberately *not* used — silently running a different version than the one
7592
+ * this CLI pins is the kind of surprise that costs an afternoon.
7593
+ */
7594
+ async function resolveProxyBinary(options = {}) {
7595
+ const override = process.env.SEEKRIT_PROXY_BIN;
7596
+ if (override) {
7597
+ if (!existsSync(override)) throw new Error(`SEEKRIT_PROXY_BIN points at ${override}, which does not exist`);
7598
+ return override;
7599
+ }
7600
+ const version = resolveVersion(options.version);
7601
+ const { target, exe } = detectTarget();
7602
+ const dest = proxyBinaryPath(version, target, exe);
7603
+ if (!options.force && version !== "latest" && existsSync(dest)) return dest;
7604
+ const baseUrl = resolveBaseUrl(options.baseUrl);
7605
+ const prefix = versionPrefix(version);
7606
+ const name = `${BIN}-${target}${exe}`;
7607
+ const binUrl = `${baseUrl}/${prefix}/bin/${name}`;
7608
+ const sumUrl = `${binUrl}.sha256`;
7609
+ if (!options.quiet) process.stderr.write(`seekrit: fetching ${BIN} ${prefix} (${target})…\n`);
7610
+ let bytes;
7611
+ let expected;
7612
+ try {
7613
+ [bytes, expected] = await Promise.all([fetchBytes(binUrl), fetchBytes(sumUrl).then((b) => Buffer.from(b).toString("utf8").trim().split(/\s+/)[0] ?? "")]);
7614
+ } catch (err) {
7615
+ const message = err instanceof Error ? err.message : String(err);
7616
+ 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.`);
7617
+ }
7618
+ const actual = createHash("sha256").update(bytes).digest("hex");
7619
+ if (!expected || actual !== expected.toLowerCase()) throw new Error(`checksum mismatch for ${name}: expected ${expected || "(none published)"}, got ${actual}. Refusing to run it.`);
7620
+ const dir = dirname(dest);
7621
+ mkdirSync(dir, { recursive: true });
7622
+ const staging = join(dir, `.${BIN}-${process.pid}-${actual.slice(0, 12)}${exe}`);
7623
+ try {
7624
+ writeFileSync(staging, bytes, { mode: 493 });
7625
+ renameSync(staging, dest);
7626
+ } catch (err) {
7627
+ rmSync(staging, { force: true });
7628
+ throw err;
7629
+ }
7630
+ chmodSync(dest, 493);
7631
+ return dest;
7632
+ }
7633
+ /**
7634
+ * Run the proxy, forwarding stdio, signals, and its exit status.
7635
+ *
7636
+ * The proxy is a long-lived foreground process, so this wrapper has to be
7637
+ * transparent: Node cannot exec-replace itself, and without relaying signals
7638
+ * Node's default SIGINT handler would kill *this* process on Ctrl-C and leave
7639
+ * the proxy running, holding decrypted secrets, with the shell prompt back.
7640
+ */
7641
+ async function runProxyBinary(argv, options = {}) {
7642
+ const bin = await resolveProxyBinary(options);
7643
+ const child = spawn(bin, argv, {
7644
+ stdio: "inherit",
7645
+ env: {
7646
+ ...process.env,
7647
+ ...options.env
7648
+ }
7649
+ });
7650
+ const signals = [
7651
+ "SIGINT",
7652
+ "SIGTERM",
7653
+ "SIGHUP",
7654
+ "SIGQUIT"
7655
+ ];
7656
+ const forward = (signal) => {
7657
+ if (child.exitCode !== null || child.signalCode !== null) return;
7658
+ child.kill(signal);
7659
+ };
7660
+ for (const signal of signals) process.on(signal, forward);
7661
+ return new Promise((resolve, reject) => {
7662
+ child.on("error", (err) => {
7663
+ for (const s of signals) process.off(s, forward);
7664
+ 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.`));
7665
+ });
7666
+ child.on("exit", (code, signal) => {
7667
+ for (const s of signals) process.off(s, forward);
7668
+ resolve(signal ? 128 + signalNumber(signal) : code ?? 0);
7669
+ });
7670
+ });
7671
+ }
7672
+ /** Signal name → number, for the 128+n exit convention. */
7673
+ function signalNumber(signal) {
7674
+ return {
7675
+ SIGHUP: 1,
7676
+ SIGINT: 2,
7677
+ SIGQUIT: 3,
7678
+ SIGKILL: 9,
7679
+ SIGTERM: 15
7680
+ }[signal] ?? 0;
7681
+ }
7682
+ //#endregion
7683
+ //#region src/proxy-presets.ts
7684
+ /** `Authorization: Bearer {{seekrit:NAME}}` — the shape most providers take. */
7685
+ function placeholder(secret) {
7686
+ return `{{seekrit:${secret}}}`;
7687
+ }
7688
+ /**
7689
+ * The catalogue. Ordered as `seekrit proxy presets` prints it: the two model
7690
+ * APIs an agent almost certainly calls, then the aggregators, then the generic
7691
+ * escape hatches.
7692
+ */
7693
+ const PROXY_PRESETS = [
7694
+ {
7695
+ id: "openai",
7696
+ label: "OpenAI API (api.openai.com)",
7697
+ host: "api.openai.com",
7698
+ prefix: "/openai",
7699
+ secret: "OPENAI_API_KEY",
7700
+ methods: ["GET", "POST"],
7701
+ paths: ["/v1/**"],
7702
+ baseUrlSuffix: "/v1",
7703
+ env: (mode) => mode === "reverse" ? [{
7704
+ name: "OPENAI_BASE_URL",
7705
+ value: "{{base}}"
7706
+ }, {
7707
+ name: "OPENAI_API_KEY",
7708
+ value: placeholder("OPENAI_API_KEY")
7709
+ }] : [{
7710
+ name: "OPENAI_API_KEY",
7711
+ value: placeholder("OPENAI_API_KEY")
7712
+ }]
7713
+ },
7714
+ {
7715
+ id: "anthropic",
7716
+ label: "Anthropic API (api.anthropic.com)",
7717
+ host: "api.anthropic.com",
7718
+ prefix: "/anthropic",
7719
+ secret: "ANTHROPIC_API_KEY",
7720
+ methods: ["GET", "POST"],
7721
+ paths: ["/v1/**"],
7722
+ baseUrlSuffix: "",
7723
+ env: (mode) => mode === "reverse" ? [{
7724
+ name: "ANTHROPIC_BASE_URL",
7725
+ value: "{{base}}"
7726
+ }, {
7727
+ name: "ANTHROPIC_API_KEY",
7728
+ value: placeholder("ANTHROPIC_API_KEY")
7729
+ }] : [{
7730
+ name: "ANTHROPIC_API_KEY",
7731
+ value: placeholder("ANTHROPIC_API_KEY")
7732
+ }]
7733
+ },
7734
+ {
7735
+ id: "openrouter",
7736
+ label: "OpenRouter (openrouter.ai) — OpenAI-compatible",
7737
+ host: "openrouter.ai",
7738
+ prefix: "/openrouter",
7739
+ secret: "OPENROUTER_API_KEY",
7740
+ methods: ["GET", "POST"],
7741
+ paths: ["/api/v1/**"],
7742
+ baseUrlSuffix: "/api/v1",
7743
+ env: (mode) => mode === "reverse" ? [{
7744
+ name: "OPENAI_BASE_URL",
7745
+ value: "{{base}}"
7746
+ }, {
7747
+ name: "OPENAI_API_KEY",
7748
+ value: placeholder("OPENROUTER_API_KEY")
7749
+ }] : [{
7750
+ name: "OPENROUTER_API_KEY",
7751
+ value: placeholder("OPENROUTER_API_KEY")
7752
+ }]
7753
+ },
7754
+ {
7755
+ id: "github",
7756
+ label: "GitHub REST + GraphQL API (api.github.com)",
7757
+ host: "api.github.com",
7758
+ prefix: "/github",
7759
+ secret: "GITHUB_TOKEN",
7760
+ methods: [],
7761
+ paths: [],
7762
+ baseUrlSuffix: "",
7763
+ env: (mode) => mode === "reverse" ? [{
7764
+ name: "GITHUB_API_URL",
7765
+ value: "{{base}}"
7766
+ }, {
7767
+ name: "GITHUB_TOKEN",
7768
+ value: placeholder("GITHUB_TOKEN")
7769
+ }] : [{
7770
+ name: "GITHUB_TOKEN",
7771
+ value: placeholder("GITHUB_TOKEN")
7772
+ }],
7773
+ note: "`gh` resolves api.github.com from GH_HOST, not a base URL — prefer forward mode for it."
7774
+ },
7775
+ {
7776
+ id: "openai-compatible",
7777
+ label: "Any OpenAI-compatible gateway — LiteLLM, vLLM, Ollama, Together, self-hosted",
7778
+ host: "",
7779
+ prefix: "/gateway",
7780
+ secret: "OPENAI_API_KEY",
7781
+ methods: ["GET", "POST"],
7782
+ paths: ["/v1/**"],
7783
+ baseUrlSuffix: "/v1",
7784
+ requiresBaseUrl: true,
7785
+ env: (mode) => mode === "reverse" ? [{
7786
+ name: "OPENAI_BASE_URL",
7787
+ value: "{{base}}"
7788
+ }, {
7789
+ name: "OPENAI_API_KEY",
7790
+ value: placeholder("OPENAI_API_KEY")
7791
+ }] : [{
7792
+ name: "OPENAI_API_KEY",
7793
+ value: placeholder("OPENAI_API_KEY")
7794
+ }],
7795
+ note: "Needs --base-url (e.g. --base-url https://litellm.internal:4000)."
7796
+ }
7797
+ ];
7798
+ function findPreset(id) {
7799
+ return PROXY_PRESETS.find((p) => p.id === id);
7800
+ }
7801
+ function presetIds() {
7802
+ return PROXY_PRESETS.map((p) => p.id);
7803
+ }
7804
+ /**
7805
+ * Apply `--base-url` / `--secret` / `--prefix` overrides to a preset.
7806
+ *
7807
+ * Returns a new preset rather than mutating the catalogue entry: the same
7808
+ * process can generate two configs in one run (a test does), and a preset that
7809
+ * remembered the last `--base-url` would be a genuinely confusing bug.
7810
+ */
7811
+ function specialize(preset, overrides) {
7812
+ let host = preset.host;
7813
+ let paths = preset.paths;
7814
+ let baseUrlSuffix = preset.baseUrlSuffix;
7815
+ if (overrides.baseUrl) {
7816
+ const url = new URL(overrides.baseUrl);
7817
+ host = url.hostname.toLowerCase();
7818
+ const upstreamPath = url.pathname.replace(/\/+$/, "");
7819
+ if (upstreamPath) {
7820
+ paths = [];
7821
+ baseUrlSuffix = `${upstreamPath}${preset.baseUrlSuffix}`;
7822
+ }
7823
+ }
7824
+ const secret = overrides.secret ?? preset.secret;
7825
+ const specialized = {
7826
+ ...preset,
7827
+ host,
7828
+ paths,
7829
+ baseUrlSuffix,
7830
+ secret,
7831
+ prefix: overrides.prefix ?? preset.prefix
7832
+ };
7833
+ if (preset.allow && secret !== preset.secret) specialized.allow = preset.allow.map((name) => name === preset.secret ? secret : name);
7834
+ if (secret !== preset.secret) specialized.env = (mode) => preset.env(mode).map((hint) => ({
7835
+ ...hint,
7836
+ value: hint.value.replace(/\{\{seekrit:[A-Za-z0-9_]+\}\}/, placeholder(secret))
7837
+ }));
7838
+ return specialized;
7839
+ }
7840
+ /** The secret names a preset's rule permits. Default-deny: empty means none. */
7841
+ function presetAllow(preset) {
7842
+ if (preset.allow) return preset.allow;
7843
+ return preset.secret ? [preset.secret] : [];
7844
+ }
7845
+ //#endregion
7846
+ //#region src/proxy-config.ts
7847
+ /**
7848
+ * A deliberately small TOML writer: basic strings and arrays of them, which is
7849
+ * every value in this config. Full TOML is not needed and a general emitter
7850
+ * would be one more thing that can disagree with the parser on an edge case.
7851
+ */
7852
+ function tomlString(value) {
7853
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, (c) => {
7854
+ return `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`;
7855
+ })}"`;
7856
+ }
7857
+ function tomlArray(values) {
7858
+ return `[${values.map(tomlString).join(", ")}]`;
7859
+ }
7860
+ /**
7861
+ * Claim `preferred` if nothing else has it, else derive one from `host`.
7862
+ *
7863
+ * Two rules can name the same preset-known host (a narrow rule above a broad
7864
+ * one is the documented pattern), and two routes cannot share a prefix — so the
7865
+ * second one needs a distinct, still-recognisable name rather than an error.
7866
+ */
7867
+ function claimPrefix(preferred, host, taken) {
7868
+ if (preferred && !taken.has(preferred)) {
7869
+ taken.add(preferred);
7870
+ return preferred;
7871
+ }
7872
+ return prefixForHost(host, taken);
7873
+ }
7874
+ /** Slug for a route prefix, derived from a hostname. */
7875
+ function prefixForHost(host, taken) {
7876
+ const labels = host.split(".").filter(Boolean);
7877
+ while (labels.length > 1 && (labels[0] === "api" || labels[0] === "www")) labels.shift();
7878
+ const base = (labels[0] ?? "upstream").replace(/[^a-z0-9-]/gi, "").toLowerCase() || "upstream";
7879
+ let prefix = `/${base}`;
7880
+ let n = 2;
7881
+ while (taken.has(prefix)) prefix = `/${base}-${n++}`;
7882
+ taken.add(prefix);
7883
+ return prefix;
7884
+ }
7885
+ const HEADER = `# seekrit-proxy configuration — generated by \`seekrit proxy init\`.
7886
+ #
7887
+ # The proxy resolves the secrets its service token grants (SEEKRIT_TOKEN in the
7888
+ # environment, never in this file), then swaps {{seekrit:NAME}} placeholders in
7889
+ # outbound requests for the decrypted values before forwarding upstream.
7890
+ #
7891
+ # Safe to commit: it contains hostnames, secret *names*, and thumbprints — no
7892
+ # secret values and no credential. Review it before you rely on it; the
7893
+ # allowlist below is a security boundary, and a generator does not know your
7894
+ # threat model.`;
7895
+ /** Render the plan as the text of a `seekrit-proxy.toml`. */
7896
+ function renderProxyConfig(plan) {
7897
+ const out = [HEADER];
7898
+ const server = Boolean(plan.policy);
7899
+ if (plan.notes.length > 0) {
7900
+ out.push("#");
7901
+ for (const note of plan.notes) out.push(`# ${note}`);
7902
+ }
7903
+ out.push("");
7904
+ const reverse = plan.mode === "reverse" || plan.mode === "both";
7905
+ const forward = plan.mode === "forward" || plan.mode === "both";
7906
+ if (reverse) {
7907
+ out.push(`listen = ${tomlString(plan.listen)}`);
7908
+ out.push("");
7909
+ } else {
7910
+ out.push("# Forward-proxy only: the reverse plane still binds this address and");
7911
+ out.push("# serves nothing, since no [[route]] is declared below.");
7912
+ out.push(`listen = ${tomlString(plan.listen)}`);
7913
+ out.push("");
7914
+ }
7915
+ if (reverse) for (const route of plan.routes) {
7916
+ out.push("[[route]]");
7917
+ out.push(`prefix = ${tomlString(route.prefix)}`);
7918
+ out.push(`upstream = ${tomlString(route.upstream)}`);
7919
+ if (server) out.push("# allow/methods/paths come from published policy in server mode.");
7920
+ else {
7921
+ if (route.allow.length > 0) out.push(`allow = ${tomlArray(route.allow)}`);
7922
+ else out.push("# No `allow`: this route permits the operation but carries no credential.");
7923
+ if (route.methods.length > 0) out.push(`methods = ${tomlArray(route.methods)}`);
7924
+ if (route.paths.length > 0) out.push(`paths = ${tomlArray(route.paths)}`);
7925
+ if (route.label) out.push(`label = ${tomlString(route.label)}`);
7926
+ }
7927
+ out.push("");
7928
+ }
7929
+ if (forward) {
7930
+ out.push("[forward]");
7931
+ out.push(`listen = ${tomlString(plan.forwardListen)}`);
7932
+ out.push(`unmatched_host_policy = ${tomlString(plan.unmatched)}`);
7933
+ out.push(`ca_cert = ${tomlString(plan.caCert)}`);
7934
+ out.push(`ca_key = ${tomlString(plan.caKey)}`);
7935
+ out.push("");
7936
+ if (server) {
7937
+ out.push("# Intercepted hosts come from published policy in server mode, so there are");
7938
+ out.push("# no [[forward.host]] blocks here — adding an upstream is a dashboard change.");
7939
+ out.push("");
7940
+ } else for (const route of plan.routes) {
7941
+ out.push("[[forward.host]]");
7942
+ out.push(`match = ${tomlString(route.host)}`);
7943
+ if (route.allow.length > 0) out.push(`allow = ${tomlArray(route.allow)}`);
7944
+ else out.push("# No `allow`: reachable, but no credential travels toward it.");
7945
+ if (route.methods.length > 0) out.push(`methods = ${tomlArray(route.methods)}`);
7946
+ if (route.paths.length > 0) out.push(`paths = ${tomlArray(route.paths)}`);
7947
+ if (route.label) out.push(`label = ${tomlString(route.label)}`);
7948
+ out.push("");
7949
+ }
7950
+ }
7951
+ if (plan.policy) {
7952
+ out.push("# ---------------------------------------------------------------------------");
7953
+ out.push("# Rules come from agent access policy in the dashboard. The bundle is signed");
7954
+ out.push("# in a publishing admin's browser, and this proxy refuses any bundle not");
7955
+ out.push("# signed by a key whose thumbprint is pinned below. So seekrit can withhold");
7956
+ out.push("# your policy (the proxy then fails closed) but cannot widen it.");
7957
+ out.push("# ---------------------------------------------------------------------------");
7958
+ out.push("[policy]");
7959
+ out.push("source = \"server\"");
7960
+ out.push(`agent = ${tomlString(plan.policy.agent)}`);
7961
+ if (plan.policy.agents.length > 1) out.push(`agents = ${tomlArray(plan.policy.agents)}`);
7962
+ out.push(`refresh_interval = ${tomlString(plan.policy.refreshInterval)}`);
7963
+ out.push("");
7964
+ out.push("# THE TRUST ANCHOR — the one value that must not come from the API. Confirm");
7965
+ out.push("# each thumbprint against the dashboard's trust-anchor panel before relying");
7966
+ out.push("# on this file, and pin a second admin's key too: one pinned signer means one");
7967
+ out.push("# lost passphrase leaves nobody able to publish.");
7968
+ for (const signer of plan.policy.signers) out.push(signer.provenance === "own" ? `# ${signer.thumbprint} — your own signing key (derived locally).` : `# ${signer.thumbprint} — read from the policy the API served. VERIFY THIS.`);
7969
+ if (plan.policy.signers.length === 0) {
7970
+ out.push("# (none found — paste the thumbprint from the dashboard.)");
7971
+ out.push("# signers = [\"<thumbprint>\"]");
7972
+ out.push("signers = [] # ← the proxy refuses to start until this is filled in.");
7973
+ } else out.push(`signers = ${tomlArray(plan.policy.signers.map((s) => s.thumbprint))}`);
7974
+ out.push("");
7975
+ } else if (plan.secretsRefresh) {
7976
+ out.push("# Re-resolve on an interval, so a secret added later reaches a running proxy.");
7977
+ out.push("# No new grant is involved: a new secret in an environment this proxy already");
7978
+ out.push("# has a key grant for decrypts with the key it already holds.");
7979
+ out.push("[secrets]");
7980
+ out.push(`refresh_interval = ${tomlString(plan.secretsRefresh)}`);
7981
+ out.push("");
7982
+ }
7983
+ if (plan.cache) {
7984
+ out.push("# Start on the last (encrypted) resolve response if the API is unreachable.");
7985
+ out.push("# A *refused* resolve still fails closed, and decrypting a cached entry still");
7986
+ out.push("# needs this proxy's service token.");
7987
+ out.push("[cache]");
7988
+ out.push("enabled = true");
7989
+ out.push(`max_age = ${tomlString(plan.cache.maxAge)}`);
7990
+ out.push("");
7991
+ }
7992
+ if (plan.control) {
7993
+ out.push("# One proxy fronting several agents: the orchestrator mints a ticket per");
7994
+ out.push("# agent. Requires SEEKRIT_PROXY_CONTROL_TOKEN in the environment, and that");
7995
+ out.push("# token must not be readable by the agents.");
7996
+ out.push("[control]");
7997
+ out.push(`listen = ${tomlString(plan.control.listen)}`);
7998
+ out.push(`ttl = ${tomlString(plan.control.ttl)}`);
7999
+ out.push(`max_ttl = ${tomlString(plan.control.maxTtl)}`);
8000
+ out.push("");
8001
+ }
8002
+ if (plan.envHints.length > 0) {
8003
+ out.push("# ---------------------------------------------------------------------------");
8004
+ out.push("# Point the workload at the proxy (these go in its environment, not here):");
8005
+ out.push("#");
8006
+ for (const hint of plan.envHints) {
8007
+ out.push(`# export ${hint.name}='${hint.value}'`);
8008
+ if (hint.note) out.push(`# ${hint.note}`);
8009
+ }
8010
+ out.push("# ---------------------------------------------------------------------------");
8011
+ }
8012
+ return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
8013
+ }
8014
+ const PLAN_DEFAULTS = {
8015
+ mode: "reverse",
8016
+ listen: "127.0.0.1:8080",
8017
+ forwardListen: "127.0.0.1:8081",
8018
+ unmatched: "tunnel",
8019
+ caCert: "seekrit-proxy-ca.pem",
8020
+ caKey: "seekrit-proxy-ca-key.pem"
8021
+ };
8022
+ /** The base URL a workload points an SDK at, for a route in reverse mode. */
8023
+ function baseUrlFor(listen, prefix, suffix) {
8024
+ const [hostPart = "127.0.0.1", port = "8080"] = splitHostPort(listen);
8025
+ return `http://${hostPart === "0.0.0.0" || hostPart === "[::]" ? "127.0.0.1" : hostPart}:${port}${prefix}${suffix}`;
8026
+ }
8027
+ /** Split `host:port`, tolerating a bracketed IPv6 literal. */
8028
+ function splitHostPort(addr) {
8029
+ const bracketed = /^\[(.+)\]:(\d+)$/.exec(addr);
8030
+ if (bracketed) return [`[${bracketed[1]}]`, bracketed[2]];
8031
+ const idx = addr.lastIndexOf(":");
8032
+ if (idx === -1) return [addr, "8080"];
8033
+ return [addr.slice(0, idx), addr.slice(idx + 1)];
8034
+ }
8035
+ /** Build a file-policy plan from presets and/or ad-hoc `host=SECRET` rules. */
8036
+ function planFromPresets(presets, options) {
8037
+ const taken = /* @__PURE__ */ new Set();
8038
+ const routes = [];
8039
+ const envHints = [];
8040
+ const notes = [];
8041
+ const hintMode = options.mode === "forward" ? "forward" : "reverse";
8042
+ for (const preset of presets) {
8043
+ const prefix = claimPrefix(preset.prefix, preset.host, taken);
8044
+ const baseUrl = baseUrlFor(options.listen, prefix, preset.baseUrlSuffix);
8045
+ routes.push({
8046
+ prefix,
8047
+ upstream: `https://${preset.host}`,
8048
+ host: preset.host,
8049
+ allow: presetAllow(preset),
8050
+ methods: preset.methods,
8051
+ paths: preset.paths,
8052
+ label: preset.label,
8053
+ baseUrl
8054
+ });
8055
+ for (const hint of preset.env(hintMode)) envHints.push({
8056
+ ...hint,
8057
+ value: hint.value.replace("{{base}}", baseUrl)
8058
+ });
8059
+ if (preset.note) notes.push(`${preset.id}: ${preset.note}`);
8060
+ }
8061
+ if (hintMode === "forward") envHints.unshift({
8062
+ name: "HTTPS_PROXY",
8063
+ value: `http://${options.forwardListen}`
8064
+ }, {
8065
+ name: "NODE_EXTRA_CA_CERTS",
8066
+ value: `$PWD/${options.caCert}`,
8067
+ note: "or SSL_CERT_FILE / REQUESTS_CA_BUNDLE, depending on the runtime."
8068
+ });
8069
+ return {
8070
+ mode: options.mode,
8071
+ listen: options.listen,
8072
+ forwardListen: options.forwardListen,
8073
+ routes,
8074
+ unmatched: options.unmatched,
8075
+ caCert: options.caCert,
8076
+ caKey: options.caKey,
8077
+ ...options.cacheMaxAge ? { cache: { maxAge: options.cacheMaxAge } } : {},
8078
+ ...options.secretsRefresh ? { secretsRefresh: options.secretsRefresh } : {},
8079
+ ...options.control ? { control: options.control } : {},
8080
+ envHints,
8081
+ notes
8082
+ };
8083
+ }
8084
+ /**
8085
+ * Build a server-policy plan from an agent's published rules.
8086
+ *
8087
+ * The rules are used for **routing only** — one `[[route]]` per distinct host,
8088
+ * so the workload has a base URL to point at — and never copied into the file as
8089
+ * authorization. That is the whole trade of server mode: adding an upstream
8090
+ * becomes a dashboard change, and a rule this file also stated would be a
8091
+ * startup error rather than a belt-and-braces duplicate.
8092
+ */
8093
+ function planFromPolicy(args, options) {
8094
+ const taken = /* @__PURE__ */ new Set();
8095
+ const routes = [];
8096
+ const envHints = [];
8097
+ const notes = [];
8098
+ const hintMode = options.mode === "forward" ? "forward" : "reverse";
8099
+ const seen = /* @__PURE__ */ new Set();
8100
+ for (const rule of args.rules) {
8101
+ if (!rule.host || seen.has(rule.host)) continue;
8102
+ seen.add(rule.host);
8103
+ const preset = PRESET_BY_HOST.get(rule.host);
8104
+ const prefix = claimPrefix(preset?.prefix, rule.host, taken);
8105
+ const baseUrl = baseUrlFor(options.listen, prefix, preset?.baseUrlSuffix ?? "");
8106
+ routes.push({
8107
+ prefix,
8108
+ upstream: `https://${rule.host}`,
8109
+ host: rule.host,
8110
+ allow: [],
8111
+ methods: [],
8112
+ paths: [],
8113
+ baseUrl
8114
+ });
8115
+ if (preset) for (const hint of preset.env(hintMode)) envHints.push({
8116
+ ...hint,
8117
+ value: hint.value.replace("{{base}}", baseUrl)
8118
+ });
8119
+ }
8120
+ if (hintMode === "forward") envHints.unshift({
8121
+ name: "HTTPS_PROXY",
8122
+ value: `http://${options.forwardListen}`
8123
+ }, {
8124
+ name: "NODE_EXTRA_CA_CERTS",
8125
+ value: `$PWD/${options.caCert}`,
8126
+ note: "or SSL_CERT_FILE / REQUESTS_CA_BUNDLE, depending on the runtime."
8127
+ });
8128
+ if (args.rules.length === 0) notes.push("The published policy has no rules yet, so this proxy permits nothing until one is published.");
8129
+ return {
8130
+ mode: options.mode,
8131
+ listen: options.listen,
8132
+ forwardListen: options.forwardListen,
8133
+ routes,
8134
+ policy: {
8135
+ agent: args.agent,
8136
+ agents: args.agents,
8137
+ refreshInterval: args.refreshInterval,
8138
+ signers: args.signers
8139
+ },
8140
+ unmatched: options.unmatched,
8141
+ caCert: options.caCert,
8142
+ caKey: options.caKey,
8143
+ ...options.cacheMaxAge ? { cache: { maxAge: options.cacheMaxAge } } : {},
8144
+ ...options.control ? { control: options.control } : {},
8145
+ envHints,
8146
+ notes
8147
+ };
8148
+ }
8149
+ /** Host → preset, for naming routes generated from published policy. */
8150
+ const PRESET_BY_HOST = /* @__PURE__ */ new Map();
8151
+ for (const id of [
8152
+ "openai",
8153
+ "anthropic",
8154
+ "openrouter",
8155
+ "github"
8156
+ ]) {
8157
+ const preset = findPreset(id);
8158
+ if (preset?.host) PRESET_BY_HOST.set(preset.host, preset);
8159
+ }
8160
+ /** YAML double-quoted scalar. Compose values here are hostnames and URLs. */
8161
+ function yamlString(value) {
8162
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
8163
+ }
8164
+ const COMPOSE_DEFAULTS = {
8165
+ service: "seekrit-proxy",
8166
+ workload: "agent",
8167
+ publish: false
8168
+ };
8169
+ /**
8170
+ * A `docker compose` sidecar snippet for a generated config.
8171
+ *
8172
+ * The container case differs from the local one in exactly the ways that break a
8173
+ * copied-from-the-docs compose file: the proxy has to bind `0.0.0.0` to be
8174
+ * reachable from a sibling container, the workload dials it by *service name*
8175
+ * rather than loopback, and in forward mode the CA has to live on a shared
8176
+ * volume or the workload trusts a certificate the proxy no longer has.
8177
+ */
8178
+ function renderComposeSnippet(plan, options) {
8179
+ const reverse = plan.mode === "reverse" || plan.mode === "both";
8180
+ const forward = plan.mode === "forward" || plan.mode === "both";
8181
+ const [, listenPort = "8080"] = splitHostPort(plan.listen);
8182
+ const [, forwardPort = "8081"] = splitHostPort(plan.forwardListen);
8183
+ const host = options.service;
8184
+ const out = [
8185
+ "# seekrit-proxy sidecar — generated by `seekrit proxy compose`.",
8186
+ "#",
8187
+ "# The proxy holds the decrypted secrets; the workload holds only placeholders.",
8188
+ "# Keeping them in separate containers is what makes that boundary real: the",
8189
+ "# service token is in the proxy's environment, where the workload cannot read it.",
8190
+ "services:",
8191
+ ` ${host}:`,
8192
+ ` image: ${options.image}`
8193
+ ];
8194
+ const command = [];
8195
+ if (reverse) command.push("--listen", `0.0.0.0:${listenPort}`);
8196
+ if (command.length > 0) out.push(` command: [${command.map(yamlString).join(", ")}]`);
8197
+ if (forward) {
8198
+ out.push(` # Forward mode: set \`[forward] listen = "0.0.0.0:${forwardPort}"\` in the`);
8199
+ out.push(" # config too — there is no flag for the forward plane's address.");
8200
+ }
8201
+ out.push(" environment:");
8202
+ out.push(" # Never inline the token. Compose reads it from your shell or a .env file.");
8203
+ out.push(" SEEKRIT_TOKEN: ${SEEKRIT_TOKEN:?SEEKRIT_TOKEN is required}");
8204
+ out.push(" volumes:");
8205
+ out.push(" - ./seekrit-proxy.toml:/seekrit-proxy.toml:ro");
8206
+ if (forward) {
8207
+ out.push(" # The interception CA must survive restarts, or the certificate the");
8208
+ out.push(" # workload trusts stops matching the one the proxy mints leaves from.");
8209
+ out.push(" - seekrit-proxy-ca:/ca");
8210
+ }
8211
+ if (options.publish) {
8212
+ out.push(" ports:");
8213
+ if (reverse) out.push(` - ${yamlString(`127.0.0.1:${listenPort}:${listenPort}`)}`);
8214
+ if (forward) out.push(` - ${yamlString(`127.0.0.1:${forwardPort}:${forwardPort}`)}`);
8215
+ } else {
8216
+ out.push(" # No `ports`: reachable on the compose network only, which is what you");
8217
+ out.push(" # want — nothing outside this project can ask the proxy to inject a key.");
8218
+ }
8219
+ out.push(" restart: unless-stopped");
8220
+ out.push("");
8221
+ out.push(` ${options.workload}:`);
8222
+ out.push(" # ← your workload. It never holds a real credential.");
8223
+ out.push(" image: your-agent:latest");
8224
+ out.push(" depends_on:");
8225
+ out.push(` - ${host}`);
8226
+ out.push(" environment:");
8227
+ if (forward) {
8228
+ out.push(` HTTPS_PROXY: ${yamlString(`http://${host}:${forwardPort}`)}`);
8229
+ out.push(` HTTP_PROXY: ${yamlString(`http://${host}:${forwardPort}`)}`);
8230
+ out.push(" NODE_EXTRA_CA_CERTS: \"/ca/seekrit-proxy-ca.pem\"");
8231
+ out.push(" # …or SSL_CERT_FILE / REQUESTS_CA_BUNDLE, depending on the runtime.");
8232
+ }
8233
+ for (const hint of plan.envHints) {
8234
+ if (hint.name === "HTTPS_PROXY" || hint.name === "NODE_EXTRA_CA_CERTS") continue;
8235
+ const value = hint.value.replace(/http:\/\/[^/]+/, `http://${host}:${listenPort}`);
8236
+ out.push(` ${hint.name}: ${yamlString(value)}`);
8237
+ }
8238
+ out.push("");
8239
+ if (forward) {
8240
+ out.push("volumes:");
8241
+ out.push(" seekrit-proxy-ca:");
8242
+ out.push("");
8243
+ }
8244
+ out.push(forward ? "# The workload can unset HTTPS_PROXY, so in a threat model where the workload" : "# The workload can ignore the base URL above, so in a threat model where the");
8245
+ out.push(forward ? "# is the adversary, make the proxy the only route out: put the workload on an" : "# workload is the adversary, make the proxy the only route out: put the workload");
8246
+ out.push(forward ? "# `internal: true` network with the proxy as its only peer." : "# on an `internal: true` network with the proxy as its only peer.");
8247
+ return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
8248
+ }
8249
+ //#endregion
8250
+ //#region src/proxy.ts
8251
+ /**
8252
+ * `seekrit proxy` — get `seekrit-proxy` running without a Rust toolchain or a
8253
+ * hand-written TOML file.
8254
+ *
8255
+ * The proxy is the only thing here that keeps a decrypted secret out of an
8256
+ * untrusted workload's memory entirely, and it was also the least likely to be
8257
+ * tried: `cargo build` and a config file, before you learn anything. These
8258
+ * subcommands close that gap from both ends —
8259
+ *
8260
+ * seekrit proxy run --preset openai # nothing installed, nothing written
8261
+ * seekrit proxy init --agent nova # a reviewable file from live policy
8262
+ *
8263
+ * — and none of them changes what the proxy does. The generated config is the
8264
+ * same config; the fetched binary is the released binary, checksum-verified.
8265
+ */
8266
+ const DEFAULT_CONFIG = "seekrit-proxy.toml";
8267
+ /**
8268
+ * The exact grammar `seekrit_cache::parse_duration` accepts: a positive integer,
8269
+ * optionally suffixed `s`/`m`/`h`/`d` (bare means seconds). Notably **not** `ms`
8270
+ * — `--refresh 500ms` is the typo this catches, and catching it here saves a
8271
+ * binary download before the proxy's own startup error.
8272
+ */
8273
+ const DURATION = /^[1-9]\d*[smhd]?$/;
8274
+ function duration(value, flag) {
8275
+ if (value === void 0) return void 0;
8276
+ const trimmed = value.trim();
8277
+ if (!DURATION.test(trimmed)) fail(`${flag} must be a duration like 30s, 10m, 24h, or 7d (got "${value}")`);
8278
+ return trimmed;
8279
+ }
8280
+ function planOptions(options) {
8281
+ const mode = options.mode ?? PLAN_DEFAULTS.mode;
8282
+ if (mode !== "reverse" && mode !== "forward" && mode !== "both") fail(`--mode must be reverse, forward, or both (got "${mode}")`);
8283
+ const unmatched = options.unmatched ?? PLAN_DEFAULTS.unmatched;
8284
+ if (unmatched !== "tunnel" && unmatched !== "deny") fail(`--unmatched must be tunnel or deny (got "${unmatched}")`);
8285
+ const listen = options.listen ?? PLAN_DEFAULTS.listen;
8286
+ const forwardListen = options.forwardListen ?? PLAN_DEFAULTS.forwardListen;
8287
+ if (mode === "both" && listen === forwardListen) fail(`--listen and --forward-listen cannot both be ${listen} — the two planes need separate ports`);
8288
+ if (options.control && (options.control === listen || options.control === forwardListen)) fail(`--control cannot share an address with a data plane (${options.control})`);
8289
+ const refresh = duration(options.refresh, "--refresh");
8290
+ return {
8291
+ mode,
8292
+ listen,
8293
+ forwardListen,
8294
+ unmatched,
8295
+ caCert: options.caCert ?? PLAN_DEFAULTS.caCert,
8296
+ caKey: options.caKey ?? PLAN_DEFAULTS.caKey,
8297
+ ...options.cache || options.cacheMaxAge ? { cacheMaxAge: duration(options.cacheMaxAge, "--cache-max-age") ?? "24h" } : {},
8298
+ ...refresh ? { secretsRefresh: refresh } : {},
8299
+ ...options.control ? { control: {
8300
+ listen: options.control,
8301
+ ttl: "1h",
8302
+ maxTtl: "12h"
8303
+ } } : {}
8304
+ };
8305
+ }
8306
+ /**
8307
+ * Turn `--host api.foo.com=FOO_KEY,BAR_KEY` into a preset-shaped rule.
8308
+ *
8309
+ * The `=SECRET` half is optional on purpose: a rule with no `allow` permits an
8310
+ * operation without letting a credential travel with it, which is a real thing
8311
+ * to want and impossible to express if the flag demanded a secret name.
8312
+ */
8313
+ function presetFromHostSpec(spec, index) {
8314
+ const [hostPart = "", secretPart] = spec.split("=", 2);
8315
+ const host = hostPart.trim().toLowerCase();
8316
+ if (!host) fail(`--host needs a hostname (got "${spec}")`);
8317
+ if (/^[a-z][a-z0-9+.-]*:\/\//.test(host) || host.includes("/")) fail(`--host takes a bare hostname, not a URL (got "${host}")`);
8318
+ if (host.includes(":")) fail(`--host takes a hostname without a port (got "${host}")`);
8319
+ const secrets = (secretPart ?? "").split(",").map((s) => s.trim()).filter(Boolean);
8320
+ for (const name of secrets) if (!/^[A-Za-z0-9_]+$/.test(name)) fail(`"${name}" is not a valid secret name (letters, digits, and _ only)`);
8321
+ const labels = host.split(".").filter(Boolean);
8322
+ while (labels.length > 1 && (labels[0] === "api" || labels[0] === "www")) labels.shift();
8323
+ return {
8324
+ id: host,
8325
+ label: host,
8326
+ host,
8327
+ prefix: `/${(labels[0] ?? `upstream${index}`).replace(/[^a-z0-9-]/g, "") || `upstream${index}`}`,
8328
+ secret: secrets[0] ?? "",
8329
+ allow: secrets,
8330
+ methods: [],
8331
+ paths: [],
8332
+ baseUrlSuffix: "",
8333
+ env: () => secrets[0] ? [{
8334
+ name: secrets[0],
8335
+ value: `{{seekrit:${secrets[0]}}}`
8336
+ }] : [],
8337
+ ...secrets.length > 1 ? { note: `also allows ${secrets.slice(1).join(", ")} — pass each as a placeholder.` } : {}
8338
+ };
8339
+ }
8340
+ /** Collect the presets/hosts a `--preset`/`--host`-driven invocation names. */
8341
+ function gatherPresets(options) {
8342
+ const chosen = [];
8343
+ for (const id of options.preset ?? []) {
8344
+ const preset = findPreset(id);
8345
+ if (!preset) fail(`unknown preset "${id}" — try one of: ${presetIds().join(", ")}`);
8346
+ if (preset.requiresBaseUrl && !options.baseUrl) fail(`preset "${id}" needs --base-url (e.g. --base-url https://litellm.internal:4000)`);
8347
+ chosen.push(specialize(preset, {
8348
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
8349
+ ...options.secret ? { secret: options.secret } : {},
8350
+ ...options.prefix ? { prefix: options.prefix } : {}
8351
+ }));
8352
+ }
8353
+ (options.host ?? []).forEach((spec, i) => {
8354
+ chosen.push(presetFromHostSpec(spec, i));
8355
+ });
8356
+ return chosen;
8357
+ }
8358
+ /**
8359
+ * Read an agent's published policy through the route the proxy itself polls.
8360
+ *
8361
+ * Deliberately the proxy-facing route rather than the org-scoped admin one: the
8362
+ * machine that runs `seekrit proxy init` is usually the machine that holds the
8363
+ * *proxy's* token, not an admin's, and a config generated from the same bytes the
8364
+ * proxy will fetch cannot disagree with what the proxy then does.
8365
+ */
8366
+ async function fetchPolicy(ctx, agentRef, orgSlug) {
8367
+ let served;
8368
+ try {
8369
+ served = await ctx.client.getAgentPolicyBundle(agentRef);
8370
+ } catch (err) {
8371
+ fail(`could not read policy for agent "${agentRef}": ${err instanceof Error ? err.message : String(err)}\n Publish a policy for it in the dashboard first (Agents → the identity → Publish),
8372
+ or generate a file-policy config instead with --preset / --host.`);
8373
+ }
8374
+ let rules = [];
8375
+ try {
8376
+ rules = parseAgentPolicyUnverified(served.bundle).rules;
8377
+ } catch (err) {
8378
+ fail(`the API served a policy bundle this CLI cannot decode: ${err instanceof Error ? err.message : String(err)}`);
8379
+ }
8380
+ const signers = [];
8381
+ try {
8382
+ const org = await resolveOrg(ctx, orgSlug);
8383
+ const { signer } = await ctx.client.getMyPolicySigner(org.id);
8384
+ if (signer) signers.push({
8385
+ thumbprint: signer.thumbprint,
8386
+ provenance: "own"
8387
+ });
8388
+ } catch {}
8389
+ if (!signers.some((s) => s.thumbprint === served.signerThumbprint)) signers.push({
8390
+ thumbprint: served.signerThumbprint,
8391
+ provenance: "published"
8392
+ });
8393
+ return {
8394
+ slug: served.agent.slug,
8395
+ rules,
8396
+ signers,
8397
+ version: served.version
8398
+ };
8399
+ }
8400
+ /** Build the plan a `--agent` / `--preset` invocation describes. */
8401
+ async function buildPlan(options) {
8402
+ const plan = planOptions(options);
8403
+ if (options.agent) {
8404
+ if ((options.preset ?? []).length > 0 || (options.host ?? []).length > 0) fail("--agent takes the rules from published policy, so --preset/--host cannot also apply.\n Server-policy mode rejects local rules rather than silently ignoring them.");
8405
+ const policy = await fetchPolicy(buildContext(), options.agent, options.org);
8406
+ const agents = options.agents?.length ? options.agents : [policy.slug];
8407
+ process.stderr.write(`seekrit: agent ${policy.slug} — policy v${policy.version}, ${policy.rules.length} rule(s)\n`);
8408
+ return planFromPolicy({
8409
+ agent: policy.slug,
8410
+ agents,
8411
+ rules: policy.rules,
8412
+ signers: policy.signers,
8413
+ refreshInterval: plan.secretsRefresh ?? "10s"
8414
+ }, plan);
8415
+ }
8416
+ const presets = gatherPresets(options);
8417
+ if (presets.length === 0) fail("nothing to configure — pass --preset <name> (see `seekrit proxy presets`), --host <host>[=SECRET],\n or --agent <slug> to take the rules from published policy.");
8418
+ return planFromPresets(presets, plan);
8419
+ }
8420
+ /** Add the generation flags to a command, so `init` and `run` stay in step. */
8421
+ function withGenerateOptions(cmd) {
8422
+ 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");
8423
+ }
8424
+ function registerProxyCommands(program) {
8425
+ const proxy = program.command("proxy").description("run and configure the agent egress proxy (`seekrit proxy --help`)");
8426
+ proxy.command("presets").description("list the ready-made upstream presets").option("--json", "machine-readable output").action((options) => {
8427
+ emit(options, { presets: PROXY_PRESETS }, () => {
8428
+ printTable(PROXY_PRESETS, [
8429
+ col("PRESET", (p) => p.id),
8430
+ col("HOST", (p) => p.host || "(--base-url)"),
8431
+ col("SECRET", (p) => p.secret),
8432
+ col("PREFIX", (p) => p.prefix),
8433
+ col("DESCRIPTION", (p) => p.label)
8434
+ ], "no presets");
8435
+ process.stderr.write("\nUse one with: seekrit proxy run --preset openai\nAnything not listed here works too: --host api.example.com=EXAMPLE_API_KEY\n");
8436
+ });
8437
+ });
8438
+ withGenerateOptions(proxy.command("init").description("write a seekrit-proxy.toml from presets or published policy")).option("-o, --out <path>", "where to write it", DEFAULT_CONFIG).option("--print", "write to stdout instead of a file").option("--force", "overwrite an existing file").action(async (options) => {
8439
+ const plan = await buildPlan(options);
8440
+ const text = renderProxyConfig(plan);
8441
+ if (options.print) {
8442
+ process.stdout.write(text);
8443
+ return;
8444
+ }
8445
+ const out = resolve(options.out);
8446
+ if (existsSync(out) && !options.force) fail(`${options.out} already exists — pass --force to overwrite, or --print to review it`);
8447
+ writeFileSync(out, text, { mode: 420 });
8448
+ process.stderr.write(`Wrote ${options.out}\n`);
8449
+ process.stderr.write(`
8450
+ Next:
8451
+ export SEEKRIT_TOKEN=skt_… # a service token with a key grant
8452
+ seekrit proxy run --config ${options.out}\n`);
8453
+ if (plan.policy) process.stderr.write("\nBefore you rely on this: confirm each pinned thumbprint against the dashboard's\ntrust-anchor panel. `signers` is the one field that must not come from the API.\n");
8454
+ });
8455
+ withGenerateOptions(proxy.command("run").description("fetch the proxy binary if needed and run it")).option("-c, --config <path>", "config file to use", DEFAULT_CONFIG).option("--proxy-version <version>", `binary version (default: ${PROXY_VERSION})`).option("--print-config", "print the config that would be used, then exit").action(async (options) => {
8456
+ const generating = Boolean(options.agent) || (options.preset ?? []).length > 0 || (options.host ?? []).length > 0;
8457
+ let configPath = resolve(options.config);
8458
+ let ephemeralDir;
8459
+ if (generating) {
8460
+ const text = renderProxyConfig(await buildPlan(options));
8461
+ if (options.printConfig) {
8462
+ process.stdout.write(text);
8463
+ return;
8464
+ }
8465
+ ephemeralDir = mkdtempSync(join(tmpdir(), "seekrit-proxy-"));
8466
+ configPath = join(ephemeralDir, DEFAULT_CONFIG);
8467
+ writeFileSync(configPath, text, { mode: 384 });
8468
+ process.stderr.write("seekrit: running with a generated config (nothing written to the project — use `seekrit proxy init` to keep it)\n");
8469
+ } else if (options.printConfig) fail("--print-config needs generation flags (--preset/--host/--agent)");
8470
+ else if (!existsSync(configPath)) fail(`no ${options.config} here, and no --preset/--host/--agent to generate one.\n Try: seekrit proxy run --preset openai
8471
+ or: seekrit proxy init --preset openai (to write a reviewable file first)`);
8472
+ if (!process.env.SEEKRIT_TOKEN) process.stderr.write("seekrit: SEEKRIT_TOKEN is not set — the proxy needs a service token with a key\n grant for the environment it serves, and will refuse to start without one.\n");
8473
+ try {
8474
+ process.exitCode = await runProxyBinary(["--config", configPath], { ...options.proxyVersion ? { version: options.proxyVersion } : {} });
8475
+ } finally {
8476
+ if (ephemeralDir) rmSync(ephemeralDir, {
8477
+ recursive: true,
8478
+ force: true
8479
+ });
8480
+ }
8481
+ });
8482
+ proxy.command("install").description("download the proxy binary and print its path").option("--proxy-version <version>", `version to fetch (default: ${PROXY_VERSION})`).option("--force", "re-download even if it is already cached").action(async (options) => {
8483
+ const path = await resolveProxyBinary({
8484
+ ...options.proxyVersion ? { version: options.proxyVersion } : {},
8485
+ ...options.force ? { force: true } : {}
8486
+ });
8487
+ console.log(path);
8488
+ const { target } = detectTarget();
8489
+ process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.8.0")} (${target})\n`);
8490
+ });
8491
+ 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) => {
8492
+ const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.8.0";
8493
+ const override = process.env.SEEKRIT_PROXY_BIN;
8494
+ const { target, exe } = detectTarget();
8495
+ const path = override ?? proxyBinaryPath(version, target, exe);
8496
+ const info = {
8497
+ path,
8498
+ version,
8499
+ target,
8500
+ source: override ? "SEEKRIT_PROXY_BIN" : existsSync(path) ? "cache" : "not-yet-downloaded",
8501
+ baseUrl: process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev"
8502
+ };
8503
+ emit(options, info, () => {
8504
+ console.log(info.path);
8505
+ process.stderr.write(`${info.version} · ${info.target} · ${info.source}\n`);
8506
+ });
8507
+ });
8508
+ 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) => {
8509
+ const plan = await buildPlan(options);
8510
+ process.stdout.write(renderComposeSnippet(plan, {
8511
+ service: options.service,
8512
+ workload: options.workload,
8513
+ image: options.image ?? `seekritdev/proxy:0.8.0`,
8514
+ publish: Boolean(options.publish)
8515
+ }));
8516
+ });
8517
+ }
8518
+ //#endregion
7283
8519
  //#region src/redis.ts
7284
8520
  /**
7285
8521
  * `seekrit redis` — temporary Redis (6+) credentials (Vault-style dynamic
@@ -7943,6 +9179,17 @@ function assertRailwayId(value, flag) {
7943
9179
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) fail(`${flag} should be a Railway UUID, not "${id}"`);
7944
9180
  return id;
7945
9181
  }
9182
+ /**
9183
+ * A LangGraph Platform deployment id is a UUID — the one in its dashboard URL.
9184
+ * The deployment *name* in the slot is the common slip, and it fails at the
9185
+ * control plane rather than here.
9186
+ */
9187
+ function assertLanggraphDeploymentId(value) {
9188
+ if (!value) fail("--langgraph-deployment is required for langgraph-platform (a UUID)");
9189
+ const id = value.trim();
9190
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) fail(`--langgraph-deployment should be the deployment UUID from its dashboard URL, not "${id}"`);
9191
+ return id;
9192
+ }
7946
9193
  /** Reject a single value outside a known set, naming the choices. */
7947
9194
  function assertMember(value, allowed, flag, fallback) {
7948
9195
  if (value === void 0) return fallback;
@@ -8067,6 +9314,7 @@ function assertRepoIds(raw) {
8067
9314
  function credentialNoun(provider) {
8068
9315
  if (provider.startsWith("aws-")) return "secret access key";
8069
9316
  if (provider === "gcp-secret-manager") return "service-account key JSON";
9317
+ if (provider === "langgraph-platform") return "LangSmith API key";
8070
9318
  return "API token";
8071
9319
  }
8072
9320
  /** Account-scope config for a connection (never the credential itself). */
@@ -8123,6 +9371,17 @@ function buildConfig(provider, options) {
8123
9371
  provider: "gcp-secret-manager",
8124
9372
  projectId: options.projectId
8125
9373
  };
9374
+ case "langgraph-platform": {
9375
+ const region = options.langgraphRegion?.trim();
9376
+ if (region && options.baseUrl) fail("pass --langgraph-region for a LangChain-hosted account or --base-url for a self-hosted one, not both");
9377
+ if (region && !LANGGRAPH_PLATFORM_REGIONS.includes(region)) fail(`unknown --langgraph-region ${region} — one of: ${LANGGRAPH_PLATFORM_REGIONS.join(", ")}`);
9378
+ return {
9379
+ provider: "langgraph-platform",
9380
+ ...region ? { region } : {},
9381
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
9382
+ ...options.langgraphTenant ? { tenantId: options.langgraphTenant.trim() } : {}
9383
+ };
9384
+ }
8126
9385
  }
8127
9386
  }
8128
9387
  /** Where inside the platform a binding writes. */
@@ -8321,6 +9580,10 @@ function buildDestination(provider, options) {
8321
9580
  ...options.gcpPruneVersions ? { pruneVersions: true } : {}
8322
9581
  };
8323
9582
  }
9583
+ case "langgraph-platform": return {
9584
+ provider: "langgraph-platform",
9585
+ deploymentId: assertLanggraphDeploymentId(options.langgraphDeployment)
9586
+ };
8324
9587
  }
8325
9588
  }
8326
9589
  /** One-line description of a destination, for list output. */
@@ -8341,6 +9604,7 @@ function describeDestination(destination) {
8341
9604
  case "netlify": return `${destination.siteId} (${destination.contexts.map((context) => context === "branch" ? `branch @${destination.branch}` : context).join(", ")})`;
8342
9605
  case "bunnyshell": return destination.kind === "environment" ? `environment ${destination.environmentId}` : `project ${destination.projectId} (inherited by new environments)`;
8343
9606
  case "gcp-secret-manager": return destination.layout === "json-bundle" ? `${destination.secretId} · json` : `${destination.idPrefix ?? ""}* · one per name`;
9607
+ case "langgraph-platform": return `deployment ${destination.deploymentId}`;
8344
9608
  case "github-actions": switch (destination.kind) {
8345
9609
  case "repo": return `${destination.owner}/${destination.repo}`;
8346
9610
  case "environment": return `${destination.owner}/${destination.repo} @${destination.environment}`;
@@ -8356,7 +9620,7 @@ function describeDestination(destination) {
8356
9620
  * application whose environment the binding reads from.
8357
9621
  */
8358
9622
  function destinationOptions(command) {
8359
- return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug · bunnyshell: project ID (writes variables new environments inherit)").option("--target <list>", "vercel / cloudflare-pages: comma-separated deployment targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--script <name>", "cloudflare-workers: Worker script name").option("--store-id <id>", "cloudflare-secrets-store: store ID (32 hex)").option("--scopes <list>", "cloudflare-secrets-store: comma-separated scopes", "workers").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)").option("--netlify-site <id>", "netlify: site API ID (the UUID under Project configuration)").option("--no-netlify-secret", "netlify: create readable variables instead of Netlify secrets (write-only)").option("--bunnyshell-environment <id>", "bunnyshell: environment ID (omit to write a project)").option("--no-bunnyshell-secret", "bunnyshell: create variables visible in the dashboard instead of secret ones").option("--gh-repo <owner/name>", "github-actions: repository, e.g. acme/storefront").option("--gh-environment <name>", "github-actions: write to one deployment environment's secrets (needs --gh-repo)").option("--gh-org <login>", "github-actions: write organization secrets instead of a repo's").option("--gh-visibility <v>", `github-actions org secrets: ${GITHUB_ACTIONS_VISIBILITIES.join(" | ")}`).option("--gh-repo-ids <ids>", "github-actions: comma-separated numeric repository IDs for --gh-visibility selected").option("--gcp-prefix <prefix>", "gcp: prepended to every secret ID, e.g. prod-storefront-").option("--gcp-replication <policy>", `gcp: ${GCP_REPLICATION_POLICIES.join(" | ")}`, "automatic").option("--gcp-locations <list>", "gcp: regions for user-managed replication, e.g. us-east1").option("--gcp-kms-key <name>", "gcp: Cloud KMS key (projects/…/cryptoKeys/…)").option("--gcp-prune-versions", "gcp: destroy the version each push supersedes, keeping one active version");
9623
+ return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug · bunnyshell: project ID (writes variables new environments inherit)").option("--target <list>", "vercel / cloudflare-pages: comma-separated deployment targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--script <name>", "cloudflare-workers: Worker script name").option("--store-id <id>", "cloudflare-secrets-store: store ID (32 hex)").option("--scopes <list>", "cloudflare-secrets-store: comma-separated scopes", "workers").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)").option("--netlify-site <id>", "netlify: site API ID (the UUID under Project configuration)").option("--no-netlify-secret", "netlify: create readable variables instead of Netlify secrets (write-only)").option("--bunnyshell-environment <id>", "bunnyshell: environment ID (omit to write a project)").option("--no-bunnyshell-secret", "bunnyshell: create variables visible in the dashboard instead of secret ones").option("--gh-repo <owner/name>", "github-actions: repository, e.g. acme/storefront").option("--gh-environment <name>", "github-actions: write to one deployment environment's secrets (needs --gh-repo)").option("--gh-org <login>", "github-actions: write organization secrets instead of a repo's").option("--gh-visibility <v>", `github-actions org secrets: ${GITHUB_ACTIONS_VISIBILITIES.join(" | ")}`).option("--gh-repo-ids <ids>", "github-actions: comma-separated numeric repository IDs for --gh-visibility selected").option("--langgraph-deployment <id>", "langgraph-platform: deployment UUID (the one in its dashboard URL)").option("--gcp-prefix <prefix>", "gcp: prepended to every secret ID, e.g. prod-storefront-").option("--gcp-replication <policy>", `gcp: ${GCP_REPLICATION_POLICIES.join(" | ")}`, "automatic").option("--gcp-locations <list>", "gcp: regions for user-managed replication, e.g. us-east1").option("--gcp-kms-key <name>", "gcp: Cloud KMS key (projects/…/cryptoKeys/…)").option("--gcp-prune-versions", "gcp: destroy the version each push supersedes, keeping one active version");
8360
9624
  }
8361
9625
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
8362
9626
  async function resolveConnection(ctx, orgId, ref) {
@@ -8380,7 +9644,7 @@ function registerSyncCommands(program) {
8380
9644
  col("id", (c) => c.id)
8381
9645
  ], "no connections — add one with `seekrit sync connect`"));
8382
9646
  });
8383
- sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com)").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").action(async (options) => {
9647
+ sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com) · langgraph-platform: self-hosted LangSmith control-plane root").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").option("--langgraph-region <region>", `langgraph-platform: ${LANGGRAPH_PLATFORM_REGIONS.join(" | ")} (omit for us)`).option("--langgraph-tenant <id>", "langgraph-platform: LangSmith workspace UUID (only an org-scoped key needs it)").action(async (options) => {
8384
9648
  const provider = assertProvider(options.provider);
8385
9649
  const ctx = buildContext();
8386
9650
  const ref = await resolveOrg(ctx, options.org);
@@ -8733,7 +9997,11 @@ program.command("login").description("sign in through the browser (or pass a cre
8733
9997
  } : {},
8734
9998
  ...options.clientId ? { clientId: options.clientId } : {},
8735
9999
  ...options.clientSecret ? { clientSecret: options.clientSecret } : {},
8736
- ...options.devUser ? { devUser: options.devUser } : {},
10000
+ ...options.devUser ? {
10001
+ devUser: options.devUser,
10002
+ sessionToken: void 0,
10003
+ token: void 0
10004
+ } : {},
8737
10005
  ...options.apiUrl ? { apiUrl: options.apiUrl } : {}
8738
10006
  });
8739
10007
  console.error("credentials saved");
@@ -9243,6 +10511,7 @@ registerPgCommands(program);
9243
10511
  registerMysqlCommands(program);
9244
10512
  registerRedisCommands(program);
9245
10513
  registerProvisionerCommands(program);
10514
+ registerProxyCommands(program);
9246
10515
  registerSshCommands(program);
9247
10516
  registerAwsCommands(program);
9248
10517
  registerGcpCommands(program);