@seekrit/cli 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,11 +25,15 @@ const policyMethodSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3,10}$/
25
25
  */
26
26
  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" });
27
27
  const policySecretNameSchema = z.string().trim().regex(/^[A-Za-z0-9_]+$/, "secret names are letters, digits, and underscores");
28
+ /** A TCP port. 0 is refused: it never matches a connection, so a rule naming it is a mistake. */
29
+ const policyPortSchema = z.number().int().min(1).max(65535);
28
30
  const agentPolicyRuleSchema = z.object({
29
31
  host: policyHostSchema,
30
32
  methods: z.array(policyMethodSchema).max(16).default([]),
31
33
  paths: z.array(policyPathSchema).max(64).default([]),
32
34
  allow: z.array(policySecretNameSchema).max(64).default([]),
35
+ https_ports: z.array(policyPortSchema).max(32).optional(),
36
+ http_ports: z.array(policyPortSchema).max(32).optional(),
33
37
  label: z.string().trim().max(120).optional()
34
38
  });
35
39
  z.object({
@@ -48,11 +52,65 @@ z.object({
48
52
  });
49
53
  z.object({
50
54
  host: policyHostSchema,
55
+ /** How the agent reaches the host. Omit for a destination the operator fixed. */
56
+ transport: z.object({
57
+ scheme: z.enum(["https", "http"]),
58
+ port: policyPortSchema
59
+ }).optional(),
51
60
  method: policyMethodSchema,
52
61
  path: z.string().trim().min(1).max(2048),
53
62
  secret: policySecretNameSchema.optional()
54
63
  });
55
64
  /**
65
+ * The ports a rule covers, with the default applied.
66
+ *
67
+ * **Absent does not mean any here**, unlike methods and paths. A bare hostname
68
+ * reads as `https://host`, so a rule naming no ports covers HTTPS on 443 and
69
+ * nothing else: another port on the same name is another service (often an
70
+ * admin one), and cleartext puts the substituted credential on the wire. The
71
+ * default applies only when *neither* list says anything — `http_ports: [8080]`
72
+ * alone means exactly that, not that plus 443.
73
+ */
74
+ function policyRulePorts(rule) {
75
+ const norm = (ports) => [...new Set(ports ?? [])].sort((a, b) => a - b);
76
+ const https = norm(rule.https_ports);
77
+ const http = norm(rule.http_ports);
78
+ if (https.length === 0 && http.length === 0) return {
79
+ https: [443],
80
+ http: []
81
+ };
82
+ return {
83
+ https,
84
+ http
85
+ };
86
+ }
87
+ /** True when a rule covers HTTPS on 443 and nothing else — all v1 can say. */
88
+ function isDefaultPolicyPorts(rule) {
89
+ const { https, http } = policyRulePorts(rule);
90
+ return http.length === 0 && https.length === 1 && https[0] === 443;
91
+ }
92
+ /**
93
+ * `https:443,8443 http:5000`, or `null` for the default — for the editor's
94
+ * collapsed rows, the publish diff, and the CLI, which only mention ports when
95
+ * a rule says something about them.
96
+ */
97
+ function describePolicyPorts(rule) {
98
+ if (isDefaultPolicyPorts(rule)) return null;
99
+ const { https, http } = policyRulePorts(rule);
100
+ const parts = [];
101
+ if (https.length > 0) parts.push(`https:${https.join(",")}`);
102
+ if (http.length > 0) parts.push(`http:${http.join(",")}`);
103
+ return parts.join(" ");
104
+ }
105
+ /**
106
+ * The bundle version to publish these rules as: v1 unless a rule needs ports v1
107
+ * cannot express. Keeping the old version wherever it suffices is what lets a
108
+ * fleet upgrade its proxies after the dashboard rather than in lockstep.
109
+ */
110
+ function policyBundleVersionFor(rules) {
111
+ return rules.every(isDefaultPolicyPorts) ? 1 : 2;
112
+ }
113
+ /**
56
114
  * Serialize a bundle to the exact bytes that get signed and transported.
57
115
  *
58
116
  * Explicit field order rather than a generic sorted-key walk: the field list is
@@ -61,6 +119,10 @@ z.object({
61
119
  * arrays are kept, since their emptiness is meaningful (any/any/none).
62
120
  */
63
121
  function canonicalizeAgentPolicy(bundle) {
122
+ if (bundle.v < 2) {
123
+ const index = bundle.rules.findIndex((rule) => !isDefaultPolicyPorts(rule));
124
+ if (index !== -1) throw new AgentPolicyError(`rule ${index + 1} names ports, which need a v2 bundle (see policyBundleVersionFor)`);
125
+ }
64
126
  const rules = bundle.rules.map((rule) => {
65
127
  const out = {
66
128
  host: rule.host.trim().toLowerCase(),
@@ -68,6 +130,11 @@ function canonicalizeAgentPolicy(bundle) {
68
130
  paths: rule.paths.map((p) => p.trim()),
69
131
  allow: [...rule.allow].sort()
70
132
  };
133
+ if (bundle.v >= 2) {
134
+ const ports = policyRulePorts(rule);
135
+ out.https_ports = ports.https;
136
+ out.http_ports = ports.http;
137
+ }
71
138
  if (rule.label?.trim()) out.label = rule.label.trim();
72
139
  return out;
73
140
  });
@@ -158,8 +225,13 @@ function parseAgentPolicyUnverified(envelope) {
158
225
  } catch (e) {
159
226
  throw new AgentPolicyError(`policy bundle body is unreadable: ${e.message}`);
160
227
  }
161
- if (body?.v !== 1) throw new AgentPolicyError(`unsupported policy bundle version ${String(body?.v)}`);
228
+ if (typeof body?.v !== "number" || body.v < 1 || body.v > 2) throw new AgentPolicyError(`unsupported policy bundle version ${String(body?.v)}`);
162
229
  if (!Array.isArray(body.rules) || !body.signer?.jwk) throw new AgentPolicyError("policy bundle is missing rules or signer");
230
+ body.rules.forEach((rule, i) => {
231
+ const carriesPorts = rule.https_ports !== void 0 || rule.http_ports !== void 0;
232
+ if (body.v < 2 && carriesPorts) throw new AgentPolicyError(`policy bundle v${body.v} rule ${i + 1} carries https_ports/http_ports, which need a v2 bundle`);
233
+ for (const port of [...rule.https_ports ?? [], ...rule.http_ports ?? []]) if (!policyPortSchema.safeParse(port).success) throw new AgentPolicyError(`policy bundle rule ${i + 1} names an invalid port ${port}`);
234
+ });
163
235
  return body;
164
236
  }
165
237
  /**
@@ -209,6 +281,11 @@ function ruleCoversPath(rule, path) {
209
281
  if (rule.paths.length === 0) return true;
210
282
  return rule.paths.some((p) => matchPolicyPath(p, path));
211
283
  }
284
+ function ruleReaches(rule, transport) {
285
+ if (!transport) return true;
286
+ const ports = policyRulePorts(rule);
287
+ return (transport.scheme === "https" ? ports.https : ports.http).includes(transport.port);
288
+ }
212
289
  /**
213
290
  * Decide a request against an ordered rule set, first match wins — the same
214
291
  * evaluation `RuleSet::decide` performs in the proxy, including which rule
@@ -218,11 +295,14 @@ function ruleCoversPath(rule, path) {
218
295
  function evaluatePolicy(rules, query) {
219
296
  const host = query.host.trim().toLowerCase();
220
297
  let hostMatched = false;
298
+ let portMatched = false;
221
299
  let pathMatchedIndex = null;
222
300
  for (let i = 0; i < rules.length; i++) {
223
301
  const rule = rules[i];
224
302
  if (rule.host.trim().toLowerCase() !== host) continue;
225
303
  hostMatched = true;
304
+ if (!ruleReaches(rule, query.transport)) continue;
305
+ portMatched = true;
226
306
  const pathsOk = ruleCoversPath(rule, query.path);
227
307
  if (pathsOk && ruleCoversMethod(rule, query.method)) {
228
308
  if (query.secret !== void 0 && !rule.allow.includes(query.secret)) return {
@@ -240,8 +320,12 @@ function evaluatePolicy(rules, query) {
240
320
  decision: "method_not_allowed",
241
321
  ruleIndex: pathMatchedIndex
242
322
  };
323
+ if (!hostMatched) return {
324
+ decision: "no_rule",
325
+ ruleIndex: null
326
+ };
243
327
  return {
244
- decision: hostMatched ? "path_not_allowed" : "no_rule",
328
+ decision: portMatched ? "path_not_allowed" : "port_not_allowed",
245
329
  ruleIndex: null
246
330
  };
247
331
  }
@@ -251,6 +335,7 @@ function describePolicyVerdict(verdict, rules) {
251
335
  switch (verdict.decision) {
252
336
  case "allow": return `permitted by rule ${(verdict.ruleIndex ?? 0) + 1}${rule?.label ? ` (${rule.label})` : ""}`;
253
337
  case "no_rule": return "no rule covers this host";
338
+ case "port_not_allowed": return "this host has rules, but none cover this scheme and port";
254
339
  case "method_not_allowed": return `rule ${(verdict.ruleIndex ?? 0) + 1} covers this path but not this method`;
255
340
  case "path_not_allowed": return "this host has rules, but none cover this path";
256
341
  case "secret_not_allowed": return `rule ${(verdict.ruleIndex ?? 0) + 1} permits the request but not this secret`;
@@ -291,6 +376,7 @@ function normalizeRule(rule) {
291
376
  methods: rule.methods.map((m) => m.toUpperCase()).sort(),
292
377
  paths: [...rule.paths],
293
378
  allow: [...rule.allow].sort(),
379
+ ports: policyRulePorts(rule),
294
380
  label: rule.label?.trim() || void 0
295
381
  };
296
382
  }
@@ -319,6 +405,7 @@ const activityEntrySchema = z.object({
319
405
  decision: z.enum([
320
406
  "allow",
321
407
  "no_rule",
408
+ "port_not_allowed",
322
409
  "method_not_allowed",
323
410
  "path_not_allowed",
324
411
  "secret_not_allowed",
@@ -354,8 +441,7 @@ z.object({
354
441
  */
355
442
  entries: z.array(activityEntrySchema).min(1).max(500)
356
443
  });
357
- //#endregion
358
- //#region ../../packages/core/src/agent-review.ts
444
+ // ../../packages/core/src/agent-review.ts
359
445
  function emptyStats() {
360
446
  return {
361
447
  allows: 0,
@@ -565,8 +651,7 @@ z.object({
565
651
  z.object({
566
652
  /** The presented token. In the body, never a URL — it is a credential. */
567
653
  token: z.string().trim().min(8).max(512) });
568
- //#endregion
569
- //#region ../../packages/core/src/archive.ts
654
+ // ../../packages/core/src/archive.ts
570
655
  /**
571
656
  * The **break-glass archive** format: one signed JSON file holding everything
572
657
  * seekrit stores for an org, in the form seekrit stores it — ciphertext stays
@@ -951,8 +1036,7 @@ const ENTITLEMENT_KEYS = Object.keys({
951
1036
  metric: "monthly_resolves"
952
1037
  }
953
1038
  });
954
- //#endregion
955
- //#region ../../packages/core/src/plans.ts
1039
+ // ../../packages/core/src/plans.ts
956
1040
  const PLAN_FAMILIES = {
957
1041
  free: {
958
1042
  id: "free",
@@ -986,8 +1070,7 @@ const PLAN_FAMILIES = {
986
1070
  const PLAN_FAMILY_IDS = Object.keys(PLAN_FAMILIES);
987
1071
  /** Plan families a plan picker should render. Excludes families marked `hidden`. */
988
1072
  const VISIBLE_PLAN_FAMILY_IDS = PLAN_FAMILY_IDS.filter((family) => !PLAN_FAMILIES[family].hidden);
989
- //#endregion
990
- //#region ../../packages/core/src/billing.ts
1073
+ // ../../packages/core/src/billing.ts
991
1074
  /**
992
1075
  * Lifecycle states a subscription can be in. Mirrors the biller's own states
993
1076
  * (Stripe) but is provider-neutral so a different biller could map onto it.
@@ -999,8 +1082,7 @@ const SUBSCRIPTION_STATUSES = [
999
1082
  "canceled",
1000
1083
  "paused"
1001
1084
  ];
1002
- //#endregion
1003
- //#region ../../packages/core/src/branches.ts
1085
+ // ../../packages/core/src/branches.ts
1004
1086
  /**
1005
1087
  * Branch (ephemeral) environments — a per-PR/preview overlay on an existing
1006
1088
  * application environment.
@@ -1044,8 +1126,7 @@ function parseBranchTtl(input) {
1044
1126
  if (multiplier === void 0) return null;
1045
1127
  return value * multiplier;
1046
1128
  }
1047
- //#endregion
1048
- //#region ../../packages/core/src/change-requests.ts
1129
+ // ../../packages/core/src/change-requests.ts
1049
1130
  const CHANGE_REQUEST_STATUSES = [
1050
1131
  "open",
1051
1132
  "applied",
@@ -1061,8 +1142,7 @@ function describeChange(kind, name, restoreFrom) {
1061
1142
  case "restore": return `restore ${name} to v${restoreFrom ?? "?"}`;
1062
1143
  }
1063
1144
  }
1064
- //#endregion
1065
- //#region ../../packages/core/src/connect.ts
1145
+ // ../../packages/core/src/connect.ts
1066
1146
  /** Workload authentication is separate from secret-decryption principals. */
1067
1147
  const workloadPublicKeySchema = z.object({
1068
1148
  kty: z.literal("EC"),
@@ -1196,8 +1276,7 @@ z.object({
1196
1276
  body: z.string().max(65536).optional()
1197
1277
  }).strict().refine((v) => !["GET", "HEAD"].includes(v.method) || v.body === void 0, "GET and HEAD cannot carry a body");
1198
1278
  z.object({ audience: audienceSchema }).strict();
1199
- //#endregion
1200
- //#region ../../packages/core/src/dotenv.ts
1279
+ // ../../packages/core/src/dotenv.ts
1201
1280
  /**
1202
1281
  * `.env` parsing and serialization — the canonical definition of what a `.env`
1203
1282
  * file means to seekrit. `apps/run/src/dotenv.rs` is a faithful port of the
@@ -1320,8 +1399,7 @@ function skipSpace(content, from, end) {
1320
1399
  while (i < end && (content[i] === " " || content[i] === " ")) i++;
1321
1400
  return i;
1322
1401
  }
1323
- //#endregion
1324
- //#region ../../packages/core/src/ids.ts
1402
+ // ../../packages/core/src/ids.ts
1325
1403
  const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1326
1404
  /**
1327
1405
  * Generate a prefixed, URL-safe random ID, e.g. `org_h9K2x…`.
@@ -1338,8 +1416,7 @@ function randomId(prefix, length = 24) {
1338
1416
  }
1339
1417
  return `${prefix}_${out}`;
1340
1418
  }
1341
- //#endregion
1342
- //#region ../../packages/core/src/interpolate.ts
1419
+ // ../../packages/core/src/interpolate.ts
1343
1420
  /**
1344
1421
  * Secret references: `${OTHER_SECRET}` inside a secret value.
1345
1422
  *
@@ -1484,8 +1561,7 @@ function interpolateSecrets(values) {
1484
1561
  unresolved: [...unresolved].sort()
1485
1562
  };
1486
1563
  }
1487
- //#endregion
1488
- //#region ../../packages/core/src/json-value.ts
1564
+ // ../../packages/core/src/json-value.ts
1489
1565
  /**
1490
1566
  * Whether a value is worth *offering* a JSON view for: an object or array.
1491
1567
  *
@@ -1939,8 +2015,7 @@ z.discriminatedUnion("provider", [
1939
2015
  mintGcpLeaseSchema,
1940
2016
  mintMongoLeaseSchema
1941
2017
  ]);
1942
- //#endregion
1943
- //#region ../../packages/core/src/providers/aws.ts
2018
+ // ../../packages/core/src/providers/aws.ts
1944
2019
  /**
1945
2020
  * The one-time IAM setup an admin performs so seekrit can assume the target
1946
2021
  * role. Analogue of `sshHostSetupInstructions` / `postgresGroupBootstrapSql` —
@@ -1967,8 +2042,7 @@ function awsTrustPolicyInstructions(config) {
1967
2042
  `# on ${config.roleArn}. seekrit only ever calls AssumeRole on this role.`
1968
2043
  ].join("\n");
1969
2044
  }
1970
- //#endregion
1971
- //#region ../../packages/core/src/providers/gcp.ts
2045
+ // ../../packages/core/src/providers/gcp.ts
1972
2046
  /**
1973
2047
  * The one-time IAM setup an admin performs so seekrit can impersonate the target
1974
2048
  * service account. Analogue of `awsTrustPolicyInstructions` — printed for the
@@ -1990,8 +2064,7 @@ function gcpSetupInstructions(config) {
1990
2064
  "gcloud services enable iamcredentials.googleapis.com"
1991
2065
  ].join("\n");
1992
2066
  }
1993
- //#endregion
1994
- //#region ../../packages/core/src/providers/mongodb.ts
2067
+ // ../../packages/core/src/providers/mongodb.ts
1995
2068
  /**
1996
2069
  * The one-time setup an admin performs so seekrit can provision users: create a
1997
2070
  * dedicated provisioning user with `userAdmin` on the target database (or
@@ -2016,8 +2089,7 @@ function mongoAdminSetupInstructions(config) {
2016
2089
  `// --uri "mongodb://seekrit_provisioner:<pwd>@${config.connection.host}:${config.connection.port}/?authSource=${authSource}"`
2017
2090
  ].join("\n");
2018
2091
  }
2019
- //#endregion
2020
- //#region ../../packages/core/src/providers/postgres.ts
2092
+ // ../../packages/core/src/providers/postgres.ts
2021
2093
  /**
2022
2094
  * The one-time setup SQL an admin runs to create the shared group role that a
2023
2095
  * read-only / read-write target's leased credentials inherit. Idempotent (safe
@@ -2048,8 +2120,7 @@ function postgresGroupBootstrapSql(config) {
2048
2120
  if (config.accessLevel === "readwrite") lines.push(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO ${group};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT USAGE, SELECT ON SEQUENCES TO ${group};`);
2049
2121
  return lines.join("\n");
2050
2122
  }
2051
- //#endregion
2052
- //#region ../../packages/core/src/providers/ssh.ts
2123
+ // ../../packages/core/src/providers/ssh.ts
2053
2124
  /**
2054
2125
  * The one-time host setup an admin runs so a target's certificates are accepted.
2055
2126
  * Analogue of `postgresGroupBootstrapSql` — displayed for the admin to run, not
@@ -2069,8 +2140,7 @@ function sshHostSetupInstructions(config) {
2069
2140
  "sudo systemctl reload sshd"
2070
2141
  ].join("\n");
2071
2142
  }
2072
- //#endregion
2073
- //#region ../../packages/core/src/types.ts
2143
+ // ../../packages/core/src/types.ts
2074
2144
  const AUDIT_ACTIONS = [
2075
2145
  "workload.created",
2076
2146
  "workload.updated",
@@ -2301,8 +2371,7 @@ const NOTIFICATION_TYPE_META = {
2301
2371
  defaultEnabled: true
2302
2372
  }
2303
2373
  };
2304
- //#endregion
2305
- //#region ../../packages/core/src/schemas.ts
2374
+ // ../../packages/core/src/schemas.ts
2306
2375
  /** URL-safe identifier segment: `my-app`, `production`, … */
2307
2376
  const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
2308
2377
  const nameSchema = z.string().trim().min(1).max(128);
@@ -2724,8 +2793,7 @@ z.object({
2724
2793
  */
2725
2794
  status: settableRotationStatusSchema.optional()
2726
2795
  }).refine((v) => v.intervalSeconds !== void 0 || v.config !== void 0 || v.status !== void 0, "provide at least one of intervalSeconds, config, or status");
2727
- //#endregion
2728
- //#region ../../packages/core/src/sync.ts
2796
+ // ../../packages/core/src/sync.ts
2729
2797
  /**
2730
2798
  * Third-party sync: seekrit as source of truth, pushing an environment's
2731
2799
  * resolved secrets out to a platform that holds its own copy (Vercel project
@@ -4209,8 +4277,7 @@ z.object({
4209
4277
  mode: z.enum(["auto", "manual"]).optional(),
4210
4278
  enabled: z.boolean().optional()
4211
4279
  });
4212
- //#endregion
4213
- //#region ../../packages/core/src/vault.ts
4280
+ // ../../packages/core/src/vault.ts
4214
4281
  /** One encrypted credential within a connection. */
4215
4282
  const VAULT_SLOTS = [
4216
4283
  "oauth2",
@@ -4494,8 +4561,7 @@ z.object({
4494
4561
  userRef: vaultUserRefSchema,
4495
4562
  leaseId: leaseIdSchema
4496
4563
  });
4497
- //#endregion
4498
- //#region ../../packages/crypto/src/encoding.ts
4564
+ // ../../packages/crypto/src/encoding.ts
4499
4565
  const CHUNK = 32768;
4500
4566
  /** Base64url (no padding) — portable across browsers, Workers, and Node. */
4501
4567
  function toBase64Url(bytes) {
@@ -4526,8 +4592,7 @@ function utf8Encode(text) {
4526
4592
  function utf8Decode(bytes) {
4527
4593
  return new TextDecoder().decode(bytes);
4528
4594
  }
4529
- //#endregion
4530
- //#region ../../packages/crypto/src/errors.ts
4595
+ // ../../packages/crypto/src/errors.ts
4531
4596
  var SeekritCryptoError = class extends Error {
4532
4597
  code;
4533
4598
  constructor(code, message) {
@@ -4547,8 +4612,7 @@ function splitBlob(blob, prefix, segments) {
4547
4612
  if (parts.length !== segments + 1 || parts.some((p) => p.length === 0)) throw new SeekritCryptoError("MALFORMED_BLOB", `malformed "${prefix}" blob`);
4548
4613
  return parts.slice(1);
4549
4614
  }
4550
- //#endregion
4551
- //#region ../../packages/crypto/src/aes.ts
4615
+ // ../../packages/crypto/src/aes.ts
4552
4616
  const SECRET_PREFIX = "sc1";
4553
4617
  const IV_LENGTH$1 = 12;
4554
4618
  /** Generate a fresh 256-bit data encryption key for an environment. */
@@ -4593,8 +4657,7 @@ async function decryptSecret(dek, blob, aad) {
4593
4657
  function secretAad(environmentId, secretName) {
4594
4658
  return `${environmentId}/${secretName}`;
4595
4659
  }
4596
- //#endregion
4597
- //#region ../../packages/crypto/src/keys.ts
4660
+ // ../../packages/crypto/src/keys.ts
4598
4661
  async function generateKeyPair() {
4599
4662
  const pair = await crypto.subtle.generateKey({
4600
4663
  name: "ECDH",
@@ -4627,8 +4690,7 @@ async function importPrivateKeyPkcs8(pkcs8) {
4627
4690
  namedCurve: "P-256"
4628
4691
  }, true, ["deriveBits"]);
4629
4692
  }
4630
- //#endregion
4631
- //#region ../../packages/crypto/src/wrap.ts
4693
+ // ../../packages/crypto/src/wrap.ts
4632
4694
  /**
4633
4695
  * ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
4634
4696
  * the recipient's public key; the shared secret is run through HKDF-SHA256 to
@@ -4696,8 +4758,7 @@ async function unwrapDek(wrapped, privateKey) {
4696
4758
  throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
4697
4759
  }
4698
4760
  }
4699
- //#endregion
4700
- //#region ../../packages/crypto/src/aws.ts
4761
+ // ../../packages/crypto/src/aws.ts
4701
4762
  /** Generate the ephemeral P-256 keypair a client uses to receive one AWS lease. */
4702
4763
  async function generateAwsRecipientKeyPair() {
4703
4764
  return generateKeyPair();
@@ -4724,8 +4785,7 @@ async function unwrapAwsCredential(wrapped, privateKeyJwk) {
4724
4785
  region: c.region
4725
4786
  };
4726
4787
  }
4727
- //#endregion
4728
- //#region ../../packages/crypto/src/gcp.ts
4788
+ // ../../packages/crypto/src/gcp.ts
4729
4789
  /** Generate the ephemeral P-256 keypair a client uses to receive one GCP lease. */
4730
4790
  async function generateGcpRecipientKeyPair() {
4731
4791
  return generateKeyPair();
@@ -4751,8 +4811,7 @@ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
4751
4811
  scopes: c.scopes
4752
4812
  };
4753
4813
  }
4754
- //#endregion
4755
- //#region ../../packages/crypto/src/kms.ts
4814
+ // ../../packages/crypto/src/kms.ts
4756
4815
  /**
4757
4816
  * Client-side KMS envelope encryption. A managed `encrypt` key is a 256-bit
4758
4817
  * AES-GCM key whose material reaches the client only as a `wd1.` grant (wrapped
@@ -4882,8 +4941,7 @@ async function decryptDataKey(material, wrapped) {
4882
4941
  throw new SeekritCryptoError("DECRYPT_FAILED", "data key unwrap failed: wrong key or tampered blob");
4883
4942
  }
4884
4943
  }
4885
- //#endregion
4886
- //#region ../../packages/crypto/src/random.ts
4944
+ // ../../packages/crypto/src/random.ts
4887
4945
  const ALPHABETS = {
4888
4946
  alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
4889
4947
  hex: "0123456789abcdef",
@@ -4919,8 +4977,7 @@ function generateSecretValue(length, alphabet = "alphanumeric") {
4919
4977
  }
4920
4978
  return out;
4921
4979
  }
4922
- //#endregion
4923
- //#region ../../packages/crypto/src/mongodb.ts
4980
+ // ../../packages/crypto/src/mongodb.ts
4924
4981
  /** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
4925
4982
  async function generateMongoRecipientKeyPair() {
4926
4983
  return generateKeyPair();
@@ -4954,8 +5011,7 @@ async function unwrapMongoCredential(wrapped, privateKeyJwk) {
4954
5011
  function mongoConnectionUri(cred) {
4955
5012
  return `mongodb://${encodeURIComponent(cred.username)}:${encodeURIComponent(cred.password)}@${cred.host}:${cred.port}/${cred.database}?authSource=${cred.authSource}`;
4956
5013
  }
4957
- //#endregion
4958
- //#region ../../packages/crypto/src/mysql.ts
5014
+ // ../../packages/crypto/src/mysql.ts
4959
5015
  /**
4960
5016
  * Client-side construction of a MySQL/MariaDB `mysql_native_password`
4961
5017
  * authentication string, for minting *temporary MySQL login credentials*
@@ -5011,8 +5067,7 @@ async function generateMysqlCredential(options = {}) {
5011
5067
  verifier: await mysqlNativePasswordVerifier(password)
5012
5068
  };
5013
5069
  }
5014
- //#endregion
5015
- //#region ../../packages/crypto/src/passphrase.ts
5070
+ // ../../packages/crypto/src/passphrase.ts
5016
5071
  /**
5017
5072
  * User private keys are stored server-side encrypted under a key derived from
5018
5073
  * the user's passphrase, so any browser or CLI session can fetch and unlock
@@ -5071,8 +5126,7 @@ async function decryptPrivateKey(passphrase, blob) {
5071
5126
  throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
5072
5127
  }
5073
5128
  }
5074
- //#endregion
5075
- //#region ../../packages/crypto/src/policy-key.ts
5129
+ // ../../packages/crypto/src/policy-key.ts
5076
5130
  /**
5077
5131
  * Signing with a principal's **existing** keypair, for agent access policy.
5078
5132
  *
@@ -5141,8 +5195,7 @@ function policySignerJwk(publicKeyJwk) {
5141
5195
  y: jwk.y
5142
5196
  };
5143
5197
  }
5144
- //#endregion
5145
- //#region ../../packages/crypto/src/shamir.ts
5198
+ // ../../packages/crypto/src/shamir.ts
5146
5199
  /**
5147
5200
  * Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
5148
5201
  * reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b). Splits a byte string
@@ -5271,8 +5324,7 @@ function combineSecret(shares) {
5271
5324
  }
5272
5325
  return secret;
5273
5326
  }
5274
- //#endregion
5275
- //#region ../../packages/crypto/src/recovery.ts
5327
+ // ../../packages/crypto/src/recovery.ts
5276
5328
  /** Generate a fresh org recovery keypair (P-256, same as any principal). */
5277
5329
  function generateRecoveryKey() {
5278
5330
  return generateKeyPair();
@@ -5326,8 +5378,7 @@ async function combineRecoveryShares(shareBytes) {
5326
5378
  throw new SeekritCryptoError("DECRYPT_FAILED", "recovery reconstruction failed: wrong, insufficient, or mismatched shares");
5327
5379
  }
5328
5380
  }
5329
- //#endregion
5330
- //#region ../../packages/crypto/src/redis.ts
5381
+ // ../../packages/crypto/src/redis.ts
5331
5382
  /**
5332
5383
  * Client-side construction of a Redis (6+) ACL password verifier, for minting
5333
5384
  * *temporary Redis login credentials* without the password plaintext ever
@@ -5428,8 +5479,7 @@ async function generatePostgresCredential(options = {}) {
5428
5479
  iterations
5429
5480
  };
5430
5481
  }
5431
- //#endregion
5432
- //#region ../../packages/crypto/src/sign.ts
5482
+ // ../../packages/crypto/src/sign.ts
5433
5483
  /**
5434
5484
  * Managed signing keys for the client-side KMS — ECDSA over P-256 (the curve
5435
5485
  * already used for principal keypairs; universal in WebCrypto). A `sign` key's
@@ -5491,8 +5541,7 @@ function signatureKeyRef(signature) {
5491
5541
  version
5492
5542
  };
5493
5543
  }
5494
- //#endregion
5495
- //#region ../../packages/crypto/src/ssh.ts
5544
+ // ../../packages/crypto/src/ssh.ts
5496
5545
  /**
5497
5546
  * Client-side SSH certificate authority for minting *temporary SSH access*
5498
5547
  * without any private key ever reaching seekrit's control plane.
@@ -5623,8 +5672,7 @@ function encodeOpensshPrivateKey(seed, pub, comment) {
5623
5672
  const b64 = toBase64(new SshWriter().bytes(utf8Encode("openssh-key-v1")).byte(0).string("none").string("none").string(/* @__PURE__ */ new Uint8Array(0)).uint32(1).string(ed25519PublicKeyBlob(pub)).string(concat(privSection, pad)).build());
5624
5673
  return `-----BEGIN OPENSSH PRIVATE KEY-----\n${b64.match(/.{1,70}/g)?.join("\n") ?? b64}\n-----END OPENSSH PRIVATE KEY-----\n`;
5625
5674
  }
5626
- //#endregion
5627
- //#region ../../packages/crypto/src/token.ts
5675
+ // ../../packages/crypto/src/token.ts
5628
5676
  /**
5629
5677
  * Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
5630
5678
  * principals. One local bundle holds independent authentication and decryption
@@ -5734,11 +5782,9 @@ async function createAgentTaskToken() {
5734
5782
  tokenHash: await hashToken(token)
5735
5783
  };
5736
5784
  }
5737
- //#endregion
5738
- //#region package.json
5739
- var version = "2.2.0";
5740
- //#endregion
5741
- //#region ../../packages/api-client/src/index.ts
5785
+ // package.json
5786
+ var version = "2.3.0";
5787
+ // ../../packages/api-client/src/index.ts
5742
5788
  /**
5743
5789
  * The proposer's note travels as a header, not a body field: an unprotected
5744
5790
  * environment ignores it, so the same call works either way and no caller has
@@ -6735,8 +6781,7 @@ function writeProjectConfig(config, dir = process.cwd()) {
6735
6781
  writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
6736
6782
  return path;
6737
6783
  }
6738
- //#endregion
6739
- //#region src/io.ts
6784
+ // src/io.ts
6740
6785
  let failThrows = false;
6741
6786
  /**
6742
6787
  * In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
@@ -6861,8 +6906,7 @@ async function readStdin() {
6861
6906
  for await (const chunk of process.stdin) chunks.push(chunk);
6862
6907
  return Buffer.concat(chunks).toString("utf8");
6863
6908
  }
6864
- //#endregion
6865
- //#region src/context.ts
6909
+ // src/context.ts
6866
6910
  /** Sent as `x-seekrit-client` so the API attributes usage to the CLI (analytics). */
6867
6911
  const CLI_CLIENT = `cli/${version}`;
6868
6912
  /**
@@ -7002,8 +7046,7 @@ async function getDek(ctx, orgId, envId) {
7002
7046
  const [{ wrappedDek }, privateKey] = await Promise.all([ctx.client.getMyEnvKey(orgId, envId), getPrivateKey(ctx)]);
7003
7047
  return unwrapDek(wrappedDek, privateKey);
7004
7048
  }
7005
- //#endregion
7006
- //#region src/output.ts
7049
+ // src/output.ts
7007
7050
  /** Shorthand for a column whose header is fixed and value is one field. */
7008
7051
  function col(header, value) {
7009
7052
  return {
@@ -7065,8 +7108,7 @@ function section(title) {
7065
7108
  console.log("");
7066
7109
  console.log(title);
7067
7110
  }
7068
- //#endregion
7069
- //#region src/target.ts
7111
+ // src/target.ts
7070
7112
  /** Resolve the target org from a flag, the committed config, or a lone org. */
7071
7113
  async function resolveOrg(ctx, orgSlug) {
7072
7114
  const wanted = orgSlug ?? findProjectConfig()?.org;
@@ -7222,8 +7264,7 @@ async function resolveGroup(ctx, opts) {
7222
7264
  slug: group.slug
7223
7265
  };
7224
7266
  }
7225
- //#endregion
7226
- //#region src/access.ts
7267
+ // src/access.ts
7227
7268
  /**
7228
7269
  * Human-readable labels for the principals holding a key grant. `grant list`
7229
7270
  * would otherwise be a column of opaque ids, which is exactly the question
@@ -7316,8 +7357,7 @@ function registerAccessCommands(program) {
7316
7357
  console.error(`revoked ${options.user ?? options.token}'s access to ${label}`);
7317
7358
  });
7318
7359
  }
7319
- //#endregion
7320
- //#region src/account.ts
7360
+ // src/account.ts
7321
7361
  function sessionStatus(session) {
7322
7362
  if (session.revokedAt) return "revoked";
7323
7363
  if (session.expiresAt && Date.parse(session.expiresAt) < Date.now()) return "expired";
@@ -7367,8 +7407,7 @@ function registerAccountCommands(program) {
7367
7407
  console.error(`${type} emails ${state}`);
7368
7408
  });
7369
7409
  }
7370
- //#endregion
7371
- //#region src/agents.ts
7410
+ // src/agents.ts
7372
7411
  /**
7373
7412
  * Agent access governance from the CLI.
7374
7413
  *
@@ -7500,6 +7539,14 @@ function describeList(values) {
7500
7539
  function describeSecrets(values) {
7501
7540
  return values.length === 0 ? "none" : values.join(" ");
7502
7541
  }
7542
+ /**
7543
+ * The third convention, and the one most likely to be misread: **no ports means
7544
+ * HTTPS on 443**, not any port. A blank or `any` here would say the opposite of
7545
+ * what the proxy enforces, so the default is spelled out.
7546
+ */
7547
+ function describePorts(rule) {
7548
+ return describePolicyPorts(rule) ?? "https:443";
7549
+ }
7503
7550
  function printRules(rules) {
7504
7551
  printTable(rules.map((rule, i) => ({
7505
7552
  n: i + 1,
@@ -7507,6 +7554,7 @@ function printRules(rules) {
7507
7554
  })), [
7508
7555
  col("#", (r) => r.n),
7509
7556
  col("host", (r) => r.rule.host),
7557
+ col("ports", (r) => describePorts(r.rule)),
7510
7558
  col("methods", (r) => describeList(r.rule.methods)),
7511
7559
  col("paths", (r) => describeList(r.rule.paths)),
7512
7560
  col("secrets", (r) => describeSecrets(r.rule.allow)),
@@ -7515,8 +7563,9 @@ function printRules(rules) {
7515
7563
  }
7516
7564
  /** One rule as a single line, for the publish diff. */
7517
7565
  function ruleLine(rule) {
7566
+ const ports = describePolicyPorts(rule);
7518
7567
  const parts = [
7519
- rule.host,
7568
+ ports ? `${rule.host} [${ports}]` : rule.host,
7520
7569
  describeList(rule.methods),
7521
7570
  describeList(rule.paths),
7522
7571
  `secrets=${describeSecrets(rule.allow)}`
@@ -7843,7 +7892,11 @@ function registerAgentCommands(program) {
7843
7892
  console.error("widening proposals are never applied: an agent must not earn permissions by retrying");
7844
7893
  }
7845
7894
  });
7846
- agents.command("simulate <ref>").description("ask what a policy would decide, without sending a request").requiredOption("--host <host>", "upstream hostname").requiredOption("--path <path>", "request path").option("--method <method>", "HTTP method", "GET").option("--secret <NAME>", "the secret the request would inject").option("--version <n>", "evaluate a specific published version").option("--org <slug>", "organization").option("--json", "machine-readable output").action(async (ref, options) => {
7895
+ agents.command("simulate <ref>").description("ask what a policy would decide, without sending a request").requiredOption("--host <host>", "upstream hostname").requiredOption("--path <path>", "request path").option("--method <method>", "HTTP method", "GET").option("--scheme <scheme>", "https or http, as the agent dials it (forward proxy)", "https").option("--port <n>", "port the agent dials (forward proxy; default 443 for https, 80 for http)").option("--secret <NAME>", "the secret the request would inject").option("--version <n>", "evaluate a specific published version").option("--org <slug>", "organization").option("--json", "machine-readable output").action(async (ref, options) => {
7896
+ const scheme = options.scheme.toLowerCase();
7897
+ if (scheme !== "https" && scheme !== "http") fail("--scheme must be https or http");
7898
+ const port = options.port === void 0 ? scheme === "https" ? 443 : 80 : Number(options.port);
7899
+ if (!Number.isInteger(port) || port < 1 || port > 65535) fail("--port must be 1-65535");
7847
7900
  const ctx = buildContext();
7848
7901
  const org = await resolveOrg(ctx, options.org);
7849
7902
  const agent = await resolveAgent(ctx, org.id, ref);
@@ -7853,6 +7906,10 @@ function registerAgentCommands(program) {
7853
7906
  if (!published) fail(`${agent.slug} has no published policy — every request is denied`);
7854
7907
  const verdict = evaluatePolicy(published.rules, {
7855
7908
  host: options.host,
7909
+ transport: {
7910
+ scheme,
7911
+ port
7912
+ },
7856
7913
  method: options.method,
7857
7914
  path: options.path,
7858
7915
  ...options.secret === void 0 ? {} : { secret: options.secret }
@@ -7863,7 +7920,7 @@ function registerAgentCommands(program) {
7863
7920
  verdict,
7864
7921
  reason
7865
7922
  }, () => {
7866
- const target = `${options.method.toUpperCase()} ${options.host}${options.path}`;
7923
+ const target = `${options.method.toUpperCase()} ${scheme}://${options.host}:${port}${options.path}`;
7867
7924
  console.log(`${verdict.decision === "allow" ? "allow" : "DENY"} ${target}${options.secret ? ` [${options.secret}]` : ""}`);
7868
7925
  console.log(` v${published.version}: ${reason}`);
7869
7926
  });
@@ -7943,7 +8000,7 @@ function registerPolicyCommands(policy) {
7943
8000
  const { signingKey, jwk } = await getPolicySigner(ctx);
7944
8001
  const issuedAt = Math.floor(Date.now() / 1e3);
7945
8002
  const bundle = await signAgentPolicy(signingKey, jwk, {
7946
- v: 1,
8003
+ v: policyBundleVersionFor(rules),
7947
8004
  org: org.id,
7948
8005
  agent: agent.id,
7949
8006
  agent_slug: agent.slug,
@@ -8356,8 +8413,7 @@ function printExplain(provenance, refs = {}) {
8356
8413
  }
8357
8414
  if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
8358
8415
  }
8359
- //#endregion
8360
- //#region src/env-diff.ts
8416
+ // src/env-diff.ts
8361
8417
  /**
8362
8418
  * The comparison itself, kept pure so it can be tested without a network or a
8363
8419
  * key. Rows come back sorted by name: a diff people read top-to-bottom should
@@ -8474,8 +8530,7 @@ function registerEnvDiff(env) {
8474
8530
  });
8475
8531
  });
8476
8532
  }
8477
- //#endregion
8478
- //#region src/group-sync-grants.ts
8533
+ // src/group-sync-grants.ts
8479
8534
  /** Wrap a newly composed group layer for connections already syncing the app env or its branches. */
8480
8535
  async function syncGrantsForGroupLink(ctx, orgId, envId, envSlug, groupId) {
8481
8536
  const [{ groups }, { bindings }, { branches }] = await Promise.all([
@@ -8500,8 +8555,7 @@ async function syncGrantsForGroupLink(ctx, orgId, envId, envSlug, groupId) {
8500
8555
  };
8501
8556
  }));
8502
8557
  }
8503
- //#endregion
8504
- //#region src/kms.ts
8558
+ // src/kms.ts
8505
8559
  /** Collect a repeatable option into a list. */
8506
8560
  function collect$9(value, acc = []) {
8507
8561
  acc.push(value);
@@ -8790,8 +8844,7 @@ function registerKmsCommands(program) {
8790
8844
  }
8791
8845
  });
8792
8846
  }
8793
- //#endregion
8794
- //#region src/recovery.ts
8847
+ // src/recovery.ts
8795
8848
  /** Collect a repeatable option into a list. */
8796
8849
  function collect$8(value, acc = []) {
8797
8850
  acc.push(value);
@@ -8990,8 +9043,7 @@ function registerRecoveryCommands(program) {
8990
9043
  console.error(`recovery request ${requestId} canceled`);
8991
9044
  });
8992
9045
  }
8993
- //#endregion
8994
- //#region src/apps.ts
9046
+ // src/apps.ts
8995
9047
  /**
8996
9048
  * Applications and their environments — the structure everything else hangs
8997
9049
  * off. Creating an environment is the one command here that does crypto: the
@@ -9193,8 +9245,7 @@ function registerAppCommands(program) {
9193
9245
  console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
9194
9246
  });
9195
9247
  }
9196
- //#endregion
9197
- //#region src/decryptor.ts
9248
+ // src/decryptor.ts
9198
9249
  /**
9199
9250
  * The standalone offline decryptor: one self-contained HTML file, written out by
9200
9251
  * `seekrit archive decryptor`, that opens a break-glass archive in any browser
@@ -10026,8 +10077,7 @@ const OFFLINE_DECRYPTOR_HTML = `<!doctype html>
10026
10077
  </body>
10027
10078
  </html>
10028
10079
  `;
10029
- //#endregion
10030
- //#region src/format.ts
10080
+ // src/format.ts
10031
10081
  function shellQuote(value) {
10032
10082
  return `'${value.replaceAll("'", `'\\''`)}'`;
10033
10083
  }
@@ -10039,8 +10089,7 @@ function formatSecrets(values, format) {
10039
10089
  case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
10040
10090
  }
10041
10091
  }
10042
- //#endregion
10043
- //#region src/archive.ts
10092
+ // src/archive.ts
10044
10093
  /**
10045
10094
  * Break-glass archives (docs/break-glass-export.md).
10046
10095
  *
@@ -10370,8 +10419,7 @@ function registerArchiveCommands(program) {
10370
10419
  console.error("open it in any browser — it declares a Content-Security-Policy of `default-src 'none'`, so it cannot reach the network.");
10371
10420
  });
10372
10421
  }
10373
- //#endregion
10374
- //#region src/audit.ts
10422
+ // src/audit.ts
10375
10423
  /** The API's per-page ceiling (`auditQuerySchema.limit`). */
10376
10424
  const MAX_PAGE = 200;
10377
10425
  function parseLimit(raw) {
@@ -10425,8 +10473,7 @@ function registerAuditCommands(program) {
10425
10473
  });
10426
10474
  });
10427
10475
  }
10428
- //#endregion
10429
- //#region src/aws.ts
10476
+ // src/aws.ts
10430
10477
  /**
10431
10478
  * `seekrit aws` — temporary AWS credentials via STS AssumeRole (Vault-style
10432
10479
  * dynamic secrets, the tier-2 sibling of `seekrit pg`).
@@ -10568,8 +10615,7 @@ function registerAwsCommands(program) {
10568
10615
  console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
10569
10616
  });
10570
10617
  }
10571
- //#endregion
10572
- //#region src/billing.ts
10618
+ // src/billing.ts
10573
10619
  /** `included: null` means unlimited; an unavailable meter is unknown, not zero. */
10574
10620
  function usageLine(usage) {
10575
10621
  if (!usage.available) return "unknown";
@@ -10644,8 +10690,7 @@ function registerBillingCommands(program) {
10644
10690
  console.error(`${ref.slug} moved to the Free plan`);
10645
10691
  });
10646
10692
  }
10647
- //#endregion
10648
- //#region src/branches.ts
10693
+ // src/branches.ts
10649
10694
  /**
10650
10695
  * Resolve `--from` to the environment being branched.
10651
10696
  *
@@ -10742,8 +10787,7 @@ function registerBranchCommands(program) {
10742
10787
  console.error(`deleted branch ${app.slug}#${target.slug}`);
10743
10788
  });
10744
10789
  }
10745
- //#endregion
10746
- //#region src/changes.ts
10790
+ // src/changes.ts
10747
10791
  /**
10748
10792
  * Change requests: the review queue in front of protected environments, and the
10749
10793
  * protection settings that create it.
@@ -11271,8 +11315,7 @@ async function resolveEnvironment(ctx, scope) {
11271
11315
  });
11272
11316
  return values;
11273
11317
  }
11274
- //#endregion
11275
- //#region src/doctor.ts
11318
+ // src/doctor.ts
11276
11319
  /** Every `SEEKRIT_*` variable the CLI reads, for the set-but-blank check. */
11277
11320
  const READ_VARS = [
11278
11321
  "SEEKRIT_TOKEN",
@@ -11692,8 +11735,7 @@ function registerDoctorCommand(program) {
11692
11735
  if (checks.some((c) => c.status === "fail")) process.exitCode = 1;
11693
11736
  });
11694
11737
  }
11695
- //#endregion
11696
- //#region src/gcp.ts
11738
+ // src/gcp.ts
11697
11739
  /**
11698
11740
  * `seekrit gcp` — temporary GCP credentials via IAM Credentials
11699
11741
  * `generateAccessToken` (Vault-style dynamic secrets, the tier-2 sibling of
@@ -11835,8 +11877,7 @@ function registerGcpCommands(program) {
11835
11877
  console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
11836
11878
  });
11837
11879
  }
11838
- //#endregion
11839
- //#region src/groups.ts
11880
+ // src/groups.ts
11840
11881
  /**
11841
11882
  * Shared groups: reusable secret bags composed into application environments
11842
11883
  * by matching slug (`seekrit env groups add`). A group holds one environment
@@ -11943,8 +11984,7 @@ function registerGroupCommands(program) {
11943
11984
  console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
11944
11985
  });
11945
11986
  }
11946
- //#endregion
11947
- //#region src/hermes.ts
11987
+ // src/hermes.ts
11948
11988
  /**
11949
11989
  * `seekrit hermes` — wire seekrit into [Hermes Agent](https://hermes-agent.nousresearch.com)
11950
11990
  * as a **secret source**.
@@ -12161,8 +12201,7 @@ function registerHermesCommands(program) {
12161
12201
  emit(options, { reference: ref }, () => console.log(ref));
12162
12202
  });
12163
12203
  }
12164
- //#endregion
12165
- //#region src/honey.ts
12204
+ // src/honey.ts
12166
12205
  /**
12167
12206
  * Honey tokens — decoy credentials that unlock nothing and alert when used.
12168
12207
  *
@@ -12207,8 +12246,7 @@ function registerHoneyTokenCommands(program) {
12207
12246
  console.error(`${honeyTokenId} deleted`);
12208
12247
  });
12209
12248
  }
12210
- //#endregion
12211
- //#region src/m2m.ts
12249
+ // src/m2m.ts
12212
12250
  /**
12213
12251
  * Resolve M2M client credentials from (in order) the process environment, a
12214
12252
  * `.env` overlay (for `seekrit run`), then saved config. Both halves must come
@@ -12445,8 +12483,7 @@ function registerInitCommand(program) {
12445
12483
  await runInit(options);
12446
12484
  });
12447
12485
  }
12448
- //#endregion
12449
- //#region src/logsink.ts
12486
+ // src/logsink.ts
12450
12487
  /** Collect repeated `--header Name: value` flags into a map. */
12451
12488
  function collectHeader(value, acc = {}) {
12452
12489
  const sep = value.indexOf(":");
@@ -12513,8 +12550,7 @@ function registerLogSinkCommands(program) {
12513
12550
  console.error("log sink removed");
12514
12551
  });
12515
12552
  }
12516
- //#endregion
12517
- //#region src/mongodb.ts
12553
+ // src/mongodb.ts
12518
12554
  /**
12519
12555
  * `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
12520
12556
  * secrets), the tier-2 sibling of `seekrit aws`.
@@ -12648,8 +12684,7 @@ function registerMongoCommands(program) {
12648
12684
  console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
12649
12685
  });
12650
12686
  }
12651
- //#endregion
12652
- //#region src/provisioner.ts
12687
+ // src/provisioner.ts
12653
12688
  /**
12654
12689
  * `seekrit provisioner` — helpers for the self-hosted **remote executor**
12655
12690
  * (`seekrit-provisioner`), the daemon that runs a target's provisioning SQL
@@ -12691,8 +12726,7 @@ function resolveLeaseAdminSecret(opts) {
12691
12726
  if (!adminUrl) fail(`provide the admin connection string via --admin-url or ${opts.adminUrlEnv}`);
12692
12727
  return adminUrl;
12693
12728
  }
12694
- //#endregion
12695
- //#region src/mysql.ts
12729
+ // src/mysql.ts
12696
12730
  /**
12697
12731
  * `seekrit mysql` — temporary MySQL/MariaDB credentials (Vault-style dynamic
12698
12732
  * secrets).
@@ -12832,8 +12866,7 @@ function collect$5(value, acc) {
12832
12866
  acc.push(value);
12833
12867
  return acc;
12834
12868
  }
12835
- //#endregion
12836
- //#region src/proxy-binary.ts
12869
+ // src/proxy-binary.ts
12837
12870
  /**
12838
12871
  * Fetch and run the `seekrit-proxy` binary without a Rust toolchain.
12839
12872
  *
@@ -12868,7 +12901,7 @@ function collect$5(value, acc) {
12868
12901
  * release-please-config.json), so the pin follows the crate without anyone
12869
12902
  * remembering to move it.
12870
12903
  */
12871
- const PROXY_VERSION = "0.13.0";
12904
+ const PROXY_VERSION = "0.15.0";
12872
12905
  const BIN = "seekrit-proxy";
12873
12906
  /**
12874
12907
  * Host → Rust target triple.
@@ -12904,7 +12937,7 @@ function versionPrefix(version) {
12904
12937
  return version.startsWith("v") ? version : `v${version}`;
12905
12938
  }
12906
12939
  function resolveVersion(explicit) {
12907
- return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
12940
+ return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.15.0";
12908
12941
  }
12909
12942
  function resolveBaseUrl(explicit) {
12910
12943
  return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
@@ -13014,8 +13047,7 @@ function signalNumber(signal) {
13014
13047
  SIGTERM: 15
13015
13048
  }[signal] ?? 0;
13016
13049
  }
13017
- //#endregion
13018
- //#region src/nomad.ts
13050
+ // src/nomad.ts
13019
13051
  /**
13020
13052
  * `seekrit nomad` — wire seekrit into [HashiCorp Nomad](https://developer.hashicorp.com/nomad)
13021
13053
  * as a **secret provider plugin**.
@@ -13399,8 +13431,7 @@ function registerNomadCommands(program) {
13399
13431
  function indent(text) {
13400
13432
  return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
13401
13433
  }
13402
- //#endregion
13403
- //#region src/orgs.ts
13434
+ // src/orgs.ts
13404
13435
  /**
13405
13436
  * Counts for `org show`. Each list is admin-gated to a different degree, so a
13406
13437
  * member who can't read one still gets the rest: a failed call reports as
@@ -13557,8 +13588,7 @@ function registerOrgCommands(program) {
13557
13588
  });
13558
13589
  });
13559
13590
  }
13560
- //#endregion
13561
- //#region src/proxy-presets.ts
13591
+ // src/proxy-presets.ts
13562
13592
  /** `Authorization: Bearer {{seekrit:NAME}}` — the shape most providers take. */
13563
13593
  function placeholder(secret) {
13564
13594
  return `{{seekrit:${secret}}}`;
@@ -13742,8 +13772,31 @@ function presetAllow(preset) {
13742
13772
  if (preset.allow) return preset.allow;
13743
13773
  return preset.secret ? [preset.secret] : [];
13744
13774
  }
13745
- //#endregion
13746
- //#region src/proxy-config.ts
13775
+ // src/proxy-config.ts
13776
+ /**
13777
+ * Generate a `seekrit-proxy.toml` — the other half of "adoptable without Rust".
13778
+ *
13779
+ * `npx @seekrit/proxy` removes the toolchain; this removes the config file. The
13780
+ * proxy's TOML is not large, but it is the kind of file where every field has to
13781
+ * be right at once before anything works: a bare hostname in one place and a
13782
+ * base URL in another, a path glob matched against the *upstream-facing* path,
13783
+ * a trust anchor that must not come from us, and two modes whose blocks look
13784
+ * similar and mean different things. Generating it turns first-run from an
13785
+ * exercise in reading reference docs into a command.
13786
+ *
13787
+ * Two things this module is careful about:
13788
+ *
13789
+ * - **It writes the config the proxy actually accepts.** In server-policy mode
13790
+ * `allow`/`methods`/`paths` on a route are *rejected*, not ignored, and the
13791
+ * forward proxy's hosts come from the bundle — so the server-mode emitter
13792
+ * leaves all four out rather than emitting a file that fails at startup. The
13793
+ * golden fixtures in `apps/proxy/testdata/generated-configs/` are parsed by a
13794
+ * Rust test, so this cannot drift from the parser.
13795
+ * - **It never pretends a fetched thumbprint is a local trust anchor.** The
13796
+ * `signers` list is the one field that must not come from the API. What is
13797
+ * emitted is annotated with where it came from, and the generated file says
13798
+ * so, in the file, next to the value.
13799
+ */
13747
13800
  /**
13748
13801
  * A deliberately small TOML writer: basic strings and arrays of them, which is
13749
13802
  * every value in this config. Full TOML is not needed and a general emitter
@@ -13964,6 +14017,22 @@ function splitHostPort(addr) {
13964
14017
  if (idx === -1) return [addr, "8080"];
13965
14018
  return [addr.slice(0, idx), addr.slice(idx + 1)];
13966
14019
  }
14020
+ /**
14021
+ * Where a policy rule's service actually is, as a reverse-proxy upstream.
14022
+ *
14023
+ * A rule names its ports (HTTPS on 443 unless it says otherwise), so a route
14024
+ * generated from it has to dial one of them — `https://host` for a rule that
14025
+ * only covers `http_ports: [5000]` would route to a service the rule never
14026
+ * mentioned. The route is routing, not authorization: the reverse plane never
14027
+ * refuses on ports, because this URL is the operator's to edit.
14028
+ */
14029
+ function upstreamForRule(rule) {
14030
+ const { https, http } = policyRulePorts(rule);
14031
+ if (https.includes(443)) return `https://${rule.host}`;
14032
+ if (https.length > 0) return `https://${rule.host}:${https[0]}`;
14033
+ const port = http[0];
14034
+ return port === 80 ? `http://${rule.host}` : `http://${rule.host}:${port}`;
14035
+ }
13967
14036
  /** Build a file-policy plan from presets and/or ad-hoc `host=SECRET` rules. */
13968
14037
  function planFromPresets(presets, options) {
13969
14038
  const taken = /* @__PURE__ */ new Set();
@@ -14039,7 +14108,7 @@ function planFromPolicy(args, options) {
14039
14108
  const baseUrl = baseUrlFor(options.listen, prefix, preset?.baseUrlSuffix ?? "");
14040
14109
  routes.push({
14041
14110
  prefix,
14042
- upstream: `https://${rule.host}`,
14111
+ upstream: upstreamForRule(rule),
14043
14112
  host: rule.host,
14044
14113
  allow: [],
14045
14114
  methods: [],
@@ -14182,8 +14251,7 @@ function renderComposeSnippet(plan, options) {
14182
14251
  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.");
14183
14252
  return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
14184
14253
  }
14185
- //#endregion
14186
- //#region src/paperclip.ts
14254
+ // src/paperclip.ts
14187
14255
  /**
14188
14256
  * `seekrit paperclip` — wire seekrit into a [Paperclip](https://docs.paperclip.ing)
14189
14257
  * agent.
@@ -14371,8 +14439,7 @@ function registerPaperclipCommands(program) {
14371
14439
  });
14372
14440
  });
14373
14441
  }
14374
- //#endregion
14375
- //#region src/pg.ts
14442
+ // src/pg.ts
14376
14443
  /**
14377
14444
  * `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
14378
14445
  *
@@ -14524,8 +14591,7 @@ function collect$3(value, acc) {
14524
14591
  acc.push(value);
14525
14592
  return acc;
14526
14593
  }
14527
- //#endregion
14528
- //#region src/proxy.ts
14594
+ // src/proxy.ts
14529
14595
  /**
14530
14596
  * `seekrit proxy` — get `seekrit-proxy` running without a Rust toolchain or a
14531
14597
  * hand-written TOML file.
@@ -14854,10 +14920,10 @@ Next:
14854
14920
  });
14855
14921
  console.log(path);
14856
14922
  const { target } = detectTarget();
14857
- process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.13.0")} (${target})\n`);
14923
+ process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.15.0")} (${target})\n`);
14858
14924
  });
14859
14925
  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) => {
14860
- const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
14926
+ const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.15.0";
14861
14927
  const override = process.env.SEEKRIT_PROXY_BIN;
14862
14928
  const { target, exe } = detectTarget();
14863
14929
  const path = override ?? proxyBinaryPath(version, target, exe);
@@ -14916,13 +14982,12 @@ Next:
14916
14982
  process.stdout.write(renderComposeSnippet(plan, {
14917
14983
  service: options.service,
14918
14984
  workload: options.workload,
14919
- image: options.image ?? `seekritdev/proxy:0.13.0`,
14985
+ image: options.image ?? `seekritdev/proxy:0.15.0`,
14920
14986
  publish: Boolean(options.publish)
14921
14987
  }));
14922
14988
  });
14923
14989
  }
14924
- //#endregion
14925
- //#region src/redis.ts
14990
+ // src/redis.ts
14926
14991
  /**
14927
14992
  * `seekrit redis` — temporary Redis (6+) credentials (Vault-style dynamic
14928
14993
  * secrets).
@@ -15062,8 +15127,7 @@ function collect$2(value, acc) {
15062
15127
  acc.push(value);
15063
15128
  return acc;
15064
15129
  }
15065
- //#endregion
15066
- //#region src/rotation.ts
15130
+ // src/rotation.ts
15067
15131
  /**
15068
15132
  * `seekrit rotation` — managed, scheduled rotation of a stored secret's value.
15069
15133
  *
@@ -15230,8 +15294,7 @@ function registerRotationCommands(program) {
15230
15294
  console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
15231
15295
  });
15232
15296
  }
15233
- //#endregion
15234
- //#region src/run-command.ts
15297
+ // src/run-command.ts
15235
15298
  /** Keep signal handlers installed from provisioning through asynchronous cleanup. */
15236
15299
  async function runCommand(cmd, args, prepare, cleanup) {
15237
15300
  let child;
@@ -15401,8 +15464,7 @@ async function reapStragglers(pids, signal) {
15401
15464
  process.kill(pid, "SIGKILL");
15402
15465
  } catch {}
15403
15466
  }
15404
- //#endregion
15405
- //#region src/run-leases.ts
15467
+ // src/run-leases.ts
15406
15468
  /** Validate all flags before contacting the API or creating any database roles. */
15407
15469
  function parseRunLeaseOptions(bindings = [], ttl) {
15408
15470
  if (!bindings.length) {
@@ -15527,8 +15589,7 @@ var RunLeases = class {
15527
15589
  return ok;
15528
15590
  }
15529
15591
  };
15530
- //#endregion
15531
- //#region src/ssh.ts
15592
+ // src/ssh.ts
15532
15593
  /**
15533
15594
  * `seekrit ssh` — temporary SSH access via short-lived certificates (Vault-style
15534
15595
  * dynamic secrets, the SSH sibling of `seekrit pg`).
@@ -15670,8 +15731,7 @@ function collect$1(value, acc) {
15670
15731
  acc.push(value);
15671
15732
  return acc;
15672
15733
  }
15673
- //#endregion
15674
- //#region src/sync.ts
15734
+ // src/sync.ts
15675
15735
  /** Split a `--flag a,b` list into trimmed, non-empty members. */
15676
15736
  function list(raw, fallback) {
15677
15737
  return (raw ?? fallback).split(",").map((item) => item.trim()).filter(Boolean);
@@ -16417,8 +16477,7 @@ function registerSyncCommands(program) {
16417
16477
  ], "no sync runs yet"));
16418
16478
  });
16419
16479
  }
16420
- //#endregion
16421
- //#region src/web-login.ts
16480
+ // src/web-login.ts
16422
16481
  /**
16423
16482
  * `seekrit login` — sign in through the browser.
16424
16483
  *
@@ -16593,8 +16652,7 @@ async function runLogout() {
16593
16652
  });
16594
16653
  if (config.clientId) console.error("(machine client credentials are kept — remove them with `seekrit login`)");
16595
16654
  }
16596
- //#endregion
16597
- //#region src/index.ts
16655
+ // src/index.ts
16598
16656
  /** Collect repeated `--with group=env` flags into a map. */
16599
16657
  function collectKv(value, acc = {}) {
16600
16658
  const eq = value.indexOf("=");
@@ -17045,7 +17103,7 @@ registerKmsCommands(program);
17045
17103
  registerRotationCommands(program);
17046
17104
  registerRecoveryCommands(program);
17047
17105
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
17048
- const { runMcpServer } = await import("./mcp-Dgj-7S9y.js");
17106
+ const { runMcpServer } = await import("./mcp-Clul2n4l.js");
17049
17107
  await runMcpServer();
17050
17108
  });
17051
17109
  registerAuditCommands(program);
@@ -17059,5 +17117,4 @@ const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
17059
17117
  program.parseAsync(argv).catch((err) => {
17060
17118
  fail(describeError(err));
17061
17119
  });
17062
- //#endregion
17063
17120
  export { signatureKeyRef as A, generateDek as B, version as C, importSigningKey as D, generateSigningKeyMaterial as E, generateEncryptKeyMaterial as F, parseBranchTtl as H, kmsBlobKeyRef as I, kmsDecrypt as L, generatePostgresCredential as M, generateMysqlCredential as N, importVerifyingKey as O, generateDataKey as P, kmsEncrypt as R, writeProjectConfig as S, parseServiceToken as T, toBase64 as V, getDek as _, kmsResolveRecipient as a, tryBuildContext as b, fetchDecryptedSecrets as c, resolveApp as d, resolveAppEnv as f, resolveOrg as g, resolveGroup as h, kmsResolveKey as i, verifyMessage as j, signMessage as k, fetchDecryptedVersion as l, resolveEnvTarget as m, kmsCallerIdentity as n, syncGrantsForGroupLink as o, resolveBranch as p, kmsRecoverMaterial as r, encryptAndSetSecret as s, ensureM2mAdminToken as t, materializeEnv as u, isTokenAuth as v, createServiceToken as w, setFailThrows as x, principalPublicKeyJwk as y, wrapDek as z };
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { spawn } from "node:child_process";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- //#region src/mcp.ts
6
+ // src/mcp.ts
7
7
  /** Lowercase-alphanumeric string for a fresh Postgres role name. */
8
8
  function randomLower(length) {
9
9
  const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -1023,5 +1023,4 @@ async function runMcpServer(options = {}) {
1023
1023
  });
1024
1024
  await server.connect(new StdioServerTransport());
1025
1025
  }
1026
- //#endregion
1027
1026
  export { runMcpServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "homepage": "https://seekrit.dev/docs/guides/cli",
6
6
  "keywords": [
@@ -38,17 +38,17 @@
38
38
  "node": ">=20"
39
39
  },
40
40
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.32.1",
41
+ "@modelcontextprotocol/sdk": "^1.32.2",
42
42
  "commander": "^15.0.0",
43
43
  "zod": "^4.6.7"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^26.6.5",
47
- "tsdown": "^0.23.1",
47
+ "tsdown": "^0.23.3",
48
48
  "vitest": "^5.0.5",
49
- "@seekrit/api-client": "0.0.1",
50
49
  "@seekrit/core": "0.0.1",
51
- "@seekrit/crypto": "0.0.1"
50
+ "@seekrit/crypto": "0.0.1",
51
+ "@seekrit/api-client": "0.0.1"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsdown",