@seekrit/cli 2.1.0 → 2.2.1

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",
@@ -2086,6 +2156,8 @@ const AUDIT_ACTIONS = [
2086
2156
  "org.created",
2087
2157
  "org.updated",
2088
2158
  "org.member_added",
2159
+ "org.member_role_changed",
2160
+ "app.access_granted",
2089
2161
  "org.member_removed",
2090
2162
  "org.member_invited",
2091
2163
  "org.invite_revoked",
@@ -2299,8 +2371,7 @@ const NOTIFICATION_TYPE_META = {
2299
2371
  defaultEnabled: true
2300
2372
  }
2301
2373
  };
2302
- //#endregion
2303
- //#region ../../packages/core/src/schemas.ts
2374
+ // ../../packages/core/src/schemas.ts
2304
2375
  /** URL-safe identifier segment: `my-app`, `production`, … */
2305
2376
  const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
2306
2377
  const nameSchema = z.string().trim().min(1).max(128);
@@ -2308,7 +2379,7 @@ const nameSchema = z.string().trim().min(1).max(128);
2308
2379
  const emailSchema = z.string().trim().toLowerCase().pipe(z.email().max(320));
2309
2380
  /** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
2310
2381
  const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
2311
- z.enum([
2382
+ const orgRoleSchema = z.enum([
2312
2383
  "owner",
2313
2384
  "admin",
2314
2385
  "member"
@@ -2347,7 +2418,20 @@ z.object({
2347
2418
  name: nameSchema,
2348
2419
  slug: slugSchema
2349
2420
  });
2421
+ /** Compose a group into an application environment (matched by env slug). */
2422
+ const appAccessKeySchema = z.object({
2423
+ environmentId: z.string().min(1),
2424
+ userId: z.string().min(1),
2425
+ wrappedDek: z.string().min(1).max(8192)
2426
+ });
2427
+ const appAccessGrantsSchema = z.array(appAccessKeySchema).max(5e3).optional();
2428
+ z.object({
2429
+ userId: z.string().min(1),
2430
+ grants: z.array(appAccessKeySchema).max(5e3)
2431
+ });
2432
+ z.object({ role: orgRoleSchema });
2350
2433
  z.object({
2434
+ appAccessGrants: appAccessGrantsSchema,
2351
2435
  groupId: z.string().min(1),
2352
2436
  /** Precedence among an env's groups (higher wins). Appended if omitted. */
2353
2437
  position: z.number().int().min(0).optional(),
@@ -2358,6 +2442,11 @@ z.object({
2358
2442
  })).optional()
2359
2443
  });
2360
2444
  z.object({
2445
+ /** Wrap the new DEK to all persistent app recipients. */
2446
+ userGrants: z.array(z.object({
2447
+ userId: z.string().min(1),
2448
+ wrappedDek: z.string().min(1).max(8192)
2449
+ })).max(5e3).optional(),
2361
2450
  name: nameSchema,
2362
2451
  slug: slugSchema,
2363
2452
  /** Environment DEK wrapped to the creator's public key — created client-side. */
@@ -2419,7 +2508,23 @@ z.object({
2419
2508
  /** The same DEK wrapped to each of the parent's existing grant-holders. */
2420
2509
  grants: z.array(grantEnvironmentKeySchema).max(500).default([])
2421
2510
  });
2511
+ /**
2512
+ * Token creation is client-driven so the server never sees the token secret:
2513
+ * the client generates the keypair + token string and submits only the hash
2514
+ * and public key.
2515
+ */
2516
+ const environmentGrantBatchSchema = z.array(z.object({
2517
+ environmentId: z.string().min(1),
2518
+ wrappedDek: z.string().min(1).max(8192)
2519
+ })).max(5e3);
2520
+ z.object({
2521
+ principalType: principalTypeSchema,
2522
+ principalId: z.string().min(1),
2523
+ grants: environmentGrantBatchSchema
2524
+ });
2422
2525
  z.object({
2526
+ /** Commit the token and all default resolve keys together, when supplied. */
2527
+ initialGrants: environmentGrantBatchSchema.optional(),
2423
2528
  name: nameSchema,
2424
2529
  tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
2425
2530
  /** SHA-256 hash (base64url) of the full token string. */
@@ -2688,8 +2793,7 @@ z.object({
2688
2793
  */
2689
2794
  status: settableRotationStatusSchema.optional()
2690
2795
  }).refine((v) => v.intervalSeconds !== void 0 || v.config !== void 0 || v.status !== void 0, "provide at least one of intervalSeconds, config, or status");
2691
- //#endregion
2692
- //#region ../../packages/core/src/sync.ts
2796
+ // ../../packages/core/src/sync.ts
2693
2797
  /**
2694
2798
  * Third-party sync: seekrit as source of truth, pushing an environment's
2695
2799
  * resolved secrets out to a platform that holds its own copy (Vercel project
@@ -4173,8 +4277,7 @@ z.object({
4173
4277
  mode: z.enum(["auto", "manual"]).optional(),
4174
4278
  enabled: z.boolean().optional()
4175
4279
  });
4176
- //#endregion
4177
- //#region ../../packages/core/src/vault.ts
4280
+ // ../../packages/core/src/vault.ts
4178
4281
  /** One encrypted credential within a connection. */
4179
4282
  const VAULT_SLOTS = [
4180
4283
  "oauth2",
@@ -4458,8 +4561,7 @@ z.object({
4458
4561
  userRef: vaultUserRefSchema,
4459
4562
  leaseId: leaseIdSchema
4460
4563
  });
4461
- //#endregion
4462
- //#region ../../packages/crypto/src/encoding.ts
4564
+ // ../../packages/crypto/src/encoding.ts
4463
4565
  const CHUNK = 32768;
4464
4566
  /** Base64url (no padding) — portable across browsers, Workers, and Node. */
4465
4567
  function toBase64Url(bytes) {
@@ -4490,8 +4592,7 @@ function utf8Encode(text) {
4490
4592
  function utf8Decode(bytes) {
4491
4593
  return new TextDecoder().decode(bytes);
4492
4594
  }
4493
- //#endregion
4494
- //#region ../../packages/crypto/src/errors.ts
4595
+ // ../../packages/crypto/src/errors.ts
4495
4596
  var SeekritCryptoError = class extends Error {
4496
4597
  code;
4497
4598
  constructor(code, message) {
@@ -4511,8 +4612,7 @@ function splitBlob(blob, prefix, segments) {
4511
4612
  if (parts.length !== segments + 1 || parts.some((p) => p.length === 0)) throw new SeekritCryptoError("MALFORMED_BLOB", `malformed "${prefix}" blob`);
4512
4613
  return parts.slice(1);
4513
4614
  }
4514
- //#endregion
4515
- //#region ../../packages/crypto/src/aes.ts
4615
+ // ../../packages/crypto/src/aes.ts
4516
4616
  const SECRET_PREFIX = "sc1";
4517
4617
  const IV_LENGTH$1 = 12;
4518
4618
  /** Generate a fresh 256-bit data encryption key for an environment. */
@@ -4557,8 +4657,7 @@ async function decryptSecret(dek, blob, aad) {
4557
4657
  function secretAad(environmentId, secretName) {
4558
4658
  return `${environmentId}/${secretName}`;
4559
4659
  }
4560
- //#endregion
4561
- //#region ../../packages/crypto/src/keys.ts
4660
+ // ../../packages/crypto/src/keys.ts
4562
4661
  async function generateKeyPair() {
4563
4662
  const pair = await crypto.subtle.generateKey({
4564
4663
  name: "ECDH",
@@ -4591,8 +4690,7 @@ async function importPrivateKeyPkcs8(pkcs8) {
4591
4690
  namedCurve: "P-256"
4592
4691
  }, true, ["deriveBits"]);
4593
4692
  }
4594
- //#endregion
4595
- //#region ../../packages/crypto/src/wrap.ts
4693
+ // ../../packages/crypto/src/wrap.ts
4596
4694
  /**
4597
4695
  * ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
4598
4696
  * the recipient's public key; the shared secret is run through HKDF-SHA256 to
@@ -4660,8 +4758,7 @@ async function unwrapDek(wrapped, privateKey) {
4660
4758
  throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
4661
4759
  }
4662
4760
  }
4663
- //#endregion
4664
- //#region ../../packages/crypto/src/aws.ts
4761
+ // ../../packages/crypto/src/aws.ts
4665
4762
  /** Generate the ephemeral P-256 keypair a client uses to receive one AWS lease. */
4666
4763
  async function generateAwsRecipientKeyPair() {
4667
4764
  return generateKeyPair();
@@ -4688,8 +4785,7 @@ async function unwrapAwsCredential(wrapped, privateKeyJwk) {
4688
4785
  region: c.region
4689
4786
  };
4690
4787
  }
4691
- //#endregion
4692
- //#region ../../packages/crypto/src/gcp.ts
4788
+ // ../../packages/crypto/src/gcp.ts
4693
4789
  /** Generate the ephemeral P-256 keypair a client uses to receive one GCP lease. */
4694
4790
  async function generateGcpRecipientKeyPair() {
4695
4791
  return generateKeyPair();
@@ -4715,8 +4811,7 @@ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
4715
4811
  scopes: c.scopes
4716
4812
  };
4717
4813
  }
4718
- //#endregion
4719
- //#region ../../packages/crypto/src/kms.ts
4814
+ // ../../packages/crypto/src/kms.ts
4720
4815
  /**
4721
4816
  * Client-side KMS envelope encryption. A managed `encrypt` key is a 256-bit
4722
4817
  * AES-GCM key whose material reaches the client only as a `wd1.` grant (wrapped
@@ -4846,8 +4941,7 @@ async function decryptDataKey(material, wrapped) {
4846
4941
  throw new SeekritCryptoError("DECRYPT_FAILED", "data key unwrap failed: wrong key or tampered blob");
4847
4942
  }
4848
4943
  }
4849
- //#endregion
4850
- //#region ../../packages/crypto/src/random.ts
4944
+ // ../../packages/crypto/src/random.ts
4851
4945
  const ALPHABETS = {
4852
4946
  alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
4853
4947
  hex: "0123456789abcdef",
@@ -4883,8 +4977,7 @@ function generateSecretValue(length, alphabet = "alphanumeric") {
4883
4977
  }
4884
4978
  return out;
4885
4979
  }
4886
- //#endregion
4887
- //#region ../../packages/crypto/src/mongodb.ts
4980
+ // ../../packages/crypto/src/mongodb.ts
4888
4981
  /** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
4889
4982
  async function generateMongoRecipientKeyPair() {
4890
4983
  return generateKeyPair();
@@ -4918,8 +5011,7 @@ async function unwrapMongoCredential(wrapped, privateKeyJwk) {
4918
5011
  function mongoConnectionUri(cred) {
4919
5012
  return `mongodb://${encodeURIComponent(cred.username)}:${encodeURIComponent(cred.password)}@${cred.host}:${cred.port}/${cred.database}?authSource=${cred.authSource}`;
4920
5013
  }
4921
- //#endregion
4922
- //#region ../../packages/crypto/src/mysql.ts
5014
+ // ../../packages/crypto/src/mysql.ts
4923
5015
  /**
4924
5016
  * Client-side construction of a MySQL/MariaDB `mysql_native_password`
4925
5017
  * authentication string, for minting *temporary MySQL login credentials*
@@ -4975,8 +5067,7 @@ async function generateMysqlCredential(options = {}) {
4975
5067
  verifier: await mysqlNativePasswordVerifier(password)
4976
5068
  };
4977
5069
  }
4978
- //#endregion
4979
- //#region ../../packages/crypto/src/passphrase.ts
5070
+ // ../../packages/crypto/src/passphrase.ts
4980
5071
  /**
4981
5072
  * User private keys are stored server-side encrypted under a key derived from
4982
5073
  * the user's passphrase, so any browser or CLI session can fetch and unlock
@@ -5035,8 +5126,7 @@ async function decryptPrivateKey(passphrase, blob) {
5035
5126
  throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
5036
5127
  }
5037
5128
  }
5038
- //#endregion
5039
- //#region ../../packages/crypto/src/policy-key.ts
5129
+ // ../../packages/crypto/src/policy-key.ts
5040
5130
  /**
5041
5131
  * Signing with a principal's **existing** keypair, for agent access policy.
5042
5132
  *
@@ -5105,8 +5195,7 @@ function policySignerJwk(publicKeyJwk) {
5105
5195
  y: jwk.y
5106
5196
  };
5107
5197
  }
5108
- //#endregion
5109
- //#region ../../packages/crypto/src/shamir.ts
5198
+ // ../../packages/crypto/src/shamir.ts
5110
5199
  /**
5111
5200
  * Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
5112
5201
  * reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b). Splits a byte string
@@ -5235,8 +5324,7 @@ function combineSecret(shares) {
5235
5324
  }
5236
5325
  return secret;
5237
5326
  }
5238
- //#endregion
5239
- //#region ../../packages/crypto/src/recovery.ts
5327
+ // ../../packages/crypto/src/recovery.ts
5240
5328
  /** Generate a fresh org recovery keypair (P-256, same as any principal). */
5241
5329
  function generateRecoveryKey() {
5242
5330
  return generateKeyPair();
@@ -5290,8 +5378,7 @@ async function combineRecoveryShares(shareBytes) {
5290
5378
  throw new SeekritCryptoError("DECRYPT_FAILED", "recovery reconstruction failed: wrong, insufficient, or mismatched shares");
5291
5379
  }
5292
5380
  }
5293
- //#endregion
5294
- //#region ../../packages/crypto/src/redis.ts
5381
+ // ../../packages/crypto/src/redis.ts
5295
5382
  /**
5296
5383
  * Client-side construction of a Redis (6+) ACL password verifier, for minting
5297
5384
  * *temporary Redis login credentials* without the password plaintext ever
@@ -5392,8 +5479,7 @@ async function generatePostgresCredential(options = {}) {
5392
5479
  iterations
5393
5480
  };
5394
5481
  }
5395
- //#endregion
5396
- //#region ../../packages/crypto/src/sign.ts
5482
+ // ../../packages/crypto/src/sign.ts
5397
5483
  /**
5398
5484
  * Managed signing keys for the client-side KMS — ECDSA over P-256 (the curve
5399
5485
  * already used for principal keypairs; universal in WebCrypto). A `sign` key's
@@ -5455,8 +5541,7 @@ function signatureKeyRef(signature) {
5455
5541
  version
5456
5542
  };
5457
5543
  }
5458
- //#endregion
5459
- //#region ../../packages/crypto/src/ssh.ts
5544
+ // ../../packages/crypto/src/ssh.ts
5460
5545
  /**
5461
5546
  * Client-side SSH certificate authority for minting *temporary SSH access*
5462
5547
  * without any private key ever reaching seekrit's control plane.
@@ -5587,8 +5672,7 @@ function encodeOpensshPrivateKey(seed, pub, comment) {
5587
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());
5588
5673
  return `-----BEGIN OPENSSH PRIVATE KEY-----\n${b64.match(/.{1,70}/g)?.join("\n") ?? b64}\n-----END OPENSSH PRIVATE KEY-----\n`;
5589
5674
  }
5590
- //#endregion
5591
- //#region ../../packages/crypto/src/token.ts
5675
+ // ../../packages/crypto/src/token.ts
5592
5676
  /**
5593
5677
  * Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
5594
5678
  * principals. One local bundle holds independent authentication and decryption
@@ -5698,11 +5782,9 @@ async function createAgentTaskToken() {
5698
5782
  tokenHash: await hashToken(token)
5699
5783
  };
5700
5784
  }
5701
- //#endregion
5702
- //#region package.json
5703
- var version = "2.1.0";
5704
- //#endregion
5705
- //#region ../../packages/api-client/src/index.ts
5785
+ // package.json
5786
+ var version = "2.2.1";
5787
+ // ../../packages/api-client/src/index.ts
5706
5788
  /**
5707
5789
  * The proposer's note travels as a header, not a body field: an unprotected
5708
5790
  * environment ignores it, so the same call works either way and no caller has
@@ -5837,6 +5919,56 @@ var SeekritClient = class {
5837
5919
  listMembers(orgId) {
5838
5920
  return this.request("GET", `/v1/orgs/${orgId}/members`);
5839
5921
  }
5922
+ updateMemberRole(orgId, userId, role) {
5923
+ return this.request("PATCH", `/v1/orgs/${orgId}/members/${userId}`, { role });
5924
+ }
5925
+ getAppAccess(orgId, appId) {
5926
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}/access`);
5927
+ }
5928
+ provisionAppAccess(orgId, appId, userId, grants) {
5929
+ return this.request("POST", `/v1/orgs/${orgId}/apps/${appId}/access`, {
5930
+ userId,
5931
+ grants
5932
+ });
5933
+ }
5934
+ accessRecipients(orgId, target) {
5935
+ const path = "appId" in target ? `apps/${target.appId}` : `groups/${target.groupId}`;
5936
+ return this.request("GET", `/v1/orgs/${orgId}/${path}/access-recipients`);
5937
+ }
5938
+ /** The caller owns the new DEK; only public keys and wrapped blobs cross the API. */
5939
+ async wrapNewEnvironmentAccess(orgId, target, dek) {
5940
+ const { recipients } = await this.accessRecipients(orgId, target);
5941
+ return Promise.all(recipients.map(async (r) => {
5942
+ if (!r.publicKeyJwk) throw new Error(`${r.email} must finish key setup before creating this environment`);
5943
+ return {
5944
+ userId: r.userId,
5945
+ wrappedDek: await wrapDek(dek, r.publicKeyJwk)
5946
+ };
5947
+ }));
5948
+ }
5949
+ async wrapGroupAppAccess(orgId, envId, groupId, getDek) {
5950
+ const { environment } = await this.getEnv(orgId, envId);
5951
+ if (!environment.applicationId) throw new Error("only app environments can compose groups");
5952
+ const { recipients } = await this.accessRecipients(orgId, { appId: environment.applicationId });
5953
+ if (!recipients.length) return [];
5954
+ const { environments } = await this.listGroupEnvs(orgId, groupId);
5955
+ const result = [];
5956
+ for (const env of environments) {
5957
+ const { grants } = await this.listEnvKeys(orgId, env.id);
5958
+ const missing = recipients.filter((r) => !grants.some((g) => g.principalType === "user" && g.principalId === r.userId));
5959
+ if (!missing.length) continue;
5960
+ const dek = await getDek(env.id);
5961
+ for (const recipient of missing) {
5962
+ if (!recipient.publicKeyJwk) throw new Error(`${recipient.email} must finish key setup first`);
5963
+ result.push({
5964
+ environmentId: env.id,
5965
+ userId: recipient.userId,
5966
+ wrappedDek: await wrapDek(dek, recipient.publicKeyJwk)
5967
+ });
5968
+ }
5969
+ }
5970
+ return result;
5971
+ }
5840
5972
  /**
5841
5973
  * The org-wide "require a second factor for all members" policy. `configured`
5842
5974
  * is false when the identity provider isn't wired up (local dev), in which
@@ -6021,6 +6153,46 @@ var SeekritClient = class {
6021
6153
  return this.request("POST", `/v1/orgs/${orgId}/change-requests/${id}/withdraw`);
6022
6154
  }
6023
6155
  /** The calling principal's wrapped DEK for this environment. */
6156
+ getEnvironmentKeyScope(orgId, envId) {
6157
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key-scope`);
6158
+ }
6159
+ async prepareComposedKeyGrants(orgId, envId, principal, getDek) {
6160
+ const scope = await this.getEnvironmentKeyScope(orgId, envId);
6161
+ const grants = [];
6162
+ for (const layer of scope.environments) {
6163
+ if (scope.grants.some((g) => g.environmentId === layer.environmentId && g.principalType === principal.principalType && g.principalId === principal.principalId)) continue;
6164
+ try {
6165
+ const dek = await getDek(layer.environmentId);
6166
+ grants.push({
6167
+ environmentId: layer.environmentId,
6168
+ wrappedDek: await wrapDek(dek, principal.publicKeyJwk)
6169
+ });
6170
+ } catch (error) {
6171
+ if (error instanceof Error && error.message === "unlock cancelled") throw error;
6172
+ throw new Error(`Could not prepare access to ${layer.label}: ${error instanceof Error ? error.message : "key unavailable"}. No keys were granted.`);
6173
+ }
6174
+ }
6175
+ return grants;
6176
+ }
6177
+ grantComposedKeys(orgId, envId, input) {
6178
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/keys/composed`, input);
6179
+ }
6180
+ /** Sharing an application environment includes the exact layers resolve reads. */
6181
+ async grantEnvironmentAccess(orgId, envId, principal, getDek) {
6182
+ const { environment } = await this.getEnv(orgId, envId);
6183
+ if (environment.applicationId) {
6184
+ const grants = await this.prepareComposedKeyGrants(orgId, envId, principal, getDek);
6185
+ await this.grantComposedKeys(orgId, envId, {
6186
+ principalType: principal.principalType,
6187
+ principalId: principal.principalId,
6188
+ grants
6189
+ });
6190
+ } else await this.grantEnvKey(orgId, envId, {
6191
+ principalType: principal.principalType,
6192
+ principalId: principal.principalId,
6193
+ wrappedDek: await wrapDek(await getDek(envId), principal.publicKeyJwk)
6194
+ });
6195
+ }
6024
6196
  getMyEnvKey(orgId, envId) {
6025
6197
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
6026
6198
  }
@@ -6609,8 +6781,7 @@ function writeProjectConfig(config, dir = process.cwd()) {
6609
6781
  writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
6610
6782
  return path;
6611
6783
  }
6612
- //#endregion
6613
- //#region src/io.ts
6784
+ // src/io.ts
6614
6785
  let failThrows = false;
6615
6786
  /**
6616
6787
  * In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
@@ -6735,8 +6906,7 @@ async function readStdin() {
6735
6906
  for await (const chunk of process.stdin) chunks.push(chunk);
6736
6907
  return Buffer.concat(chunks).toString("utf8");
6737
6908
  }
6738
- //#endregion
6739
- //#region src/context.ts
6909
+ // src/context.ts
6740
6910
  /** Sent as `x-seekrit-client` so the API attributes usage to the CLI (analytics). */
6741
6911
  const CLI_CLIENT = `cli/${version}`;
6742
6912
  /**
@@ -6876,8 +7046,7 @@ async function getDek(ctx, orgId, envId) {
6876
7046
  const [{ wrappedDek }, privateKey] = await Promise.all([ctx.client.getMyEnvKey(orgId, envId), getPrivateKey(ctx)]);
6877
7047
  return unwrapDek(wrappedDek, privateKey);
6878
7048
  }
6879
- //#endregion
6880
- //#region src/output.ts
7049
+ // src/output.ts
6881
7050
  /** Shorthand for a column whose header is fixed and value is one field. */
6882
7051
  function col(header, value) {
6883
7052
  return {
@@ -6939,8 +7108,7 @@ function section(title) {
6939
7108
  console.log("");
6940
7109
  console.log(title);
6941
7110
  }
6942
- //#endregion
6943
- //#region src/target.ts
7111
+ // src/target.ts
6944
7112
  /** Resolve the target org from a flag, the committed config, or a lone org. */
6945
7113
  async function resolveOrg(ctx, orgSlug) {
6946
7114
  const wanted = orgSlug ?? findProjectConfig()?.org;
@@ -7096,8 +7264,7 @@ async function resolveGroup(ctx, opts) {
7096
7264
  slug: group.slug
7097
7265
  };
7098
7266
  }
7099
- //#endregion
7100
- //#region src/access.ts
7267
+ // src/access.ts
7101
7268
  /**
7102
7269
  * Human-readable labels for the principals holding a key grant. `grant list`
7103
7270
  * would otherwise be a column of opaque ids, which is exactly the question
@@ -7154,12 +7321,11 @@ function registerAccessCommands(program) {
7154
7321
  const ctx = buildContext();
7155
7322
  const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
7156
7323
  const principal = await resolvePrincipal(ctx, orgId, options);
7157
- const dek = await getDek(ctx, orgId, envId);
7158
- await ctx.client.grantEnvKey(orgId, envId, {
7324
+ await ctx.client.grantEnvironmentAccess(orgId, envId, {
7159
7325
  principalType: principal.type,
7160
7326
  principalId: principal.id,
7161
- wrappedDek: await wrapDek(dek, principal.publicKeyJwk)
7162
- });
7327
+ publicKeyJwk: principal.publicKeyJwk
7328
+ }, (id) => getDek(ctx, orgId, id));
7163
7329
  console.error(`granted ${label} access to ${principal.id}`);
7164
7330
  });
7165
7331
  withEnvTarget(grant.command("list").alias("ls").description("list who holds a key for an environment").option("--json", "print the raw API response")).action(async (options) => {
@@ -7191,8 +7357,7 @@ function registerAccessCommands(program) {
7191
7357
  console.error(`revoked ${options.user ?? options.token}'s access to ${label}`);
7192
7358
  });
7193
7359
  }
7194
- //#endregion
7195
- //#region src/account.ts
7360
+ // src/account.ts
7196
7361
  function sessionStatus(session) {
7197
7362
  if (session.revokedAt) return "revoked";
7198
7363
  if (session.expiresAt && Date.parse(session.expiresAt) < Date.now()) return "expired";
@@ -7242,8 +7407,7 @@ function registerAccountCommands(program) {
7242
7407
  console.error(`${type} emails ${state}`);
7243
7408
  });
7244
7409
  }
7245
- //#endregion
7246
- //#region src/agents.ts
7410
+ // src/agents.ts
7247
7411
  /**
7248
7412
  * Agent access governance from the CLI.
7249
7413
  *
@@ -7375,6 +7539,14 @@ function describeList(values) {
7375
7539
  function describeSecrets(values) {
7376
7540
  return values.length === 0 ? "none" : values.join(" ");
7377
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
+ }
7378
7550
  function printRules(rules) {
7379
7551
  printTable(rules.map((rule, i) => ({
7380
7552
  n: i + 1,
@@ -7382,6 +7554,7 @@ function printRules(rules) {
7382
7554
  })), [
7383
7555
  col("#", (r) => r.n),
7384
7556
  col("host", (r) => r.rule.host),
7557
+ col("ports", (r) => describePorts(r.rule)),
7385
7558
  col("methods", (r) => describeList(r.rule.methods)),
7386
7559
  col("paths", (r) => describeList(r.rule.paths)),
7387
7560
  col("secrets", (r) => describeSecrets(r.rule.allow)),
@@ -7390,8 +7563,9 @@ function printRules(rules) {
7390
7563
  }
7391
7564
  /** One rule as a single line, for the publish diff. */
7392
7565
  function ruleLine(rule) {
7566
+ const ports = describePolicyPorts(rule);
7393
7567
  const parts = [
7394
- rule.host,
7568
+ ports ? `${rule.host} [${ports}]` : rule.host,
7395
7569
  describeList(rule.methods),
7396
7570
  describeList(rule.paths),
7397
7571
  `secrets=${describeSecrets(rule.allow)}`
@@ -7718,7 +7892,11 @@ function registerAgentCommands(program) {
7718
7892
  console.error("widening proposals are never applied: an agent must not earn permissions by retrying");
7719
7893
  }
7720
7894
  });
7721
- 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");
7722
7900
  const ctx = buildContext();
7723
7901
  const org = await resolveOrg(ctx, options.org);
7724
7902
  const agent = await resolveAgent(ctx, org.id, ref);
@@ -7728,6 +7906,10 @@ function registerAgentCommands(program) {
7728
7906
  if (!published) fail(`${agent.slug} has no published policy — every request is denied`);
7729
7907
  const verdict = evaluatePolicy(published.rules, {
7730
7908
  host: options.host,
7909
+ transport: {
7910
+ scheme,
7911
+ port
7912
+ },
7731
7913
  method: options.method,
7732
7914
  path: options.path,
7733
7915
  ...options.secret === void 0 ? {} : { secret: options.secret }
@@ -7738,7 +7920,7 @@ function registerAgentCommands(program) {
7738
7920
  verdict,
7739
7921
  reason
7740
7922
  }, () => {
7741
- const target = `${options.method.toUpperCase()} ${options.host}${options.path}`;
7923
+ const target = `${options.method.toUpperCase()} ${scheme}://${options.host}:${port}${options.path}`;
7742
7924
  console.log(`${verdict.decision === "allow" ? "allow" : "DENY"} ${target}${options.secret ? ` [${options.secret}]` : ""}`);
7743
7925
  console.log(` v${published.version}: ${reason}`);
7744
7926
  });
@@ -7818,7 +8000,7 @@ function registerPolicyCommands(policy) {
7818
8000
  const { signingKey, jwk } = await getPolicySigner(ctx);
7819
8001
  const issuedAt = Math.floor(Date.now() / 1e3);
7820
8002
  const bundle = await signAgentPolicy(signingKey, jwk, {
7821
- v: 1,
8003
+ v: policyBundleVersionFor(rules),
7822
8004
  org: org.id,
7823
8005
  agent: agent.id,
7824
8006
  agent_slug: agent.slug,
@@ -8231,8 +8413,7 @@ function printExplain(provenance, refs = {}) {
8231
8413
  }
8232
8414
  if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
8233
8415
  }
8234
- //#endregion
8235
- //#region src/env-diff.ts
8416
+ // src/env-diff.ts
8236
8417
  /**
8237
8418
  * The comparison itself, kept pure so it can be tested without a network or a
8238
8419
  * key. Rows come back sorted by name: a diff people read top-to-bottom should
@@ -8349,8 +8530,7 @@ function registerEnvDiff(env) {
8349
8530
  });
8350
8531
  });
8351
8532
  }
8352
- //#endregion
8353
- //#region src/group-sync-grants.ts
8533
+ // src/group-sync-grants.ts
8354
8534
  /** Wrap a newly composed group layer for connections already syncing the app env or its branches. */
8355
8535
  async function syncGrantsForGroupLink(ctx, orgId, envId, envSlug, groupId) {
8356
8536
  const [{ groups }, { bindings }, { branches }] = await Promise.all([
@@ -8375,8 +8555,7 @@ async function syncGrantsForGroupLink(ctx, orgId, envId, envSlug, groupId) {
8375
8555
  };
8376
8556
  }));
8377
8557
  }
8378
- //#endregion
8379
- //#region src/kms.ts
8558
+ // src/kms.ts
8380
8559
  /** Collect a repeatable option into a list. */
8381
8560
  function collect$9(value, acc = []) {
8382
8561
  acc.push(value);
@@ -8665,8 +8844,7 @@ function registerKmsCommands(program) {
8665
8844
  }
8666
8845
  });
8667
8846
  }
8668
- //#endregion
8669
- //#region src/recovery.ts
8847
+ // src/recovery.ts
8670
8848
  /** Collect a repeatable option into a list. */
8671
8849
  function collect$8(value, acc = []) {
8672
8850
  acc.push(value);
@@ -8865,8 +9043,7 @@ function registerRecoveryCommands(program) {
8865
9043
  console.error(`recovery request ${requestId} canceled`);
8866
9044
  });
8867
9045
  }
8868
- //#endregion
8869
- //#region src/apps.ts
9046
+ // src/apps.ts
8870
9047
  /**
8871
9048
  * Applications and their environments — the structure everything else hangs
8872
9049
  * off. Creating an environment is the one command here that does crypto: the
@@ -9013,6 +9190,7 @@ function registerAppCommands(program) {
9013
9190
  const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
9014
9191
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
9015
9192
  const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
9193
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(orgRef.id, { appId: appRow.id }, dek),
9016
9194
  name: options.name,
9017
9195
  slug: options.slug,
9018
9196
  wrappedDek,
@@ -9038,6 +9216,7 @@ function registerAppCommands(program) {
9038
9216
  });
9039
9217
  const syncGrants = await syncGrantsForGroupLink(ctx, target.orgId, target.envId, target.envSlug, group.id);
9040
9218
  await ctx.client.linkEnvGroup(target.orgId, target.envId, {
9219
+ appAccessGrants: await ctx.client.wrapGroupAppAccess(target.orgId, target.envId, group.id, (id) => getDek(ctx, target.orgId, id)),
9041
9220
  groupId: group.id,
9042
9221
  position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10),
9043
9222
  syncGrants
@@ -9066,8 +9245,7 @@ function registerAppCommands(program) {
9066
9245
  console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
9067
9246
  });
9068
9247
  }
9069
- //#endregion
9070
- //#region src/decryptor.ts
9248
+ // src/decryptor.ts
9071
9249
  /**
9072
9250
  * The standalone offline decryptor: one self-contained HTML file, written out by
9073
9251
  * `seekrit archive decryptor`, that opens a break-glass archive in any browser
@@ -9899,8 +10077,7 @@ const OFFLINE_DECRYPTOR_HTML = `<!doctype html>
9899
10077
  </body>
9900
10078
  </html>
9901
10079
  `;
9902
- //#endregion
9903
- //#region src/format.ts
10080
+ // src/format.ts
9904
10081
  function shellQuote(value) {
9905
10082
  return `'${value.replaceAll("'", `'\\''`)}'`;
9906
10083
  }
@@ -9912,8 +10089,7 @@ function formatSecrets(values, format) {
9912
10089
  case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
9913
10090
  }
9914
10091
  }
9915
- //#endregion
9916
- //#region src/archive.ts
10092
+ // src/archive.ts
9917
10093
  /**
9918
10094
  * Break-glass archives (docs/break-glass-export.md).
9919
10095
  *
@@ -10243,8 +10419,7 @@ function registerArchiveCommands(program) {
10243
10419
  console.error("open it in any browser — it declares a Content-Security-Policy of `default-src 'none'`, so it cannot reach the network.");
10244
10420
  });
10245
10421
  }
10246
- //#endregion
10247
- //#region src/audit.ts
10422
+ // src/audit.ts
10248
10423
  /** The API's per-page ceiling (`auditQuerySchema.limit`). */
10249
10424
  const MAX_PAGE = 200;
10250
10425
  function parseLimit(raw) {
@@ -10298,8 +10473,7 @@ function registerAuditCommands(program) {
10298
10473
  });
10299
10474
  });
10300
10475
  }
10301
- //#endregion
10302
- //#region src/aws.ts
10476
+ // src/aws.ts
10303
10477
  /**
10304
10478
  * `seekrit aws` — temporary AWS credentials via STS AssumeRole (Vault-style
10305
10479
  * dynamic secrets, the tier-2 sibling of `seekrit pg`).
@@ -10441,8 +10615,7 @@ function registerAwsCommands(program) {
10441
10615
  console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
10442
10616
  });
10443
10617
  }
10444
- //#endregion
10445
- //#region src/billing.ts
10618
+ // src/billing.ts
10446
10619
  /** `included: null` means unlimited; an unavailable meter is unknown, not zero. */
10447
10620
  function usageLine(usage) {
10448
10621
  if (!usage.available) return "unknown";
@@ -10517,8 +10690,7 @@ function registerBillingCommands(program) {
10517
10690
  console.error(`${ref.slug} moved to the Free plan`);
10518
10691
  });
10519
10692
  }
10520
- //#endregion
10521
- //#region src/branches.ts
10693
+ // src/branches.ts
10522
10694
  /**
10523
10695
  * Resolve `--from` to the environment being branched.
10524
10696
  *
@@ -10615,8 +10787,7 @@ function registerBranchCommands(program) {
10615
10787
  console.error(`deleted branch ${app.slug}#${target.slug}`);
10616
10788
  });
10617
10789
  }
10618
- //#endregion
10619
- //#region src/changes.ts
10790
+ // src/changes.ts
10620
10791
  /**
10621
10792
  * Change requests: the review queue in front of protected environments, and the
10622
10793
  * protection settings that create it.
@@ -11144,8 +11315,7 @@ async function resolveEnvironment(ctx, scope) {
11144
11315
  });
11145
11316
  return values;
11146
11317
  }
11147
- //#endregion
11148
- //#region src/doctor.ts
11318
+ // src/doctor.ts
11149
11319
  /** Every `SEEKRIT_*` variable the CLI reads, for the set-but-blank check. */
11150
11320
  const READ_VARS = [
11151
11321
  "SEEKRIT_TOKEN",
@@ -11565,8 +11735,7 @@ function registerDoctorCommand(program) {
11565
11735
  if (checks.some((c) => c.status === "fail")) process.exitCode = 1;
11566
11736
  });
11567
11737
  }
11568
- //#endregion
11569
- //#region src/gcp.ts
11738
+ // src/gcp.ts
11570
11739
  /**
11571
11740
  * `seekrit gcp` — temporary GCP credentials via IAM Credentials
11572
11741
  * `generateAccessToken` (Vault-style dynamic secrets, the tier-2 sibling of
@@ -11708,8 +11877,7 @@ function registerGcpCommands(program) {
11708
11877
  console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
11709
11878
  });
11710
11879
  }
11711
- //#endregion
11712
- //#region src/groups.ts
11880
+ // src/groups.ts
11713
11881
  /**
11714
11882
  * Shared groups: reusable secret bags composed into application environments
11715
11883
  * by matching slug (`seekrit env groups add`). A group holds one environment
@@ -11807,6 +11975,7 @@ function registerGroupCommands(program) {
11807
11975
  const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
11808
11976
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
11809
11977
  const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
11978
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(groupRef.orgId, { groupId: groupRef.id }, dek),
11810
11979
  name: options.name,
11811
11980
  slug: options.slug,
11812
11981
  wrappedDek,
@@ -11815,8 +11984,7 @@ function registerGroupCommands(program) {
11815
11984
  console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
11816
11985
  });
11817
11986
  }
11818
- //#endregion
11819
- //#region src/hermes.ts
11987
+ // src/hermes.ts
11820
11988
  /**
11821
11989
  * `seekrit hermes` — wire seekrit into [Hermes Agent](https://hermes-agent.nousresearch.com)
11822
11990
  * as a **secret source**.
@@ -12033,8 +12201,7 @@ function registerHermesCommands(program) {
12033
12201
  emit(options, { reference: ref }, () => console.log(ref));
12034
12202
  });
12035
12203
  }
12036
- //#endregion
12037
- //#region src/honey.ts
12204
+ // src/honey.ts
12038
12205
  /**
12039
12206
  * Honey tokens — decoy credentials that unlock nothing and alert when used.
12040
12207
  *
@@ -12079,8 +12246,7 @@ function registerHoneyTokenCommands(program) {
12079
12246
  console.error(`${honeyTokenId} deleted`);
12080
12247
  });
12081
12248
  }
12082
- //#endregion
12083
- //#region src/m2m.ts
12249
+ // src/m2m.ts
12084
12250
  /**
12085
12251
  * Resolve M2M client credentials from (in order) the process environment, a
12086
12252
  * `.env` overlay (for `seekrit run`), then saved config. Both halves must come
@@ -12270,6 +12436,7 @@ async function runInit(options) {
12270
12436
  const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
12271
12437
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
12272
12438
  ({environment: env} = await ctx.client.createEnv(orgRef.id, app.id, {
12439
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(orgRef.id, { appId: app.id }, dek),
12273
12440
  name: titleCase(envSlug),
12274
12441
  slug: envSlug,
12275
12442
  wrappedDek,
@@ -12316,8 +12483,7 @@ function registerInitCommand(program) {
12316
12483
  await runInit(options);
12317
12484
  });
12318
12485
  }
12319
- //#endregion
12320
- //#region src/logsink.ts
12486
+ // src/logsink.ts
12321
12487
  /** Collect repeated `--header Name: value` flags into a map. */
12322
12488
  function collectHeader(value, acc = {}) {
12323
12489
  const sep = value.indexOf(":");
@@ -12384,8 +12550,7 @@ function registerLogSinkCommands(program) {
12384
12550
  console.error("log sink removed");
12385
12551
  });
12386
12552
  }
12387
- //#endregion
12388
- //#region src/mongodb.ts
12553
+ // src/mongodb.ts
12389
12554
  /**
12390
12555
  * `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
12391
12556
  * secrets), the tier-2 sibling of `seekrit aws`.
@@ -12519,8 +12684,7 @@ function registerMongoCommands(program) {
12519
12684
  console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
12520
12685
  });
12521
12686
  }
12522
- //#endregion
12523
- //#region src/provisioner.ts
12687
+ // src/provisioner.ts
12524
12688
  /**
12525
12689
  * `seekrit provisioner` — helpers for the self-hosted **remote executor**
12526
12690
  * (`seekrit-provisioner`), the daemon that runs a target's provisioning SQL
@@ -12562,8 +12726,7 @@ function resolveLeaseAdminSecret(opts) {
12562
12726
  if (!adminUrl) fail(`provide the admin connection string via --admin-url or ${opts.adminUrlEnv}`);
12563
12727
  return adminUrl;
12564
12728
  }
12565
- //#endregion
12566
- //#region src/mysql.ts
12729
+ // src/mysql.ts
12567
12730
  /**
12568
12731
  * `seekrit mysql` — temporary MySQL/MariaDB credentials (Vault-style dynamic
12569
12732
  * secrets).
@@ -12703,8 +12866,7 @@ function collect$5(value, acc) {
12703
12866
  acc.push(value);
12704
12867
  return acc;
12705
12868
  }
12706
- //#endregion
12707
- //#region src/proxy-binary.ts
12869
+ // src/proxy-binary.ts
12708
12870
  /**
12709
12871
  * Fetch and run the `seekrit-proxy` binary without a Rust toolchain.
12710
12872
  *
@@ -12739,7 +12901,7 @@ function collect$5(value, acc) {
12739
12901
  * release-please-config.json), so the pin follows the crate without anyone
12740
12902
  * remembering to move it.
12741
12903
  */
12742
- const PROXY_VERSION = "0.13.0";
12904
+ const PROXY_VERSION = "0.14.0";
12743
12905
  const BIN = "seekrit-proxy";
12744
12906
  /**
12745
12907
  * Host → Rust target triple.
@@ -12775,7 +12937,7 @@ function versionPrefix(version) {
12775
12937
  return version.startsWith("v") ? version : `v${version}`;
12776
12938
  }
12777
12939
  function resolveVersion(explicit) {
12778
- return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
12940
+ return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.14.0";
12779
12941
  }
12780
12942
  function resolveBaseUrl(explicit) {
12781
12943
  return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
@@ -12885,8 +13047,7 @@ function signalNumber(signal) {
12885
13047
  SIGTERM: 15
12886
13048
  }[signal] ?? 0;
12887
13049
  }
12888
- //#endregion
12889
- //#region src/nomad.ts
13050
+ // src/nomad.ts
12890
13051
  /**
12891
13052
  * `seekrit nomad` — wire seekrit into [HashiCorp Nomad](https://developer.hashicorp.com/nomad)
12892
13053
  * as a **secret provider plugin**.
@@ -13270,8 +13431,7 @@ function registerNomadCommands(program) {
13270
13431
  function indent(text) {
13271
13432
  return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
13272
13433
  }
13273
- //#endregion
13274
- //#region src/orgs.ts
13434
+ // src/orgs.ts
13275
13435
  /**
13276
13436
  * Counts for `org show`. Each list is admin-gated to a different degree, so a
13277
13437
  * member who can't read one still gets the rest: a failed call reports as
@@ -13428,8 +13588,7 @@ function registerOrgCommands(program) {
13428
13588
  });
13429
13589
  });
13430
13590
  }
13431
- //#endregion
13432
- //#region src/proxy-presets.ts
13591
+ // src/proxy-presets.ts
13433
13592
  /** `Authorization: Bearer {{seekrit:NAME}}` — the shape most providers take. */
13434
13593
  function placeholder(secret) {
13435
13594
  return `{{seekrit:${secret}}}`;
@@ -13613,8 +13772,31 @@ function presetAllow(preset) {
13613
13772
  if (preset.allow) return preset.allow;
13614
13773
  return preset.secret ? [preset.secret] : [];
13615
13774
  }
13616
- //#endregion
13617
- //#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
+ */
13618
13800
  /**
13619
13801
  * A deliberately small TOML writer: basic strings and arrays of them, which is
13620
13802
  * every value in this config. Full TOML is not needed and a general emitter
@@ -13835,6 +14017,22 @@ function splitHostPort(addr) {
13835
14017
  if (idx === -1) return [addr, "8080"];
13836
14018
  return [addr.slice(0, idx), addr.slice(idx + 1)];
13837
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
+ }
13838
14036
  /** Build a file-policy plan from presets and/or ad-hoc `host=SECRET` rules. */
13839
14037
  function planFromPresets(presets, options) {
13840
14038
  const taken = /* @__PURE__ */ new Set();
@@ -13910,7 +14108,7 @@ function planFromPolicy(args, options) {
13910
14108
  const baseUrl = baseUrlFor(options.listen, prefix, preset?.baseUrlSuffix ?? "");
13911
14109
  routes.push({
13912
14110
  prefix,
13913
- upstream: `https://${rule.host}`,
14111
+ upstream: upstreamForRule(rule),
13914
14112
  host: rule.host,
13915
14113
  allow: [],
13916
14114
  methods: [],
@@ -14053,8 +14251,7 @@ function renderComposeSnippet(plan, options) {
14053
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.");
14054
14252
  return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
14055
14253
  }
14056
- //#endregion
14057
- //#region src/paperclip.ts
14254
+ // src/paperclip.ts
14058
14255
  /**
14059
14256
  * `seekrit paperclip` — wire seekrit into a [Paperclip](https://docs.paperclip.ing)
14060
14257
  * agent.
@@ -14242,8 +14439,7 @@ function registerPaperclipCommands(program) {
14242
14439
  });
14243
14440
  });
14244
14441
  }
14245
- //#endregion
14246
- //#region src/pg.ts
14442
+ // src/pg.ts
14247
14443
  /**
14248
14444
  * `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
14249
14445
  *
@@ -14395,8 +14591,7 @@ function collect$3(value, acc) {
14395
14591
  acc.push(value);
14396
14592
  return acc;
14397
14593
  }
14398
- //#endregion
14399
- //#region src/proxy.ts
14594
+ // src/proxy.ts
14400
14595
  /**
14401
14596
  * `seekrit proxy` — get `seekrit-proxy` running without a Rust toolchain or a
14402
14597
  * hand-written TOML file.
@@ -14725,10 +14920,10 @@ Next:
14725
14920
  });
14726
14921
  console.log(path);
14727
14922
  const { target } = detectTarget();
14728
- process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.13.0")} (${target})\n`);
14923
+ process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.14.0")} (${target})\n`);
14729
14924
  });
14730
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) => {
14731
- const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
14926
+ const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.14.0";
14732
14927
  const override = process.env.SEEKRIT_PROXY_BIN;
14733
14928
  const { target, exe } = detectTarget();
14734
14929
  const path = override ?? proxyBinaryPath(version, target, exe);
@@ -14787,13 +14982,12 @@ Next:
14787
14982
  process.stdout.write(renderComposeSnippet(plan, {
14788
14983
  service: options.service,
14789
14984
  workload: options.workload,
14790
- image: options.image ?? `seekritdev/proxy:0.13.0`,
14985
+ image: options.image ?? `seekritdev/proxy:0.14.0`,
14791
14986
  publish: Boolean(options.publish)
14792
14987
  }));
14793
14988
  });
14794
14989
  }
14795
- //#endregion
14796
- //#region src/redis.ts
14990
+ // src/redis.ts
14797
14991
  /**
14798
14992
  * `seekrit redis` — temporary Redis (6+) credentials (Vault-style dynamic
14799
14993
  * secrets).
@@ -14933,8 +15127,7 @@ function collect$2(value, acc) {
14933
15127
  acc.push(value);
14934
15128
  return acc;
14935
15129
  }
14936
- //#endregion
14937
- //#region src/rotation.ts
15130
+ // src/rotation.ts
14938
15131
  /**
14939
15132
  * `seekrit rotation` — managed, scheduled rotation of a stored secret's value.
14940
15133
  *
@@ -15101,8 +15294,7 @@ function registerRotationCommands(program) {
15101
15294
  console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
15102
15295
  });
15103
15296
  }
15104
- //#endregion
15105
- //#region src/run-command.ts
15297
+ // src/run-command.ts
15106
15298
  /** Keep signal handlers installed from provisioning through asynchronous cleanup. */
15107
15299
  async function runCommand(cmd, args, prepare, cleanup) {
15108
15300
  let child;
@@ -15272,8 +15464,7 @@ async function reapStragglers(pids, signal) {
15272
15464
  process.kill(pid, "SIGKILL");
15273
15465
  } catch {}
15274
15466
  }
15275
- //#endregion
15276
- //#region src/run-leases.ts
15467
+ // src/run-leases.ts
15277
15468
  /** Validate all flags before contacting the API or creating any database roles. */
15278
15469
  function parseRunLeaseOptions(bindings = [], ttl) {
15279
15470
  if (!bindings.length) {
@@ -15398,8 +15589,7 @@ var RunLeases = class {
15398
15589
  return ok;
15399
15590
  }
15400
15591
  };
15401
- //#endregion
15402
- //#region src/ssh.ts
15592
+ // src/ssh.ts
15403
15593
  /**
15404
15594
  * `seekrit ssh` — temporary SSH access via short-lived certificates (Vault-style
15405
15595
  * dynamic secrets, the SSH sibling of `seekrit pg`).
@@ -15541,8 +15731,7 @@ function collect$1(value, acc) {
15541
15731
  acc.push(value);
15542
15732
  return acc;
15543
15733
  }
15544
- //#endregion
15545
- //#region src/sync.ts
15734
+ // src/sync.ts
15546
15735
  /** Split a `--flag a,b` list into trimmed, non-empty members. */
15547
15736
  function list(raw, fallback) {
15548
15737
  return (raw ?? fallback).split(",").map((item) => item.trim()).filter(Boolean);
@@ -16288,8 +16477,7 @@ function registerSyncCommands(program) {
16288
16477
  ], "no sync runs yet"));
16289
16478
  });
16290
16479
  }
16291
- //#endregion
16292
- //#region src/web-login.ts
16480
+ // src/web-login.ts
16293
16481
  /**
16294
16482
  * `seekrit login` — sign in through the browser.
16295
16483
  *
@@ -16464,8 +16652,7 @@ async function runLogout() {
16464
16652
  });
16465
16653
  if (config.clientId) console.error("(machine client credentials are kept — remove them with `seekrit login`)");
16466
16654
  }
16467
- //#endregion
16468
- //#region src/index.ts
16655
+ // src/index.ts
16469
16656
  /** Collect repeated `--with group=env` flags into a map. */
16470
16657
  function collectKv(value, acc = {}) {
16471
16658
  const eq = value.indexOf("=");
@@ -16826,42 +17013,43 @@ token.command("create").description("create a service token (runtime, or --admin
16826
17013
  if (boundToEnv && !(options.app && options.env)) fail("pass both --app and --env to bind a token to an environment");
16827
17014
  const target = boundToEnv ? await resolveAppEnv(ctx, options) : { orgId: (await resolveOrg(ctx, options.org)).id };
16828
17015
  const created = await createServiceToken();
17016
+ const granted = boundToEnv && options.grant !== false;
17017
+ const initialGrants = granted && "envId" in target ? await ctx.client.prepareComposedKeyGrants(target.orgId, target.envId, {
17018
+ principalType: "service_token",
17019
+ principalId: created.tokenId,
17020
+ publicKeyJwk: created.publicKeyJwk
17021
+ }, (id) => getDek(ctx, target.orgId, id)) : void 0;
17022
+ const extraGrants = [];
17023
+ if (granted) for (const [groupSlug, envSlug] of Object.entries(options.allow ?? {})) {
17024
+ const groupRef = await resolveGroup(ctx, {
17025
+ org: options.org,
17026
+ group: groupSlug
17027
+ });
17028
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, groupRef.id);
17029
+ const slice = environments.find((e) => e.slug === envSlug);
17030
+ if (!slice) fail(`group "${groupSlug}" has no "${envSlug}" environment`);
17031
+ extraGrants.push({
17032
+ environmentId: slice.id,
17033
+ wrappedDek: await wrapDek(await getDek(ctx, target.orgId, slice.id), created.publicKeyJwk)
17034
+ });
17035
+ }
16829
17036
  await ctx.client.createToken(target.orgId, {
16830
17037
  name: options.name,
16831
17038
  tokenId: created.tokenId,
16832
17039
  tokenHash: created.tokenHash,
16833
17040
  publicKeyJwk: created.publicKeyJwk,
16834
17041
  role,
16835
- environmentId: "envId" in target ? target.envId : null
17042
+ environmentId: "envId" in target ? target.envId : null,
17043
+ initialGrants
16836
17044
  });
16837
- const grantEnv = async (envId) => {
16838
- const dek = await getDek(ctx, target.orgId, envId);
16839
- await ctx.client.grantEnvKey(target.orgId, envId, {
17045
+ for (const extra of extraGrants) try {
17046
+ await ctx.client.grantEnvKey(target.orgId, extra.environmentId, {
16840
17047
  principalType: "service_token",
16841
17048
  principalId: created.tokenId,
16842
- wrappedDek: await wrapDek(dek, created.publicKeyJwk)
17049
+ wrappedDek: extra.wrappedDek
16843
17050
  });
16844
- };
16845
- const granted = boundToEnv && options.grant !== false;
16846
- if (granted && "envId" in target) {
16847
- await grantEnv(target.envId);
16848
- const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
16849
- for (const g of groups) {
16850
- const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
16851
- const slice = environments.find((e) => e.slug === target.envSlug);
16852
- if (!slice) fail(`group "${g.slug}" has no "${target.envSlug}" environment — create it before granting`);
16853
- await grantEnv(slice.id);
16854
- }
16855
- for (const [groupSlug, envSlug] of Object.entries(options.allow ?? {})) {
16856
- const groupRef = await resolveGroup(ctx, {
16857
- org: options.org,
16858
- group: groupSlug
16859
- });
16860
- const { environments } = await ctx.client.listGroupEnvs(target.orgId, groupRef.id);
16861
- const slice = environments.find((e) => e.slug === envSlug);
16862
- if (!slice) fail(`group "${groupSlug}" has no "${envSlug}" environment`);
16863
- await grantEnv(slice.id);
16864
- }
17051
+ } catch (error) {
17052
+ console.error(`Token created with its default environment keys, but extra grant ${extra.environmentId} failed: ${error instanceof Error ? error.message : "unknown error"}`);
16865
17053
  }
16866
17054
  const scope = "appSlug" in target ? `${target.appSlug}/${target.envSlug}` : "org-scoped (admin)";
16867
17055
  console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
@@ -16915,7 +17103,7 @@ registerKmsCommands(program);
16915
17103
  registerRotationCommands(program);
16916
17104
  registerRecoveryCommands(program);
16917
17105
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
16918
- const { runMcpServer } = await import("./mcp-DZoBFmDF.js");
17106
+ const { runMcpServer } = await import("./mcp-Clul2n4l.js");
16919
17107
  await runMcpServer();
16920
17108
  });
16921
17109
  registerAuditCommands(program);
@@ -16929,5 +17117,4 @@ const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
16929
17117
  program.parseAsync(argv).catch((err) => {
16930
17118
  fail(describeError(err));
16931
17119
  });
16932
- //#endregion
16933
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 };