@seekrit/mcp 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1657 -44
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -8,6 +8,229 @@ import { homedir } from "node:os";
8
8
  import { dirname, join, parse } from "node:path";
9
9
  import { createInterface } from "node:readline";
10
10
  import { Writable } from "node:stream";
11
+ //#region ../../packages/core/src/agent-policy.ts
12
+ /** A bare hostname: no scheme, no port, no path, no wildcard. */
13
+ const policyHostSchema = z.string().trim().min(1).max(253).toLowerCase().refine((h) => !/[:/\s*]/.test(h), { message: "host must be a bare hostname (no scheme, port, path, or wildcard)" }).refine((h) => /^[a-z0-9.-]+$/.test(h), { message: "host contains invalid characters" });
14
+ const policyMethodSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3,10}$/, "method must be an HTTP method name");
15
+ /**
16
+ * A path pattern. Must be absolute, because it is matched against a request
17
+ * path — a relative pattern is a mistake that would silently match nothing.
18
+ */
19
+ 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" });
20
+ const policySecretNameSchema = z.string().trim().regex(/^[A-Za-z0-9_]+$/, "secret names are letters, digits, and underscores");
21
+ z.object({
22
+ host: policyHostSchema,
23
+ methods: z.array(policyMethodSchema).max(16).default([]),
24
+ paths: z.array(policyPathSchema).max(64).default([]),
25
+ allow: z.array(policySecretNameSchema).max(64).default([]),
26
+ label: z.string().trim().max(120).optional()
27
+ });
28
+ z.object({
29
+ /** `ap1.<body>.<signature>` — opaque to the server. */
30
+ bundle: z.string().min(16).max(256 * 1024) });
31
+ z.object({
32
+ name: z.string().trim().min(1).max(120),
33
+ slug: z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "slug is lowercase letters, digits, and dashes"),
34
+ /** The environment whose secrets this agent's policy may name. Optional. */
35
+ environmentId: z.string().trim().min(1).max(64).optional()
36
+ });
37
+ z.object({
38
+ name: z.string().trim().min(1).max(120).optional(),
39
+ enabled: z.boolean().optional(),
40
+ environmentId: z.string().trim().min(1).max(64).nullish()
41
+ });
42
+ z.object({
43
+ host: policyHostSchema,
44
+ method: policyMethodSchema,
45
+ path: z.string().trim().min(1).max(2048),
46
+ secret: policySecretNameSchema.optional()
47
+ });
48
+ /** One aggregated cell: a dimension tuple and how many times it happened. */
49
+ const activityEntrySchema = z.object({
50
+ host: policyHostSchema,
51
+ method: policyMethodSchema,
52
+ decision: z.enum([
53
+ "allow",
54
+ "no_rule",
55
+ "method_not_allowed",
56
+ "path_not_allowed",
57
+ "secret_not_allowed",
58
+ "unknown_secret",
59
+ "ratchet_withdrawn",
60
+ "policy_unavailable"
61
+ ]),
62
+ /**
63
+ * Which published rule decided, when one did. Null for refusals that never
64
+ * reached a rule (`no_rule`, `policy_unavailable`) — the distinction matters to
65
+ * a review, because "rule 3 refused this" and "nothing covered this" call for
66
+ * opposite changes.
67
+ */
68
+ ruleIndex: z.number().int().min(0).max(255).nullable(),
69
+ count: z.number().int().min(1).max(1e6),
70
+ /**
71
+ * Secret names actually injected, name → count. Only meaningful on `allow`.
72
+ * This is what lets a review say "rule 2 permits three secrets and the agent
73
+ * has only ever used one" — the most useful narrowing there is, and impossible
74
+ * to see from policy alone.
75
+ */
76
+ secrets: z.record(policySecretNameSchema, z.number().int().min(1)).optional()
77
+ });
78
+ z.object({
79
+ /** Start of the window these counts cover (ISO 8601). */
80
+ windowStart: z.string().trim().min(20).max(40),
81
+ /** Policy version in force while they were collected, for the ledger. */
82
+ policyVersion: z.number().int().min(0).optional(),
83
+ /**
84
+ * Capped so one report cannot be unbounded work. A proxy with more distinct
85
+ * cells than this in a window has a policy far broader than a review can help
86
+ * with, and truncating loudly beats accepting anything.
87
+ */
88
+ entries: z.array(activityEntrySchema).min(1).max(500)
89
+ });
90
+ /**
91
+ * Twelve hours, matching `[control] max_ttl` in a proxy config. A task is meant
92
+ * to bound one run; something that needs longer wants a policy change, not a
93
+ * longer ticket.
94
+ */
95
+ const TASK_MAX_TTL_SECONDS = 720 * 60;
96
+ /** An EC P-256 public JWK, for a sender-constraint proof key. */
97
+ const taskProofJwkSchema = z.object({
98
+ kty: z.literal("EC"),
99
+ crv: z.literal("P-256"),
100
+ x: z.string().min(1).max(128),
101
+ y: z.string().min(1).max(128)
102
+ });
103
+ z.object({
104
+ /**
105
+ * The public `skd_…` segment of the minted token. Sent because the API never
106
+ * sees the token at dispatch and still needs a readable handle for the audit
107
+ * row and for a revoke to name — the id half of a credential, without the
108
+ * secret half.
109
+ */
110
+ taskRef: z.string().trim().regex(/^skd_[0-9A-Za-z]+$/, "taskRef must be the skd_… segment of the minted token"),
111
+ /**
112
+ * SHA-256 (base64url) of the token the dispatcher minted. The token itself
113
+ * never reaches this API on the dispatch path — only on introspection, where
114
+ * it is hashed and discarded.
115
+ */
116
+ tokenHash: z.string().trim().min(16).max(128),
117
+ /**
118
+ * Secret names this run may use. Omit for "whatever the agent's policy
119
+ * allows" — mirroring `Session.scopes: Option<BTreeSet<String>>` in the proxy,
120
+ * so absent means unnarrowed in both places.
121
+ */
122
+ scopes: z.array(policySecretNameSchema).max(64).optional(),
123
+ ttlSeconds: z.number().int().min(60).max(TASK_MAX_TTL_SECONDS).optional(),
124
+ /**
125
+ * What this run is for, for the audit row and the operator's task list. Free
126
+ * text, and **not** a security input: never put a secret value in it.
127
+ */
128
+ label: z.string().trim().max(200).optional(),
129
+ /**
130
+ * Public half of a proof key the presenter holds, recorded as an RFC 7638
131
+ * thumbprint. See `AgentTaskSession.proofThumbprint` for what this does and —
132
+ * importantly — does not yet do.
133
+ */
134
+ proofJwk: taskProofJwkSchema.optional()
135
+ });
136
+ z.object({
137
+ /** The presented token. In the body, never a URL — it is a credential. */
138
+ token: z.string().trim().min(8).max(512) });
139
+ //#endregion
140
+ //#region ../../packages/core/src/archive.ts
141
+ /**
142
+ * The **break-glass archive** format: one signed JSON file holding everything
143
+ * seekrit stores for an org, in the form seekrit stores it — ciphertext stays
144
+ * ciphertext. Its whole purpose is to be openable on a machine that has never
145
+ * heard of seekrit, so the format is plain JSON, self-describing, and versioned
146
+ * by name (`seekrit-archive/v1`); a breaking change ships a new format string
147
+ * rather than mutating this one, exactly like the `sc1.`/`wd1.` blob prefixes.
148
+ *
149
+ * Three parts:
150
+ * - `manifest` — what this archive is, and a SHA-256 digest per section.
151
+ * - `signature` — Ed25519 over the canonical manifest, or null when the
152
+ * producing deployment has no signing key configured.
153
+ * - `data` — the sections themselves.
154
+ *
155
+ * Integrity fields (`digest`, `signature.value`, `publicKey`, `keyId`) are
156
+ * lowercase hex. Every blob *inside* `data` keeps its native base64url form, so
157
+ * the one encoding rule to remember is "the archive's own bookkeeping is hex,
158
+ * seekrit's blobs are unchanged".
159
+ *
160
+ * See docs/break-glass-export.md for what is deliberately excluded and why.
161
+ */
162
+ const ARCHIVE_FORMAT = "seekrit-archive/v1";
163
+ const sectionHeaderSchema = z.object({
164
+ name: z.enum([
165
+ "organization",
166
+ "users",
167
+ "memberships",
168
+ "invites",
169
+ "applications",
170
+ "groups",
171
+ "environments",
172
+ "environmentGroups",
173
+ "environmentKeys",
174
+ "secrets",
175
+ "secretVersions",
176
+ "serviceTokens",
177
+ "m2mClients",
178
+ "kmsKeys",
179
+ "kmsKeyVersions",
180
+ "kmsKeyGrants",
181
+ "recoveryConfig",
182
+ "recoveryShares",
183
+ "rotations",
184
+ "syncConnections",
185
+ "syncBindings",
186
+ "leaseTargets",
187
+ "agentIdentities",
188
+ "agentPolicies",
189
+ "auditLog",
190
+ "keyMaterial"
191
+ ]),
192
+ count: z.number().int().min(0),
193
+ truncated: z.boolean(),
194
+ digest: z.string().regex(/^sha256:[0-9a-f]{64}$/)
195
+ });
196
+ const manifestSchema = z.object({
197
+ archiveId: z.string().min(1),
198
+ createdAt: z.string().min(1),
199
+ org: z.object({
200
+ id: z.string(),
201
+ slug: z.string(),
202
+ name: z.string()
203
+ }),
204
+ producer: z.object({
205
+ service: z.string(),
206
+ environment: z.string(),
207
+ formatVersion: z.string()
208
+ }),
209
+ requestedBy: z.object({
210
+ actorType: z.string(),
211
+ actorId: z.string(),
212
+ label: z.string().nullable()
213
+ }),
214
+ options: z.object({
215
+ includeVersions: z.boolean(),
216
+ includeAudit: z.boolean(),
217
+ auditLimit: z.number().int().min(0)
218
+ }),
219
+ sections: z.array(sectionHeaderSchema),
220
+ digest: z.string().regex(/^sha256:[0-9a-f]{64}$/)
221
+ });
222
+ const signatureSchema = z.object({
223
+ algorithm: z.literal("ed25519"),
224
+ publicKey: z.string().regex(/^[0-9a-f]{64}$/),
225
+ keyId: z.string().regex(/^[0-9a-f]{16}$/),
226
+ value: z.string().regex(/^[0-9a-f]{128}$/)
227
+ });
228
+ z.object({
229
+ format: z.literal(ARCHIVE_FORMAT),
230
+ manifest: manifestSchema,
231
+ signature: signatureSchema.nullable(),
232
+ data: z.record(z.string(), z.unknown())
233
+ });
11
234
  /** All catalog keys as a runtime array (for iteration / zod enums). */
12
235
  const ENTITLEMENT_KEYS = Object.keys({
13
236
  "feature.kms": {
@@ -16,12 +239,24 @@ const ENTITLEMENT_KEYS = Object.keys({
16
239
  description: "Client-side managed keys for application-layer encryption and signing.",
17
240
  default: true
18
241
  },
242
+ "feature.honey_tokens": {
243
+ kind: "feature",
244
+ label: "Honey tokens",
245
+ description: "Decoy credentials that alert the moment anyone tries to use them.",
246
+ default: true
247
+ },
19
248
  "feature.leases": {
20
249
  kind: "feature",
21
250
  label: "Temporary access",
22
251
  description: "Vault-style short-lived database and cloud credentials.",
23
252
  default: true
24
253
  },
254
+ "feature.rotation": {
255
+ kind: "feature",
256
+ label: "Secret rotation",
257
+ description: "Managed, scheduled rotation of stored credentials.",
258
+ default: true
259
+ },
25
260
  "feature.log_sink": {
26
261
  kind: "feature",
27
262
  label: "Audit log export (SIEM)",
@@ -100,6 +335,12 @@ const ENTITLEMENT_KEYS = Object.keys({
100
335
  description: "Maximum registered third-party sync destinations.",
101
336
  default: null
102
337
  },
338
+ "rotation.policies.max": {
339
+ kind: "limit",
340
+ label: "Rotation policies",
341
+ description: "Maximum secrets with managed rotation configured.",
342
+ default: null
343
+ },
103
344
  members: {
104
345
  kind: "metered",
105
346
  label: "Members",
@@ -122,7 +363,7 @@ const PLAN_FAMILIES = {
122
363
  id: "free",
123
364
  name: "Free",
124
365
  description: "Get started with the essentials.",
125
- current: 1,
366
+ current: 3,
126
367
  hidden: false
127
368
  },
128
369
  team: {
@@ -512,7 +753,7 @@ const redisSha256VerifierSchema = z.string().regex(/^[0-9a-f]{64}$/, "must be a
512
753
  */
513
754
  const awsRoleArnSchema = z.string().regex(/^arn:aws(?:-us-gov|-cn)?:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/, "must be an IAM role ARN (arn:aws:iam::<account>:role/<name>)");
514
755
  /** An AWS region id, e.g. `us-east-1`, `eu-west-2`, `us-gov-west-1`. */
515
- const awsRegionSchema = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be an AWS region id (e.g. us-east-1)");
756
+ const awsRegionSchema$1 = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be an AWS region id (e.g. us-east-1)");
516
757
  /**
517
758
  * An STS external id — the shared string a role's trust policy can require so a
518
759
  * confused-deputy can't assume it. AWS allows a broad charset; we keep to the
@@ -592,7 +833,7 @@ const connectionSchema = z.object({
592
833
  database: z.string().min(1)
593
834
  });
594
835
  /** A `{{name}}`/`{{verifier}}`/`{{valid_until}}` templated SQL statement. */
595
- const statementSchema = z.string().min(1).max(4e3);
836
+ const statementSchema$1 = z.string().min(1).max(4e3);
596
837
  /** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
597
838
  const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
598
839
  const postgresTargetConfigSchema = z.object({
@@ -602,20 +843,20 @@ const postgresTargetConfigSchema = z.object({
602
843
  schema: identifierSchema.optional(),
603
844
  connection: connectionSchema,
604
845
  provisionerUrl: z.url().optional(),
605
- createStatements: z.array(statementSchema).max(16).optional(),
606
- revokeStatements: z.array(statementSchema).max(16).optional()
846
+ createStatements: z.array(statementSchema$1).max(16).optional(),
847
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
607
848
  });
608
849
  /** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
609
- const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
850
+ const mysqlHostSchema$1 = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
610
851
  const mysqlTargetConfigSchema = z.object({
611
852
  provider: z.literal("mysql"),
612
853
  executor: executorModeSchema,
613
854
  accessLevel: mysqlAccessLevelSchema.optional(),
614
855
  connection: connectionSchema,
615
- userHost: mysqlHostSchema.optional(),
856
+ userHost: mysqlHostSchema$1.optional(),
616
857
  provisionerUrl: z.url().optional(),
617
- createStatements: z.array(statementSchema).max(16).optional(),
618
- revokeStatements: z.array(statementSchema).max(16).optional()
858
+ createStatements: z.array(statementSchema$1).max(16).optional(),
859
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
619
860
  });
620
861
  const redisConnectionSchema = z.object({
621
862
  host: z.string().min(1),
@@ -629,8 +870,8 @@ const redisTargetConfigSchema = z.object({
629
870
  accessLevel: redisAccessLevelSchema.optional(),
630
871
  connection: redisConnectionSchema,
631
872
  provisionerUrl: z.url().optional(),
632
- createStatements: z.array(statementSchema).max(16).optional(),
633
- revokeStatements: z.array(statementSchema).max(16).optional()
873
+ createStatements: z.array(statementSchema$1).max(16).optional(),
874
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
634
875
  });
635
876
  const sshTargetConfigSchema = z.object({
636
877
  provider: z.literal("ssh"),
@@ -649,7 +890,7 @@ const awsTargetConfigSchema = z.object({
649
890
  provider: z.literal("aws"),
650
891
  executor: z.literal("in_do"),
651
892
  roleArn: awsRoleArnSchema,
652
- region: awsRegionSchema,
893
+ region: awsRegionSchema$1,
653
894
  externalId: awsExternalIdSchema.optional(),
654
895
  sessionPolicy: z.string().min(1).max(4e3).optional(),
655
896
  maxTtlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS).optional()
@@ -813,7 +1054,9 @@ const NOTIFICATION_TYPES = [
813
1054
  "org_welcome",
814
1055
  "token_expiring",
815
1056
  "lease_expired",
816
- "sync_failed"
1057
+ "sync_failed",
1058
+ "honey_token_tripped",
1059
+ "secret_rotation_failed"
817
1060
  ];
818
1061
  //#endregion
819
1062
  //#region ../../packages/core/src/schemas.ts
@@ -834,6 +1077,13 @@ const inviteRoleSchema = z.enum(["admin", "member"]);
834
1077
  const principalTypeSchema = z.enum(["user", "service_token"]);
835
1078
  /** Org-level capability a service token can hold (never `owner`). */
836
1079
  const serviceTokenRoleSchema = z.enum(["admin", "member"]);
1080
+ z.object({
1081
+ /** Include the full append-only ciphertext history of every secret. */
1082
+ includeVersions: z.boolean().optional(),
1083
+ includeAudit: z.boolean().optional(),
1084
+ /** Newest audit rows to keep. Exceeding the server cap is a 400, not a silent trim. */
1085
+ auditLimit: z.number().int().min(0).optional()
1086
+ });
837
1087
  z.object({
838
1088
  name: nameSchema,
839
1089
  slug: slugSchema
@@ -845,6 +1095,8 @@ z.object({
845
1095
  z.object({ name: nameSchema });
846
1096
  z.object({ name: nameSchema });
847
1097
  z.object({ name: nameSchema });
1098
+ z.object({ name: nameSchema });
1099
+ z.object({ name: nameSchema });
848
1100
  z.object({ required: z.boolean() });
849
1101
  z.object({
850
1102
  email: emailSchema,
@@ -920,6 +1172,14 @@ z.object({
920
1172
  environmentId: z.string().min(1).nullish(),
921
1173
  expiresAt: z.iso.datetime().nullish()
922
1174
  });
1175
+ z.object({
1176
+ name: nameSchema,
1177
+ tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
1178
+ /** SHA-256 hash (base64url) of the full token string. */
1179
+ tokenHash: z.string().min(1),
1180
+ /** Where the decoy was planted, as a reminder for whoever reads the alert. */
1181
+ placement: z.string().max(200).nullish()
1182
+ });
923
1183
  const kmsKeyPurposeSchema = z.enum(["encrypt", "sign"]);
924
1184
  const kmsKeySpecSchema = z.enum(["aes-256-gcm", "ecdsa-p256"]);
925
1185
  /** A wrapped key grant supplied by the client (server never sees plaintext material). */
@@ -1032,6 +1292,25 @@ z.object({
1032
1292
  note: z.string().max(500).nullish(),
1033
1293
  expiresAt: z.iso.datetime().nullish()
1034
1294
  });
1295
+ z.object({
1296
+ code: z.string().min(4).max(40),
1297
+ family: planFamilySchema,
1298
+ version: z.number().int().positive().optional(),
1299
+ durationDays: z.number().int().positive().max(3650).nullish(),
1300
+ maxRedemptions: z.number().int().positive().nullish(),
1301
+ startsAt: z.iso.datetime().nullish(),
1302
+ endsAt: z.iso.datetime().nullish(),
1303
+ note: z.string().max(500).nullish()
1304
+ });
1305
+ z.object({
1306
+ maxRedemptions: z.number().int().positive().nullish(),
1307
+ startsAt: z.iso.datetime().nullish(),
1308
+ endsAt: z.iso.datetime().nullish(),
1309
+ note: z.string().max(500).nullish(),
1310
+ /** Kill switch. Disabling stops new redemptions; live grants are untouched. */
1311
+ disabled: z.boolean().optional()
1312
+ });
1313
+ z.object({ code: z.string().min(1).max(40) });
1035
1314
  z.object({ family: planFamilySchema });
1036
1315
  z.object({
1037
1316
  sessionId: z.string().regex(/^skc_[0-9A-Za-z]+$/),
@@ -1049,7 +1328,118 @@ z.object({
1049
1328
  action: z.string().optional(),
1050
1329
  resourceType: z.string().optional()
1051
1330
  });
1052
- z.enum(["vercel"]);
1331
+ z.enum([
1332
+ "generated",
1333
+ "postgres",
1334
+ "mysql",
1335
+ "redis"
1336
+ ]);
1337
+ z.enum([
1338
+ "active",
1339
+ "paused",
1340
+ "failed"
1341
+ ]);
1342
+ /** Statuses an admin may set directly (`failed` is only reached by the sweep). */
1343
+ const settableRotationStatusSchema = z.enum(["active", "paused"]);
1344
+ const rotationAlphabetSchema = z.enum([
1345
+ "alphanumeric",
1346
+ "hex",
1347
+ "base64url",
1348
+ "printable"
1349
+ ]);
1350
+ const rotationIntervalSchema = z.number().int().min(300).max(3600 * 24 * 365);
1351
+ /**
1352
+ * A database user name we are willing to *re-key*. Deliberately more permissive
1353
+ * than the lease providers' name schemas — those name accounts seekrit creates,
1354
+ * whereas this names an account the customer's DBA created years ago, which may
1355
+ * be mixed-case or contain dots or dashes.
1356
+ *
1357
+ * It stays injection-safe for every place it is interpolated: a double-quoted
1358
+ * Postgres identifier, a single-quoted MySQL literal, and a bare Redis command
1359
+ * token. The charset excludes both quote characters, backslash, whitespace, and
1360
+ * `;`, so there is no way out of the surrounding quoting, and no whitespace to
1361
+ * split one Redis argument into two.
1362
+ */
1363
+ const rotationUsernameSchema = z.string().regex(/^[A-Za-z0-9_$.-]{1,63}$/, "must be 1–63 chars of letters, digits, underscore, dollar, dot or dash");
1364
+ /** A `{{name}}`/`{{host}}`/`{{verifier}}` templated statement or command line. */
1365
+ const statementSchema = z.string().min(1).max(4e3);
1366
+ const passwordLengthSchema = z.number().int().min(16).max(256);
1367
+ /** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
1368
+ const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
1369
+ const generatedRotationConfigSchema = z.object({
1370
+ kind: z.literal("generated"),
1371
+ length: passwordLengthSchema.optional(),
1372
+ alphabet: rotationAlphabetSchema.optional()
1373
+ });
1374
+ const postgresRotationConfigSchema = z.object({
1375
+ kind: z.literal("postgres"),
1376
+ username: rotationUsernameSchema,
1377
+ passwordLength: passwordLengthSchema.optional(),
1378
+ statements: z.array(statementSchema).max(16).optional()
1379
+ });
1380
+ const mysqlRotationConfigSchema = z.object({
1381
+ kind: z.literal("mysql"),
1382
+ username: rotationUsernameSchema,
1383
+ userHost: mysqlHostSchema.optional(),
1384
+ passwordLength: passwordLengthSchema.optional(),
1385
+ statements: z.array(statementSchema).max(16).optional()
1386
+ });
1387
+ const redisRotationConfigSchema = z.object({
1388
+ kind: z.literal("redis"),
1389
+ username: rotationUsernameSchema,
1390
+ passwordLength: passwordLengthSchema.optional(),
1391
+ statements: z.array(statementSchema).max(16).optional()
1392
+ });
1393
+ const rotationConfigSchema = z.discriminatedUnion("kind", [
1394
+ generatedRotationConfigSchema,
1395
+ postgresRotationConfigSchema,
1396
+ mysqlRotationConfigSchema,
1397
+ redisRotationConfigSchema
1398
+ ]);
1399
+ z.object({
1400
+ environmentId: z.string().min(1),
1401
+ /** The secret whose value rotates. It must already exist. */
1402
+ secretName: secretNameSchema,
1403
+ config: rotationConfigSchema,
1404
+ intervalSeconds: rotationIntervalSchema,
1405
+ /**
1406
+ * The registered lease target supplying the connection, executor mode, and
1407
+ * wrapped admin credential. Required for every kind but `generated`.
1408
+ */
1409
+ targetId: z.string().min(1).optional(),
1410
+ /** Environment DEK wrapped to the rotator public key (`wd1.` blob). */
1411
+ wrappedDek: z.string().min(1).optional(),
1412
+ /** Rotate once immediately instead of waiting for the first interval. */
1413
+ rotateNow: z.boolean().optional()
1414
+ });
1415
+ z.object({
1416
+ intervalSeconds: rotationIntervalSchema.optional(),
1417
+ config: rotationConfigSchema.optional(),
1418
+ /**
1419
+ * `paused` stops the sweep; `active` resumes it and clears the failure
1420
+ * streak, which is also how a `failed` policy is recovered.
1421
+ */
1422
+ status: settableRotationStatusSchema.optional()
1423
+ }).refine((v) => v.intervalSeconds !== void 0 || v.config !== void 0 || v.status !== void 0, "provide at least one of intervalSeconds, config, or status");
1424
+ z.enum([
1425
+ "vercel",
1426
+ "cloudflare-workers",
1427
+ "cloudflare-pages",
1428
+ "cloudflare-secrets-store",
1429
+ "railway",
1430
+ "aws-secrets-manager",
1431
+ "aws-parameter-store",
1432
+ "render",
1433
+ "fly",
1434
+ "northflank",
1435
+ "digitalocean",
1436
+ "heroku",
1437
+ "netlify",
1438
+ "bunnyshell",
1439
+ "github-actions",
1440
+ "gcp-secret-manager",
1441
+ "langgraph-platform"
1442
+ ]);
1053
1443
  /**
1054
1444
  * Vercel account scope. The API token itself is never here — it is wrapped to
1055
1445
  * the connection's public key and stored as ciphertext.
@@ -1063,7 +1453,325 @@ const vercelConnectionConfigSchema = z.object({
1063
1453
  /** Vercel Team id (`team_…`). Omit for a personal account. */
1064
1454
  teamId: z.string().trim().min(1).max(128).optional()
1065
1455
  });
1066
- const syncConnectionConfigSchema = z.discriminatedUnion("provider", [vercelConnectionConfigSchema]);
1456
+ /**
1457
+ * A Cloudflare account id — 32 lowercase hex characters, found in the sidebar
1458
+ * of any account's dashboard. Every Cloudflare endpoint seekrit calls is
1459
+ * account-scoped, so this is the account half of "which account, which thing".
1460
+ *
1461
+ * Validated by shape because the alternative is a bare 400 from Cloudflare
1462
+ * hours later inside an alarm, with nobody watching. It does not catch pasting
1463
+ * a *zone* id, which has the same shape — only the API can tell those apart.
1464
+ */
1465
+ const cloudflareAccountIdSchema = z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Cloudflare account ID (lowercase hex)");
1466
+ /**
1467
+ * Cloudflare account scope, shared by all three Cloudflare providers. The API
1468
+ * token is never here — it is wrapped to the connection's public key and
1469
+ * stored as ciphertext, exactly as Vercel's is.
1470
+ *
1471
+ * The three providers are deliberately separate kinds rather than one
1472
+ * `cloudflare` with a mode field: they target different APIs, take different
1473
+ * destinations, and fail in different ways. Splitting them keeps the
1474
+ * exhaustiveness guard in `connectorFor` meaningful.
1475
+ */
1476
+ const cloudflareWorkersConnectionConfigSchema = z.object({
1477
+ provider: z.literal("cloudflare-workers"),
1478
+ accountId: cloudflareAccountIdSchema
1479
+ });
1480
+ const cloudflarePagesConnectionConfigSchema = z.object({
1481
+ provider: z.literal("cloudflare-pages"),
1482
+ accountId: cloudflareAccountIdSchema
1483
+ });
1484
+ const cloudflareSecretsStoreConnectionConfigSchema = z.object({
1485
+ provider: z.literal("cloudflare-secrets-store"),
1486
+ accountId: cloudflareAccountIdSchema
1487
+ });
1488
+ /**
1489
+ * A Railway id — every project, environment, and service is a UUID. Validated
1490
+ * by shape for the same reason Cloudflare's account id is: the alternative is a
1491
+ * bare GraphQL "Problem processing request" hours later inside an alarm, with
1492
+ * nobody watching.
1493
+ */
1494
+ const railwayIdSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a Railway UUID");
1495
+ /**
1496
+ * Railway account scope. The token is never here — it is wrapped to the
1497
+ * connection's public key and stored as ciphertext, exactly as Vercel's is.
1498
+ *
1499
+ * There is no workspace/team id to carry: Railway ids are globally unique and
1500
+ * a destination names its project outright, so the token plus the destination
1501
+ * is the whole address.
1502
+ */
1503
+ const railwayConnectionConfigSchema = z.object({
1504
+ provider: z.literal("railway"),
1505
+ tokenKind: z.enum(["account", "project"]).default("account")
1506
+ });
1507
+ /**
1508
+ * An AWS region id (`us-east-1`, `eu-central-1`, `us-gov-west-1`).
1509
+ *
1510
+ * Validated by shape rather than against a list, because AWS adds regions
1511
+ * faster than we ship. The endpoint host is built from this string, so a typo
1512
+ * would otherwise surface as a DNS failure inside an alarm with nobody
1513
+ * watching — which is a much worse place to learn about it than this form.
1514
+ */
1515
+ const awsRegionSchema = z.string().trim().regex(/^[a-z]{2}(-[a-z]+)+-\d$/, "must be an AWS region ID, e.g. us-east-1");
1516
+ /**
1517
+ * The IAM access key id seekrit signs with.
1518
+ *
1519
+ * This lives in `config` — the *non-secret* half — on purpose: an access key id
1520
+ * is an identifier, not a credential. It appears in CloudTrail, in the IAM
1521
+ * console, and in the `Authorization` header of every signed request; only the
1522
+ * **secret access key** is secret, and that is what gets wrapped to the
1523
+ * connection's public key. Keeping the id here also lets the dashboard say
1524
+ * which key a connection is using, which is the first thing you want to know
1525
+ * when a connection starts failing after a key rotation.
1526
+ *
1527
+ * Long-lived IAM user keys only. `ASIA…` session credentials from STS expire
1528
+ * within hours, and a sync connection has to keep working unattended.
1529
+ */
1530
+ const awsAccessKeyIdSchema = z.string().trim().regex(/^[A-Z0-9]{16,128}$/, "must be an AWS access key ID, e.g. AKIAIOSFODNN7EXAMPLE");
1531
+ /**
1532
+ * AWS account scope, shared by both AWS providers: which region to call and
1533
+ * which key to sign with. There is no account id — every endpoint seekrit calls
1534
+ * is reached through the regional host and authorizes off the signature, so the
1535
+ * account is whichever one the key belongs to.
1536
+ *
1537
+ * Two providers rather than one `aws` with a mode field, for the same reason
1538
+ * the three Cloudflare kinds are separate: different APIs, different
1539
+ * destinations, different IAM actions.
1540
+ */
1541
+ const awsSecretsManagerConnectionConfigSchema = z.object({
1542
+ provider: z.literal("aws-secrets-manager"),
1543
+ region: awsRegionSchema,
1544
+ accessKeyId: awsAccessKeyIdSchema
1545
+ });
1546
+ const awsParameterStoreConnectionConfigSchema = z.object({
1547
+ provider: z.literal("aws-parameter-store"),
1548
+ region: awsRegionSchema,
1549
+ accessKeyId: awsAccessKeyIdSchema
1550
+ });
1551
+ /**
1552
+ * Render account scope — deliberately empty.
1553
+ *
1554
+ * Like Railway's, and unlike Vercel (which 403s team-owned resources without
1555
+ * `teamId`) or Cloudflare (whose every endpoint is account-scoped): a Render
1556
+ * API key is issued to a user, and every endpoint seekrit calls addresses its
1557
+ * resource by id — `srv-…`, `crn-…`, `evg-…`. There is nothing to scope, so
1558
+ * nothing is stored. The connection's `name` is what tells an operator which Render
1559
+ * workspace it belongs to.
1560
+ */
1561
+ const renderConnectionConfigSchema = z.object({ provider: z.literal("render") });
1562
+ /**
1563
+ * Fly.io account scope — empty, as Render's is.
1564
+ *
1565
+ * Neither half of "which account, which thing" needs stating: Fly app names are
1566
+ * globally unique, so the destination names its app and that is the whole
1567
+ * address. Nor is there a token kind to declare the way Railway's `tokenKind`
1568
+ * is — Fly's two token shapes do take different auth schemes, but
1569
+ * `flyAuthorization` in the connector reads which one from the token itself.
1570
+ */
1571
+ const flyConnectionConfigSchema = z.object({ provider: z.literal("fly") });
1572
+ /**
1573
+ * A Northflank id — projects and secret groups are both slugs derived from the
1574
+ * name they were created with (`default-project`, `example-secret-group`), and
1575
+ * both appear in the resource's URL. Validated against Northflank's own pattern
1576
+ * so the common slip — pasting the *display name*, spaces and all — fails here
1577
+ * rather than as a bare 404 inside an alarm with nobody watching.
1578
+ */
1579
+ const northflankIdSchema = z.string().trim().min(3).max(100).regex(/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/, "must be a Northflank ID — the slug in the resource's URL, not its display name");
1580
+ /**
1581
+ * Northflank account scope — deliberately empty.
1582
+ *
1583
+ * A Northflank API token is issued by exactly one team (or org-owned team) and
1584
+ * carries that scope itself; `GET /v1/auth` reports which. There is no team id
1585
+ * to disambiguate the way Vercel needs one, and no account id the way
1586
+ * Cloudflare does: the token plus the destination's project is the whole
1587
+ * address. The kind exists so the discriminated union stays uniform.
1588
+ */
1589
+ const northflankConnectionConfigSchema = z.object({ provider: z.literal("northflank") });
1590
+ /**
1591
+ * DigitalOcean account scope — deliberately empty, as Render's and Fly's are.
1592
+ *
1593
+ * A DigitalOcean personal access token belongs to one account (or one team, if
1594
+ * it was issued inside one) and carries that scope itself, and every endpoint
1595
+ * this connector calls addresses its app by id. There is no team id to
1596
+ * disambiguate the way Vercel needs one: the token plus the destination's app
1597
+ * id is the whole address.
1598
+ */
1599
+ const digitalOceanConnectionConfigSchema = z.object({ provider: z.literal("digitalocean") });
1600
+ /**
1601
+ * Heroku account scope — empty, as Fly's and Render's are.
1602
+ *
1603
+ * A Heroku API token carries its user's access to every app and team they can
1604
+ * reach, and app names are globally unique, so the destination's app is the
1605
+ * whole address. There is no team id to state: unlike Vercel, where a personal
1606
+ * token 403s a team-owned project without `teamId`, Heroku resolves
1607
+ * `/apps/{app_id_or_name}` against everything the token can see, team-owned or
1608
+ * not.
1609
+ */
1610
+ const herokuConnectionConfigSchema = z.object({ provider: z.literal("heroku") });
1611
+ /**
1612
+ * Netlify team scope — the one thing a Netlify token cannot tell us itself.
1613
+ *
1614
+ * Every environment variable endpoint is account-scoped
1615
+ * (`/accounts/{account_id}/env`), and a personal access token belongs to a
1616
+ * *user*, who may sit in several teams. So unlike Fly's or Heroku's, this
1617
+ * config is not empty: the token says who you are, and this says which team's
1618
+ * variables to write.
1619
+ *
1620
+ * Netlify treats the team's id and its slug as interchangeable wherever
1621
+ * `{account_id}` appears, so both are accepted. The slug is the one an operator
1622
+ * can find without an API call — it is in the dashboard URL
1623
+ * (`app.netlify.com/teams/<slug>`) and under Team settings → General.
1624
+ */
1625
+ const netlifyConnectionConfigSchema = z.object({
1626
+ provider: z.literal("netlify"),
1627
+ /** Netlify team slug (`acme`) or account id — `{account_id}` accepts either. */
1628
+ accountId: z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a Netlify team slug or account ID — no slashes or spaces")
1629
+ });
1630
+ /**
1631
+ * Bunnyshell account scope — empty, as Fly's, Heroku's, and Northflank's are.
1632
+ *
1633
+ * A Bunnyshell access token (from `environments.bunnyshell.com/access-token`)
1634
+ * belongs to a *user* and carries their access to every organization they are
1635
+ * in, exactly as a Heroku token does. Unlike Netlify's, that does not force an
1636
+ * organization onto the connection, because nothing here is addressed *through*
1637
+ * one: both variable collections name their parent by an opaque, globally
1638
+ * unique id (`environment` or `project`), so the token plus the destination's
1639
+ * id is the whole address. The API offers an `organization` filter, but it
1640
+ * narrows a listing — it is not part of an address.
1641
+ */
1642
+ const bunnyshellConnectionConfigSchema = z.object({ provider: z.literal("bunnyshell") });
1643
+ /**
1644
+ * GitHub account scope — empty for github.com, which is the whole point.
1645
+ *
1646
+ * A GitHub token addresses everything by `{owner}/{repo}` or `{org}`, and those
1647
+ * are the destination's business, so there is no account half to state the way
1648
+ * Cloudflare and Netlify need one. `baseUrl` is the single exception, and it is
1649
+ * not an account scope at all: it names a **GitHub Enterprise Server** install,
1650
+ * whose API lives on the customer's own host rather than on `api.github.com`.
1651
+ *
1652
+ * Left unset for github.com and for Enterprise Cloud (which is `api.github.com`
1653
+ * with a different plan behind it). Set only for a self-hosted GHES appliance,
1654
+ * where the REST API is at `https://<host>/api/v3`.
1655
+ */
1656
+ const githubActionsConnectionConfigSchema = z.object({
1657
+ provider: z.literal("github-actions"),
1658
+ /**
1659
+ * GitHub Enterprise Server API root, e.g. `https://github.acme.com/api/v3`.
1660
+ * Omit for github.com. Must be `https:` — this URL carries the token.
1661
+ */
1662
+ baseUrl: z.string().trim().max(300).refine((value) => {
1663
+ let parsed;
1664
+ try {
1665
+ parsed = new URL(value);
1666
+ } catch {
1667
+ return false;
1668
+ }
1669
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password;
1670
+ }, "must be an https:// URL — the GitHub Enterprise Server API root, e.g. https://github.acme.com/api/v3").optional()
1671
+ });
1672
+ /**
1673
+ * A Google Cloud project, as `projects/{project}` accepts one: either the
1674
+ * project **ID** (`acme-prod`, 6–30 characters, what the console shows) or the
1675
+ * project **number** (all digits). Both are accepted because both work, and
1676
+ * the id is the one an operator can read off their own dashboard.
1677
+ *
1678
+ * Validated by shape for the reason Cloudflare's account id is: every Secret
1679
+ * Manager URL is built from this string, and a typo would otherwise surface as
1680
+ * a 403 from Google hours later inside an alarm, with nobody watching.
1681
+ */
1682
+ const gcpProjectSchema = z.string().trim().refine((value) => /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(value) || /^\d{1,20}$/.test(value), "must be a Google Cloud project ID (e.g. acme-prod) or project number");
1683
+ /**
1684
+ * Google Cloud project scope — which project's Secret Manager to write.
1685
+ *
1686
+ * The service account is *not* here, unlike AWS's access key id: a GCP
1687
+ * credential is a key JSON that names its own `client_email`, so the identity
1688
+ * arrives with the credential the way a Vercel token's does. What the
1689
+ * credential cannot say is which project to write, because a service account
1690
+ * can be granted access to secrets in projects other than its own — so that is
1691
+ * this field, exactly as Cloudflare's account id is.
1692
+ *
1693
+ * One project per connection. Syncing an environment into two projects means
1694
+ * two connections, which also keeps their key grants separate.
1695
+ *
1696
+ * Global secrets only: v1 addresses `secretmanager.googleapis.com`, not the
1697
+ * per-location `secretmanager.<location>.rep.googleapis.com` endpoints that
1698
+ * regional secrets live behind. Data residency is expressed instead through the
1699
+ * destination's user-managed replication.
1700
+ */
1701
+ const gcpSecretManagerConnectionConfigSchema = z.object({
1702
+ provider: z.literal("gcp-secret-manager"),
1703
+ /** Project ID (`acme-prod`) or project number. */
1704
+ projectId: gcpProjectSchema
1705
+ });
1706
+ /**
1707
+ * LangSmith workspace/tenant scope for LangGraph Platform.
1708
+ *
1709
+ * The API key is never here — it is wrapped to the connection's public key and
1710
+ * stored as ciphertext, exactly as Vercel's token is.
1711
+ *
1712
+ * Two optional fields, for two different situations, and setting both is
1713
+ * rejected rather than silently resolved:
1714
+ *
1715
+ * - `region` picks one of {@link LANGGRAPH_PLATFORM_HOSTS}. Omitted means
1716
+ * `us`, which is where an account created at `smith.langchain.com` lives.
1717
+ * - `baseUrl` points the connection at a **self-hosted** LangSmith install,
1718
+ * whose control plane is served from the customer's own host under
1719
+ * `/api-host` rather than from `*.api.host.langchain.com`.
1720
+ *
1721
+ * `tenantId` is the workspace a key was minted in. A workspace-scoped key names
1722
+ * its own tenant and does not need it; an organization-scoped key reaches
1723
+ * several workspaces and gets a bare 403 without it, which is the same trap
1724
+ * Vercel's `teamId` sets — so it is passed through as `X-Tenant-Id` whenever
1725
+ * it is present.
1726
+ */
1727
+ const langgraphPlatformConnectionConfigSchema = z.object({
1728
+ provider: z.literal("langgraph-platform"),
1729
+ /** Control-plane region. Omit for `us`. Mutually exclusive with `baseUrl`. */
1730
+ region: z.enum([
1731
+ "us",
1732
+ "eu",
1733
+ "apac",
1734
+ "aws-us"
1735
+ ]).optional(),
1736
+ /**
1737
+ * Self-hosted LangSmith control-plane root, e.g.
1738
+ * `https://langsmith.acme.com/api-host`. Omit for LangChain's own hosts.
1739
+ * Must be `https:` — this URL carries the API key.
1740
+ */
1741
+ baseUrl: z.string().trim().max(300).refine((value) => {
1742
+ let parsed;
1743
+ try {
1744
+ parsed = new URL(value);
1745
+ } catch {
1746
+ return false;
1747
+ }
1748
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password;
1749
+ }, "must be an https:// URL — the self-hosted control-plane root, e.g. https://langsmith.acme.com/api-host").optional(),
1750
+ /** LangSmith workspace (tenant) UUID, sent as `X-Tenant-Id`. */
1751
+ tenantId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangSmith workspace UUID").optional()
1752
+ }).refine((c) => !(c.baseUrl !== void 0 && c.region !== void 0), {
1753
+ message: "set region for a LangChain-hosted account or baseUrl for a self-hosted one, not both",
1754
+ path: ["baseUrl"]
1755
+ });
1756
+ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1757
+ vercelConnectionConfigSchema,
1758
+ cloudflareWorkersConnectionConfigSchema,
1759
+ cloudflarePagesConnectionConfigSchema,
1760
+ cloudflareSecretsStoreConnectionConfigSchema,
1761
+ railwayConnectionConfigSchema,
1762
+ awsSecretsManagerConnectionConfigSchema,
1763
+ awsParameterStoreConnectionConfigSchema,
1764
+ renderConnectionConfigSchema,
1765
+ flyConnectionConfigSchema,
1766
+ northflankConnectionConfigSchema,
1767
+ digitalOceanConnectionConfigSchema,
1768
+ herokuConnectionConfigSchema,
1769
+ netlifyConnectionConfigSchema,
1770
+ bunnyshellConnectionConfigSchema,
1771
+ githubActionsConnectionConfigSchema,
1772
+ gcpSecretManagerConnectionConfigSchema,
1773
+ langgraphPlatformConnectionConfigSchema
1774
+ ]);
1067
1775
  const vercelDestinationSchema = z.object({
1068
1776
  provider: z.literal("vercel"),
1069
1777
  /** Vercel project id (`prj_…`) or project name. */
@@ -1080,7 +1788,715 @@ const vercelDestinationSchema = z.object({
1080
1788
  */
1081
1789
  gitBranch: z.string().trim().min(1).max(255).optional()
1082
1790
  });
1083
- const syncDestinationSchema = z.discriminatedUnion("provider", [vercelDestinationSchema]);
1791
+ /**
1792
+ * The Worker whose secrets a binding owns. Wrangler *environments* are not a
1793
+ * separate field because they are not a separate concept at the API: deploying
1794
+ * `my-api` with `--env staging` creates a Worker literally named
1795
+ * `my-api-staging`, so pointing at an environment means naming that script.
1796
+ */
1797
+ const cloudflareWorkersDestinationSchema = z.object({
1798
+ provider: z.literal("cloudflare-workers"),
1799
+ /** Worker script name, as shown in the dashboard (`my-api`). */
1800
+ scriptName: z.string().trim().min(1).max(63).regex(/^[A-Za-z0-9_][A-Za-z0-9_-]*$/, "must be a Worker script name")
1801
+ });
1802
+ const cloudflarePagesDestinationSchema = z.object({
1803
+ provider: z.literal("cloudflare-pages"),
1804
+ /** Pages project name (`my-site`) — Pages has no separate project id. */
1805
+ projectName: z.string().trim().min(1).max(58).regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/, "must be a Pages project name"),
1806
+ /** Which deployment configs receive these values. At least one. */
1807
+ environments: z.array(z.enum(["production", "preview"])).min(1)
1808
+ });
1809
+ const cloudflareSecretsStoreDestinationSchema = z.object({
1810
+ provider: z.literal("cloudflare-secrets-store"),
1811
+ /** Store id (32 hex). An account has exactly one store today. */
1812
+ storeId: z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Secrets Store ID (lowercase hex)"),
1813
+ /** Scopes applied to secrets this binding creates. At least one. */
1814
+ scopes: z.array(z.enum([
1815
+ "workers",
1816
+ "ai_gateway",
1817
+ "dex",
1818
+ "access",
1819
+ "containers",
1820
+ "websearch"
1821
+ ])).min(1)
1822
+ });
1823
+ /**
1824
+ * Where inside Railway a binding writes.
1825
+ *
1826
+ * Railway variables are addressed by (project, environment, service) — the
1827
+ * environment here is *Railway's* (`production`, `pr-42`), not the seekrit
1828
+ * environment the binding reads from; a binding is precisely the mapping
1829
+ * between the two.
1830
+ *
1831
+ * Omitting `serviceId` targets the project's **shared** variables for that
1832
+ * environment, which services opt into with `${{shared.NAME}}`. That is a
1833
+ * genuinely different destination from any one service's variables, so it is an
1834
+ * absent field rather than a sentinel.
1835
+ */
1836
+ const railwayDestinationSchema = z.object({
1837
+ provider: z.literal("railway"),
1838
+ /** Railway project id (a UUID, from the project's Settings page or URL). */
1839
+ projectId: railwayIdSchema,
1840
+ /** Railway environment id (a UUID) — the deployment environment to write. */
1841
+ environmentId: railwayIdSchema,
1842
+ /** Service to write. Omit to write the environment's shared variables. */
1843
+ serviceId: railwayIdSchema.optional(),
1844
+ /**
1845
+ * Suppress the redeploy Railway triggers when a variable changes.
1846
+ *
1847
+ * Left off (the default), a sync that changes a value redeploys the service,
1848
+ * which is what makes the new value actually reach the running process —
1849
+ * Railway applies variables at deploy time. Turn it on when deploys are
1850
+ * gated behind a release process and a secrets push must not start one; the
1851
+ * values then sit staged until the next deploy.
1852
+ */
1853
+ skipDeploys: z.boolean().optional()
1854
+ });
1855
+ /**
1856
+ * A customer-managed KMS key to encrypt with, as a key id, ARN, or alias
1857
+ * (`alias/seekrit`). Omitted means the AWS-managed default for that service
1858
+ * (`aws/secretsmanager`, `aws/ssm`), which is what most accounts want.
1859
+ *
1860
+ * Deliberately loose: a KMS key can be named five different ways, half of them
1861
+ * cross-account ARNs, and rejecting a valid one here would be worse than
1862
+ * letting KMS give its own (very clear) error.
1863
+ */
1864
+ const awsKmsKeyIdSchema = z.string().trim().min(1).max(2048);
1865
+ /**
1866
+ * Where in Secrets Manager a binding writes.
1867
+ *
1868
+ * `pathPrefix` exists rather than reusing {@link NameTransform}'s `prefix`
1869
+ * because the two answer different questions: a name transform produces a
1870
+ * *variable name* (`[A-Za-z0-9_]`, no slashes), while this produces a
1871
+ * *namespace* — `prod/storefront/` — and slashes are the whole point of it.
1872
+ */
1873
+ const awsSecretsManagerDestinationSchema = z.object({
1874
+ provider: z.literal("aws-secrets-manager"),
1875
+ layout: z.enum(["secret-per-name", "json-bundle"]).default("secret-per-name"),
1876
+ /**
1877
+ * `secret-per-name` only: prepended to every secret's name, e.g.
1878
+ * `prod/storefront/`. Optional, but strongly advised in an account that
1879
+ * holds anything else — without it a binding writes at the root of a
1880
+ * namespace it does not own.
1881
+ */
1882
+ pathPrefix: z.string().trim().max(400).regex(/^[A-Za-z0-9/_+=.@-]*$/, "may contain letters, digits, and / _ + = . @ -").optional(),
1883
+ /** `json-bundle` only: the one secret that holds every value, e.g. `prod/storefront/env`. */
1884
+ secretName: z.string().trim().min(1).max(512).regex(/^[A-Za-z0-9/_+=.@-]+$/, "may contain letters, digits, and / _ + = . @ -").optional(),
1885
+ kmsKeyId: awsKmsKeyIdSchema.optional()
1886
+ }).refine((d) => d.layout !== "json-bundle" || d.secretName !== void 0, {
1887
+ message: "a json-bundle destination needs the name of the secret to write",
1888
+ path: ["secretName"]
1889
+ });
1890
+ /**
1891
+ * The Parameter Store hierarchy a binding owns, e.g. `/prod/storefront/`.
1892
+ *
1893
+ * A path rather than a free-form prefix because that is what the API is built
1894
+ * around: `GetParametersByPath` is how an application reads a whole
1895
+ * environment in one call, and it only works on `/`-delimited names. Leading
1896
+ * and trailing slashes are required so the binding's names concatenate
1897
+ * unambiguously — `/prod/storefront/` + `DB_URL`.
1898
+ */
1899
+ const awsParameterStoreDestinationSchema = z.object({
1900
+ provider: z.literal("aws-parameter-store"),
1901
+ /** Must start and end with `/`. `aws`/`ssm` are reserved by AWS as the first segment. */
1902
+ path: z.string().trim().max(1011).regex(/^\/([A-Za-z0-9_.-]+\/)*$/, "must be a parameter path like /prod/storefront/"),
1903
+ type: z.enum(["SecureString", "String"]).default("SecureString"),
1904
+ /**
1905
+ * Standard caps a value at 4KB and costs nothing; Advanced raises that to 8KB
1906
+ * and is billed per parameter per month. `Intelligent-Tiering` lets AWS pick,
1907
+ * upgrading only the parameters that need it.
1908
+ */
1909
+ tier: z.enum([
1910
+ "Standard",
1911
+ "Advanced",
1912
+ "Intelligent-Tiering"
1913
+ ]).default("Standard"),
1914
+ kmsKeyId: awsKmsKeyIdSchema.optional()
1915
+ });
1916
+ /**
1917
+ * Render resource ids are `<prefix>-<slug>`, and the two prefixes below are the
1918
+ * documented ones: `srv-` for every service type, `crn-` for cron jobs, `evg-`
1919
+ * for an environment group.
1920
+ *
1921
+ * The patterns reject the *other* kind's prefix rather than requiring their own.
1922
+ * The mistake worth catching is pasting an env-group id into the service field
1923
+ * (or the reverse) — which is otherwise a 404 hours later inside an alarm, with
1924
+ * nobody watching. Requiring the positive prefix would also reject a valid id
1925
+ * the day Render introduces a new resource prefix, which is not our call to
1926
+ * make.
1927
+ */
1928
+ const renderServiceIdSchema = z.string().trim().regex(/^(?!evg-)[A-Za-z0-9_-]{1,64}$/, "must be a Render service ID (`srv-…` or `crn-…`), not an environment group");
1929
+ const renderEnvGroupIdSchema = z.string().trim().regex(/^(?!srv-|crn-)[A-Za-z0-9_-]{1,64}$/, "must be a Render environment group ID (`evg-…`), not a service");
1930
+ /** Environment variables set directly on one service. */
1931
+ const renderServiceDestinationSchema = z.object({
1932
+ provider: z.literal("render"),
1933
+ kind: z.literal("service"),
1934
+ /** Service id (`srv-…`, or `crn-…` for a cron job), from its dashboard URL. */
1935
+ serviceId: renderServiceIdSchema
1936
+ });
1937
+ /**
1938
+ * Environment variables in a shared environment group. Every service linked to
1939
+ * the group sees them, which is the point — and the reason a group binding is
1940
+ * worth thinking about twice: its blast radius is the link list, not one
1941
+ * service.
1942
+ */
1943
+ const renderEnvGroupDestinationSchema = z.object({
1944
+ provider: z.literal("render"),
1945
+ kind: z.literal("env-group"),
1946
+ /** Environment group id (`evg-…`), from its dashboard URL. */
1947
+ envGroupId: renderEnvGroupIdSchema
1948
+ });
1949
+ const renderDestinationSchema = z.discriminatedUnion("kind", [renderServiceDestinationSchema, renderEnvGroupDestinationSchema]);
1950
+ /**
1951
+ * The Fly app whose secret set a binding owns.
1952
+ *
1953
+ * A Fly app has **one** secret set, shared by every Machine in every region —
1954
+ * there is no per-target split to state, the way Vercel and Pages have one.
1955
+ * Fly's convention is that staging and production are separate *apps*
1956
+ * (`storefront`, `storefront-staging`), so pointing at an environment means
1957
+ * naming that app, exactly as a Wrangler environment means naming its own
1958
+ * Worker.
1959
+ *
1960
+ * Values land **staged**: Fly injects secrets when a Machine boots, so already
1961
+ * running Machines keep what they started with until the app is deployed or its
1962
+ * Machines are updated (`fly secrets deploy -a <app>`), while Machines created
1963
+ * after the push get them straight away. The connector deliberately restarts
1964
+ * nothing — see the note in `apps/api/src/lib/sync/connectors/fly.ts`.
1965
+ */
1966
+ const flyDestinationSchema = z.object({
1967
+ provider: z.literal("fly"),
1968
+ /** Fly app name, as `fly apps list` prints it. */
1969
+ appName: z.string().trim().min(1).max(63).regex(/^[a-z0-9][a-z0-9-]*$/, "must be a Fly app name (lowercase letters, numbers, and dashes)")
1970
+ });
1971
+ /**
1972
+ * Where inside Northflank a binding writes: one **secret group** in one
1973
+ * project.
1974
+ *
1975
+ * A secret group is Northflank's unit of injection — services and jobs in the
1976
+ * project inherit its variables, subject to the group's own restrictions and
1977
+ * priority. Those settings belong to the operator, not to seekrit: a binding
1978
+ * names an existing group and only ever writes its `variables` map, so
1979
+ * restrictions, priority, secret type, and any secret *files* stay as they were
1980
+ * configured.
1981
+ *
1982
+ * There is no environment field. Northflank has no per-group environment axis —
1983
+ * separate environments are separate projects (or separate groups restricted to
1984
+ * a stage), so the binding's seekrit environment maps to a group, one to one.
1985
+ */
1986
+ const northflankDestinationSchema = z.object({
1987
+ provider: z.literal("northflank"),
1988
+ /** Project id — the slug in the project URL (`default-project`). */
1989
+ projectId: northflankIdSchema,
1990
+ /** Secret group id — the slug in the group's URL (`example-secret-group`). */
1991
+ secretGroupId: northflankIdSchema
1992
+ });
1993
+ /**
1994
+ * When App Platform makes a variable visible. DigitalOcean's enum also has
1995
+ * `UNSET`, which is not offered: it means "no scope stated", and a secrets
1996
+ * manager that writes a value should say when that value applies.
1997
+ *
1998
+ * The default here is `RUN_TIME` rather than DigitalOcean's own
1999
+ * `RUN_AND_BUILD_TIME`, and the difference is deliberate. A build-time variable
2000
+ * is visible to every build command, every buildpack, and anything they print;
2001
+ * a secret only the running process needs has no business being there. Binding
2002
+ * a value a build genuinely needs — a private registry token, a sourcemap
2003
+ * upload key — is a decision worth making explicitly.
2004
+ */
2005
+ const DIGITALOCEAN_ENV_SCOPES = [
2006
+ "RUN_TIME",
2007
+ "BUILD_TIME",
2008
+ "RUN_AND_BUILD_TIME"
2009
+ ];
2010
+ /**
2011
+ * A DigitalOcean app id — the UUID in the app's dashboard URL
2012
+ * (`cloud.digitalocean.com/apps/<id>`), and what `doctl apps list` prints.
2013
+ *
2014
+ * DigitalOcean's own spec types this as a bare string, but every app id it
2015
+ * issues is a UUID, and the slip worth catching is the one the API cannot tell
2016
+ * from a typo: pasting the app's *name* (`storefront`), which `GET /v2/apps/{id}`
2017
+ * answers with a flat 404 hours later inside an alarm, with nobody watching.
2018
+ */
2019
+ const digitalOceanAppIdSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a DigitalOcean app ID (the UUID in the app's URL), not its name");
2020
+ /**
2021
+ * A component name, matching App Platform's own pattern for one. Names are
2022
+ * unique within an app, which is what makes a name — rather than an index into
2023
+ * `services` — the stable way to address a component's variables.
2024
+ */
2025
+ const digitalOceanComponentNameSchema = z.string().trim().regex(/^[a-z][a-z0-9-]{0,30}[a-z0-9]$/, "must be an App Platform component name (lowercase letters, numbers, and dashes)");
2026
+ /**
2027
+ * Where inside a DigitalOcean app a binding writes.
2028
+ *
2029
+ * ## A push is a deployment
2030
+ *
2031
+ * App Platform has no per-variable endpoint. Environment variables live in the
2032
+ * app spec, and the only way to change one is to submit a new spec — which
2033
+ * starts a **new deployment** of the app. That is not a side effect this
2034
+ * connector chose and there is no flag to suppress it; it is what "set an
2035
+ * environment variable" means on this platform, in the control panel and in
2036
+ * `doctl` alike.
2037
+ *
2038
+ * The deployment reuses each component's current source (seekrit never sends
2039
+ * `update_all_source_versions`), so it redeploys the code already running
2040
+ * rather than pulling a newer commit or image. It is still a real deployment:
2041
+ * a build, a health check, and a rollout. Bind an environment here knowing that
2042
+ * changing a secret in it will roll the app.
2043
+ *
2044
+ * ## Values are written encrypted
2045
+ *
2046
+ * Everything seekrit writes goes in as `type: SECRET`, so App Platform encrypts
2047
+ * it at rest and hands it back as an opaque `EV[1:…]` blob rather than as
2048
+ * plaintext. That is also why this connector cannot tell whether a value it is
2049
+ * about to write is already there — see
2050
+ * `apps/api/src/lib/sync/connectors/digitalocean.ts`.
2051
+ */
2052
+ const digitalOceanAppDestinationSchema = z.object({
2053
+ provider: z.literal("digitalocean"),
2054
+ kind: z.literal("app"),
2055
+ /** App id — the UUID in `cloud.digitalocean.com/apps/<id>`. */
2056
+ appId: digitalOceanAppIdSchema,
2057
+ scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
2058
+ });
2059
+ /**
2060
+ * One component's own environment variables. Narrower than the app-level list:
2061
+ * only this service, worker, job, static site, or function sees them, and a key
2062
+ * here wins over the same key at app level.
2063
+ */
2064
+ const digitalOceanComponentDestinationSchema = z.object({
2065
+ provider: z.literal("digitalocean"),
2066
+ kind: z.literal("component"),
2067
+ appId: digitalOceanAppIdSchema,
2068
+ /** Component name, as it appears in the app spec — not its type. */
2069
+ componentName: digitalOceanComponentNameSchema,
2070
+ scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
2071
+ });
2072
+ const digitalOceanDestinationSchema = z.discriminatedUnion("kind", [digitalOceanAppDestinationSchema, digitalOceanComponentDestinationSchema]);
2073
+ /**
2074
+ * A Heroku app, named the way `/apps/{app_id_or_name}` names one: either the
2075
+ * app name or its UUID id. Both are accepted because both work, and the id is
2076
+ * the durable one — renaming an app in the dashboard breaks a binding that
2077
+ * holds its name, and does not break one that holds its id.
2078
+ *
2079
+ * The name pattern is Heroku's own (`^[a-z][a-z0-9-]{1,28}[a-z0-9]$`): 3–30
2080
+ * characters, starting with a letter and ending alphanumeric. Checking it here
2081
+ * turns the habitual slip — pasting `example.herokuapp.com`, or a name with
2082
+ * capitals — into a message at the form rather than a bare 404 from an alarm
2083
+ * with nobody watching.
2084
+ */
2085
+ const herokuAppSchema = z.string().trim().refine((value) => /^[a-z][a-z0-9-]{1,28}[a-z0-9]$/.test(value) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), "must be a Heroku app name (lowercase letters, numbers, and dashes) or its UUID");
2086
+ /**
2087
+ * The Heroku app whose config vars a binding owns.
2088
+ *
2089
+ * A Heroku app has **one** set of config vars, shared by every dyno and every
2090
+ * process type — there is no per-target split to state the way Vercel and Pages
2091
+ * have one. Heroku's convention is that staging and production are separate
2092
+ * *apps* (`storefront`, `storefront-staging`), so pointing at an environment
2093
+ * means naming that app, exactly as it does on Fly.
2094
+ *
2095
+ * Unlike Fly, values take effect **immediately**: setting config vars cuts a new
2096
+ * release and restarts the app's dynos, which is why a run sends exactly one
2097
+ * request — see the note in `apps/api/src/lib/sync/connectors/heroku.ts`.
2098
+ */
2099
+ const herokuDestinationSchema = z.object({
2100
+ provider: z.literal("heroku"),
2101
+ /** App name as `heroku apps` prints it, or the app's UUID. */
2102
+ app: herokuAppSchema
2103
+ });
2104
+ /**
2105
+ * The deploy contexts a Netlify value can be set for.
2106
+ *
2107
+ * These are Netlify's own, minus two. `all` is missing deliberately: Netlify
2108
+ * requires a **secret** value to be set against explicit contexts, and its
2109
+ * `setEnvVarValue` endpoint is reported to fail outright on `context: "all"` —
2110
+ * so the union offers only contexts that work under both. Naming the contexts
2111
+ * you mean is what you want here anyway; a binding already exists to map one
2112
+ * seekrit environment onto one deploy context. `dev-server` (Preview Server) is
2113
+ * left out for want of anyone asking.
2114
+ *
2115
+ * `branch` is the odd one: it needs a branch name alongside it, which the
2116
+ * destination carries as {@link netlifyDestinationSchema}'s `branch`.
2117
+ */
2118
+ const NETLIFY_CONTEXTS = [
2119
+ "production",
2120
+ "deploy-preview",
2121
+ "branch-deploy",
2122
+ "branch",
2123
+ "dev"
2124
+ ];
2125
+ /**
2126
+ * A Netlify site, by its **API ID** — the UUID under Project configuration →
2127
+ * General → Project information.
2128
+ *
2129
+ * Netlify accepts a site's domain in place of its id where a site appears in a
2130
+ * *path* (`/sites/{site_id}`), but the environment variable endpoints take the
2131
+ * site as a `?site_id=` **query parameter** instead, and Netlify documents no
2132
+ * name resolution there. That asymmetry is why this is strict where the Heroku
2133
+ * and Fly destinations are permissive: a `site_id` Netlify does not resolve
2134
+ * does not 404 — the write lands on the *team*, as a shared variable inherited
2135
+ * by every site in it. Refusing anything but the UUID keeps a slip from turning
2136
+ * into a much wider blast radius than the operator asked for.
2137
+ */
2138
+ const netlifySiteIdSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be the site's API ID (a UUID), not its name or URL");
2139
+ /**
2140
+ * The Netlify site, and which of its deploy contexts a binding owns.
2141
+ *
2142
+ * Netlify keys a value by (site, context), so the destination is that pair.
2143
+ * Contexts are a list rather than a single value for the same reason Vercel's
2144
+ * `targets` is one: a binding is unique per (connection, environment), so an
2145
+ * environment that feeds both production and deploy previews has to say so in
2146
+ * one destination or not at all.
2147
+ *
2148
+ * A push writes **only** the contexts listed here. Other contexts of the same
2149
+ * variable — and every variable this binding does not manage — are left as they
2150
+ * are, which is what makes it safe to point at a site that already has
2151
+ * variables set by hand.
2152
+ *
2153
+ * `secret` marks what seekrit creates as a Netlify **secret**: write-only, and
2154
+ * unreadable afterwards through the UI, CLI, and API. On by default, since that
2155
+ * is the whole point of pushing from a secrets manager. It applies only to
2156
+ * variables seekrit *creates* — Netlify will not let a flag be added to an
2157
+ * existing variable, or removed from one ever — and it needs a plan that
2158
+ * includes Secrets Controller.
2159
+ */
2160
+ const netlifyDestinationSchema = z.object({
2161
+ provider: z.literal("netlify"),
2162
+ /** Site API ID (a UUID), from Project configuration → General. */
2163
+ siteId: netlifySiteIdSchema,
2164
+ /** Which deploy contexts receive these values. At least one. */
2165
+ contexts: z.array(z.enum(NETLIFY_CONTEXTS)).min(1),
2166
+ /** Branch name, required when `contexts` includes `branch`; ignored otherwise. */
2167
+ branch: z.string().trim().min(1).max(255).optional(),
2168
+ /** Create variables as Netlify secrets (default true). */
2169
+ secret: z.boolean().optional()
2170
+ }).refine((d) => !d.contexts.includes("branch") || d.branch !== void 0, {
2171
+ message: "a branch context needs the branch name it applies to",
2172
+ path: ["branch"]
2173
+ });
2174
+ /**
2175
+ * A Bunnyshell resource id, as the platform hands it out.
2176
+ *
2177
+ * Deliberately loose. Bunnyshell documents no format for these — they are
2178
+ * opaque strings from `bns environments list` or the dashboard URL — so
2179
+ * asserting a shape here would be inventing a rule the platform never stated,
2180
+ * and the failure mode would be seekrit refusing an id that works.
2181
+ *
2182
+ * Being loose is affordable here in a way it is not on Netlify, where an
2183
+ * unresolved `site_id` silently widens a write to the whole team. Both
2184
+ * Bunnyshell variable collections name their parent in the **request body** of
2185
+ * a create, as a required relation: an id the platform cannot resolve is a 422
2186
+ * naming the field, not a write that lands somewhere broader. The listing side
2187
+ * is fenced separately — the connector re-checks every variable's own parent
2188
+ * before it touches it, so a filter that failed to bite cannot turn into an
2189
+ * edit of a neighbouring environment's variables.
2190
+ */
2191
+ const bunnyshellIdSchema = z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a Bunnyshell ID — no slashes or spaces");
2192
+ /**
2193
+ * Variables on one Bunnyshell **environment** — the set every component in it
2194
+ * inherits, and the closest match to a seekrit environment.
2195
+ *
2196
+ * This is the destination for an environment that already exists and stays
2197
+ * around: a primary environment, or a long-lived ephemeral one.
2198
+ */
2199
+ const bunnyshellEnvironmentDestinationSchema = z.object({
2200
+ provider: z.literal("bunnyshell"),
2201
+ kind: z.literal("environment"),
2202
+ /** Environment ID, as `bns environments list` prints it. */
2203
+ environmentId: bunnyshellIdSchema,
2204
+ /** Mark what seekrit creates as a Bunnyshell secret (default true). */
2205
+ secret: z.boolean().optional()
2206
+ });
2207
+ /**
2208
+ * Variables on a Bunnyshell **project** — inherited by every environment
2209
+ * created in it from then on.
2210
+ *
2211
+ * Worth thinking about twice, for the reason a Render environment group is:
2212
+ * the blast radius is the project, not one environment. It earns its place
2213
+ * anyway, because it is the only destination that reaches an environment which
2214
+ * *does not exist yet*. Bunnyshell's whole shape is ephemeral environments spun
2215
+ * up per branch or per pull request; pushing to the environment cannot seed one
2216
+ * that a webhook will create tomorrow, and pushing to the project can.
2217
+ *
2218
+ * An environment inherits the project's value at creation and may then be
2219
+ * overridden at its own scope — so a project binding does not fight an
2220
+ * environment binding pointed at the same name, it loses to it.
2221
+ */
2222
+ const bunnyshellProjectDestinationSchema = z.object({
2223
+ provider: z.literal("bunnyshell"),
2224
+ kind: z.literal("project"),
2225
+ /** Project ID, as `bns projects list` prints it. */
2226
+ projectId: bunnyshellIdSchema,
2227
+ /** Mark what seekrit creates as a Bunnyshell secret (default true). */
2228
+ secret: z.boolean().optional()
2229
+ });
2230
+ /**
2231
+ * Where in Bunnyshell a binding writes.
2232
+ *
2233
+ * Split on `kind` rather than into two providers — the way Render's service and
2234
+ * environment group are, and unlike Cloudflare's three — because the two are the
2235
+ * same API twice over: `/v1/environment_variables` and `/v1/project_variables`
2236
+ * take the same fields, fail the same ways, and differ only in which parent they
2237
+ * name. One connector serves both, so one provider does too.
2238
+ *
2239
+ * `secret` is Bunnyshell's `isSecret`, and means less than Netlify's flag of the
2240
+ * same name: Bunnyshell encrypts every variable with an organization key whether
2241
+ * or not the flag is set, so this only decides whether the value is obscured in
2242
+ * the dashboard and stored encrypted in an exported definition. It is on by
2243
+ * default all the same — a value pushed from a secrets manager should not be
2244
+ * sitting in plain view of everyone with project access. It applies only to
2245
+ * variables seekrit **creates**: an update never sends the flag, so a variable
2246
+ * an operator deliberately un-secreted stays that way.
2247
+ */
2248
+ const bunnyshellDestinationSchema = z.discriminatedUnion("kind", [bunnyshellEnvironmentDestinationSchema, bunnyshellProjectDestinationSchema]);
2249
+ /**
2250
+ * Which repositories in an organization can read an org-level secret.
2251
+ *
2252
+ * GitHub's own enum, unchanged. There is deliberately **no default**: `all` hands
2253
+ * the value to every repository in the organization — including ones added
2254
+ * tomorrow, and including forks' workflows to the extent the org allows them —
2255
+ * and that is not a blast radius a secrets manager should pick on an operator's
2256
+ * behalf. Naming it is the point.
2257
+ */
2258
+ const GITHUB_ACTIONS_VISIBILITIES = [
2259
+ "all",
2260
+ "private",
2261
+ "selected"
2262
+ ];
2263
+ /**
2264
+ * A GitHub account or organization login, matching GitHub's own rule:
2265
+ * alphanumeric with single internal hyphens, 39 characters at most.
2266
+ *
2267
+ * Checked here so the habitual slip — pasting a URL, or `owner/repo` into the
2268
+ * owner field — fails at the form rather than as a 404 from an alarm with nobody
2269
+ * watching.
2270
+ */
2271
+ const githubOwnerSchema = z.string().trim().min(1).max(39).regex(/^[A-Za-z0-9](?:-?[A-Za-z0-9])*$/, "must be a GitHub user or organization login — not a URL or an owner/repo pair");
2272
+ /**
2273
+ * A repository name. GitHub's rules are looser than an owner's: letters,
2274
+ * numbers, hyphens, underscores, and periods, up to 100 characters. `.` and `..`
2275
+ * are refused outright — they would traverse the API path rather than name a
2276
+ * repository.
2277
+ */
2278
+ const githubRepoSchema = z.string().trim().min(1).max(100).regex(/^[A-Za-z0-9._-]+$/, "must be a repository name alone (`api`), not `owner/repo` or a URL").refine((value) => value !== "." && value !== "..", "is not a repository name");
2279
+ /**
2280
+ * A deployment environment name.
2281
+ *
2282
+ * Deliberately permissive: GitHub allows spaces and most punctuation here, and
2283
+ * the dashboard shows names like `prod (eu-west)`. Only the two things that would
2284
+ * break the request are refused — an empty name, and the path separators that
2285
+ * would let a name escape its URL segment. Everything else is GitHub's to reject.
2286
+ */
2287
+ const githubEnvironmentSchema = z.string().trim().min(1).max(255).refine((value) => !value.includes("/") && !value.includes("\\"), "cannot contain a slash — that is a path separator, not part of an environment name");
2288
+ /**
2289
+ * One repository's Actions secrets.
2290
+ *
2291
+ * Every workflow in the repository can read these, including one added by a pull
2292
+ * request from a collaborator with write access. That is GitHub's model, not a
2293
+ * choice this connector makes — but it is the reason `environment` exists below,
2294
+ * and the reason to prefer it for anything that touches production.
2295
+ */
2296
+ const githubActionsRepoDestinationSchema = z.object({
2297
+ provider: z.literal("github-actions"),
2298
+ kind: z.literal("repo"),
2299
+ /** Repository owner — a user or organization login. */
2300
+ owner: githubOwnerSchema,
2301
+ /** Repository name, without the owner. */
2302
+ repo: githubRepoSchema
2303
+ });
2304
+ /**
2305
+ * One deployment environment's Actions secrets — the narrowest scope GitHub has.
2306
+ *
2307
+ * A job reads these only by declaring `environment: <name>`, which also subjects
2308
+ * it to that environment's protection rules: required reviewers, wait timers, and
2309
+ * the branch policy. That combination is the closest GitHub gets to "this secret
2310
+ * is for production, and reaching it requires approval", and it is the scope to
2311
+ * reach for by default.
2312
+ *
2313
+ * The environment must already exist. This connector will not create one: an
2314
+ * environment is a deployment gate, and silently creating an unprotected one
2315
+ * because a name was misspelled would quietly remove the protection the operator
2316
+ * was relying on.
2317
+ */
2318
+ const githubActionsEnvironmentDestinationSchema = z.object({
2319
+ provider: z.literal("github-actions"),
2320
+ kind: z.literal("environment"),
2321
+ owner: githubOwnerSchema,
2322
+ repo: githubRepoSchema,
2323
+ /** Environment name, exactly as the repository's Settings → Environments shows it. */
2324
+ environment: githubEnvironmentSchema
2325
+ });
2326
+ /**
2327
+ * An organization's Actions secrets.
2328
+ *
2329
+ * The widest scope in the product, and the only destination on any provider that
2330
+ * can hand a value to repositories nobody named. Read {@link
2331
+ * GITHUB_ACTIONS_VISIBILITIES} before using it.
2332
+ *
2333
+ * `selectedRepositoryIds` takes numeric repository **ids**, not names, because
2334
+ * that is what GitHub's API takes. An id is visible at
2335
+ * `GET /repos/{owner}/{repo}` as `id`, and in the dashboard nowhere at all —
2336
+ * which is friction worth accepting rather than resolving names to ids here: name
2337
+ * resolution would mean this connector picking which repository an ambiguous
2338
+ * name meant, and getting that wrong widens a secret's reach silently.
2339
+ */
2340
+ const githubActionsOrgDestinationSchema = z.object({
2341
+ provider: z.literal("github-actions"),
2342
+ kind: z.literal("org"),
2343
+ /** Organization login. */
2344
+ org: githubOwnerSchema,
2345
+ /** Which repositories may read these secrets. Stated, never defaulted. */
2346
+ visibility: z.enum(GITHUB_ACTIONS_VISIBILITIES),
2347
+ /** Numeric repository ids, required when `visibility` is `selected`. */
2348
+ selectedRepositoryIds: z.array(z.number().int().positive()).max(500).optional()
2349
+ }).refine((dest) => dest.visibility !== "selected" || dest.selectedRepositoryIds !== void 0 && dest.selectedRepositoryIds.length > 0, {
2350
+ message: "selected visibility needs at least one repository id",
2351
+ path: ["selectedRepositoryIds"]
2352
+ }).refine((dest) => dest.visibility === "selected" || dest.selectedRepositoryIds === void 0, {
2353
+ message: "repository ids only apply to selected visibility — remove them, or select it",
2354
+ path: ["selectedRepositoryIds"]
2355
+ });
2356
+ const githubActionsDestinationSchema = z.discriminatedUnion("kind", [
2357
+ githubActionsRepoDestinationSchema,
2358
+ githubActionsEnvironmentDestinationSchema,
2359
+ githubActionsOrgDestinationSchema
2360
+ ]);
2361
+ /**
2362
+ * How a binding lays its secrets out in Secret Manager. The same two shapes the
2363
+ * AWS Secrets Manager destination offers, and for the same reasons:
2364
+ *
2365
+ * - `secret-per-name` — one GCP secret per seekrit secret. The direct
2366
+ * translation, and what Cloud Run's `--set-secrets` and GKE's Secret Manager
2367
+ * CSI driver mount one at a time.
2368
+ * - `json-bundle` — every value as one JSON object in a single secret. Costs one
2369
+ * active version instead of fifty, which is the whole billing unit here.
2370
+ */
2371
+ const GCP_SECRET_MANAGER_LAYOUTS = ["secret-per-name", "json-bundle"];
2372
+ /**
2373
+ * Where Google keeps the copies of a secret. Chosen at creation and
2374
+ * **immutable** afterwards — changing it means deleting the secret and letting
2375
+ * the next run recreate it.
2376
+ *
2377
+ * - `automatic` — Google picks the locations. One billable replica, and what
2378
+ * you want unless a policy says otherwise.
2379
+ * - `user-managed` — the binding names the regions. This is how data residency
2380
+ * is expressed for global secrets, and each region is billed as its own
2381
+ * active version.
2382
+ */
2383
+ const GCP_REPLICATION_POLICIES = ["automatic", "user-managed"];
2384
+ /**
2385
+ * A Secret Manager secret ID. Google's own rule, quoted from the API reference:
2386
+ * "a string with a maximum length of 255 characters and can contain uppercase
2387
+ * and lowercase letters, numerals, and the hyphen (`-`) and underscore (`_`)
2388
+ * characters."
2389
+ *
2390
+ * Notably **no slashes and no dots**, which is what makes this a different
2391
+ * field from AWS's `pathPrefix` rather than the same idea renamed: a Secret
2392
+ * Manager namespace is spelled `prod-storefront-DB_URL`, not
2393
+ * `prod/storefront/DB_URL`.
2394
+ */
2395
+ const gcpSecretIdSchema = z.string().trim().min(1).max(255).regex(/^[A-Za-z0-9_-]+$/, "may contain letters, digits, hyphens, and underscores");
2396
+ /**
2397
+ * A GCP region for a user-managed replica (`us-east1`, `europe-west4`,
2398
+ * `northamerica-northeast1`). Validated by shape rather than against a list,
2399
+ * because Google adds regions faster than we ship — a name Secret Manager does
2400
+ * not know is refused by Google with a clear message at creation.
2401
+ */
2402
+ const gcpLocationSchema = z.string().trim().regex(/^[a-z]+-[a-z]+\d+$/, "must be a GCP region ID, e.g. us-east1");
2403
+ /**
2404
+ * A Cloud KMS key, as its full resource name — the only form the API accepts:
2405
+ * `projects/p/locations/l/keyRings/r/cryptoKeys/k`.
2406
+ *
2407
+ * Stricter than AWS's `kmsKeyId` (which tolerates five spellings) because
2408
+ * Google tolerates exactly one, and because a key in the wrong *location* is
2409
+ * rejected at creation: an automatic-replication secret needs a `global` key,
2410
+ * and a user-managed replica needs one in its own region.
2411
+ */
2412
+ const gcpKmsKeyNameSchema = z.string().trim().max(1024).regex(/^projects\/[^/]+\/locations\/[^/]+\/keyRings\/[^/]+\/cryptoKeys\/[^/]+$/, "must be a full Cloud KMS key name (projects/…/locations/…/keyRings/…/cryptoKeys/…)");
2413
+ /**
2414
+ * Where inside a project's Secret Manager a binding writes.
2415
+ *
2416
+ * ## Every push would otherwise cost a version
2417
+ *
2418
+ * Secret Manager has no "set the value" call — only `addVersion`, which appends.
2419
+ * A run pushes the whole environment (never a diff), so changing one secret in
2420
+ * an environment of fifty would leave fifty new versions behind, forty-nine of
2421
+ * them identical to their predecessors, each one billed for as long as it stays
2422
+ * active.
2423
+ *
2424
+ * So this connector writes a version only when the value actually changed,
2425
+ * decided from a keyed digest it keeps in the secret's own **annotations** — see
2426
+ * `apps/api/src/lib/sync/connectors/gcp-secret-manager.ts` for why it is keyed
2427
+ * and what that costs. `pruneVersions` is the other half of the bill: with it
2428
+ * on, the version a push supersedes is destroyed as soon as the new one lands,
2429
+ * so a secret keeps exactly one active version.
2430
+ */
2431
+ const gcpSecretManagerDestinationSchema = z.object({
2432
+ provider: z.literal("gcp-secret-manager"),
2433
+ layout: z.enum(GCP_SECRET_MANAGER_LAYOUTS).default("secret-per-name"),
2434
+ /**
2435
+ * `secret-per-name` only: prepended to every secret ID, e.g.
2436
+ * `prod-storefront-`. Optional, but strongly advised in a project that holds
2437
+ * anything else — without it a binding writes at the root of a namespace it
2438
+ * does not own, and Secret Manager has no folders to hide behind.
2439
+ */
2440
+ idPrefix: z.string().trim().max(200).regex(/^[A-Za-z0-9_-]*$/, "may contain letters, digits, hyphens, and underscores").optional(),
2441
+ /** `json-bundle` only: the one secret that holds every value, e.g. `prod-storefront-env`. */
2442
+ secretId: gcpSecretIdSchema.optional(),
2443
+ replication: z.enum(GCP_REPLICATION_POLICIES).default("automatic"),
2444
+ /** `user-managed` only: the regions to replicate to. At least one. */
2445
+ locations: z.array(gcpLocationSchema).min(1).max(16).optional(),
2446
+ /** Customer-managed encryption key. Omitted means Google-managed keys. */
2447
+ kmsKeyName: gcpKmsKeyNameSchema.optional(),
2448
+ /** Destroy the version each push supersedes, keeping one active version. */
2449
+ pruneVersions: z.boolean().optional()
2450
+ }).refine((d) => d.layout !== "json-bundle" || d.secretId !== void 0, {
2451
+ message: "a json-bundle destination needs the ID of the secret to write",
2452
+ path: ["secretId"]
2453
+ }).refine((d) => d.replication !== "user-managed" || (d.locations?.length ?? 0) > 0, {
2454
+ message: "user-managed replication needs at least one location",
2455
+ path: ["locations"]
2456
+ }).refine((d) => d.kmsKeyName === void 0 || d.replication === "automatic" || (d.locations?.length ?? 0) === 1, {
2457
+ message: "a customer-managed key covers one location — use automatic replication, or a single location",
2458
+ path: ["kmsKeyName"]
2459
+ });
2460
+ /**
2461
+ * One LangGraph Platform (Agent Server) **deployment**, addressed by its id.
2462
+ *
2463
+ * A deployment is the whole unit here: its secrets are a property of the
2464
+ * deployment, delivered to the agent container as environment variables, and
2465
+ * there is nothing finer to point at — no per-revision or per-graph scope, and
2466
+ * no equivalent of Vercel's `production`/`preview` split. A deployment that
2467
+ * needs different values is a different deployment, so it is a different
2468
+ * binding.
2469
+ *
2470
+ * Validated as a UUID because `PATCH /v2/deployments/{deployment_id}` declares
2471
+ * the path parameter as one: a name or a URL slug in the slot fails validation
2472
+ * at the control plane hours later inside an alarm, with nobody watching. It is
2473
+ * the `id` from `GET /v2/deployments`, and the UUID in the deployment's
2474
+ * dashboard URL.
2475
+ */
2476
+ const langgraphPlatformDestinationSchema = z.object({
2477
+ provider: z.literal("langgraph-platform"),
2478
+ /** Deployment UUID, from the dashboard URL or `GET /v2/deployments`. */
2479
+ deploymentId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangGraph Platform deployment UUID")
2480
+ });
2481
+ const syncDestinationSchema = z.discriminatedUnion("provider", [
2482
+ vercelDestinationSchema,
2483
+ cloudflareWorkersDestinationSchema,
2484
+ cloudflarePagesDestinationSchema,
2485
+ cloudflareSecretsStoreDestinationSchema,
2486
+ railwayDestinationSchema,
2487
+ awsSecretsManagerDestinationSchema,
2488
+ awsParameterStoreDestinationSchema,
2489
+ renderDestinationSchema,
2490
+ flyDestinationSchema,
2491
+ northflankDestinationSchema,
2492
+ digitalOceanDestinationSchema,
2493
+ herokuDestinationSchema,
2494
+ netlifyDestinationSchema,
2495
+ bunnyshellDestinationSchema,
2496
+ githubActionsDestinationSchema,
2497
+ gcpSecretManagerDestinationSchema,
2498
+ langgraphPlatformDestinationSchema
2499
+ ]);
1084
2500
  /**
1085
2501
  * How seekrit secret names become destination key names. Applied in order:
1086
2502
  * explicit `rename` (wins outright), then `prefix`/`suffix`, then `case`.
@@ -1458,6 +2874,43 @@ async function generateDataKey(material, ref) {
1458
2874
  };
1459
2875
  }
1460
2876
  //#endregion
2877
+ //#region ../../packages/crypto/src/random.ts
2878
+ const ALPHABETS = {
2879
+ alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
2880
+ hex: "0123456789abcdef",
2881
+ base64url: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
2882
+ /**
2883
+ * Alphanumerics plus punctuation chosen to survive being pasted anywhere a
2884
+ * secret goes: no quote of either kind, no backslash, backtick, `$`, or
2885
+ * whitespace, so the value can't break out of a shell word, a SQL literal, a
2886
+ * URL component, or a `.env` line.
2887
+ */
2888
+ printable: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!*+="
2889
+ };
2890
+ /**
2891
+ * A cryptographically random string of `length` characters drawn uniformly from
2892
+ * `alphabet` (default `alphanumeric`, ≈5.95 bits/char — 32 chars ≈ 190 bits).
2893
+ *
2894
+ * Uses rejection sampling: bytes at or above the largest multiple of the
2895
+ * alphabet size are discarded rather than folded, so `% n` introduces no modulo
2896
+ * bias toward the low end of the alphabet.
2897
+ */
2898
+ function generateSecretValue(length, alphabet = "alphanumeric") {
2899
+ if (!Number.isInteger(length) || length < 1) throw new RangeError("length must be a positive integer");
2900
+ const chars = ALPHABETS[alphabet];
2901
+ const n = chars.length;
2902
+ const limit = 256 - 256 % n;
2903
+ let out = "";
2904
+ while (out.length < length) {
2905
+ const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
2906
+ for (const byte of bytes) {
2907
+ if (byte < limit) out += chars[byte % n];
2908
+ if (out.length === length) break;
2909
+ }
2910
+ }
2911
+ return out;
2912
+ }
2913
+ //#endregion
1461
2914
  //#region ../../packages/crypto/src/mysql.ts
1462
2915
  /**
1463
2916
  * Client-side construction of a MySQL/MariaDB `mysql_native_password`
@@ -1487,7 +2940,6 @@ async function generateDataKey(material, ref) {
1487
2940
  * SCRAM helper does, with no hand-rolled hash primitive.
1488
2941
  */
1489
2942
  const DEFAULT_PASSWORD_LENGTH$1 = 32;
1490
- const PASSWORD_ALPHABET$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1491
2943
  async function sha1(data) {
1492
2944
  return new Uint8Array(await crypto.subtle.digest("SHA-1", data));
1493
2945
  }
@@ -1496,17 +2948,6 @@ function toUpperHex(bytes) {
1496
2948
  for (const b of bytes) hex += b.toString(16).padStart(2, "0");
1497
2949
  return hex.toUpperCase();
1498
2950
  }
1499
- function randomPassword$1(length) {
1500
- let out = "";
1501
- while (out.length < length) {
1502
- const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
1503
- for (const byte of bytes) {
1504
- if (byte < 248) out += PASSWORD_ALPHABET$1[byte % 62];
1505
- if (out.length === length) break;
1506
- }
1507
- }
1508
- return out;
1509
- }
1510
2951
  /**
1511
2952
  * Compute the `mysql_native_password` auth string `*<UPPER(HEX(SHA1(SHA1(pw))))>`
1512
2953
  * for a known password. Pass the result straight to
@@ -1520,7 +2961,7 @@ async function mysqlNativePasswordVerifier(password) {
1520
2961
  * — the client-side half of a Vault-style dynamic MySQL credential.
1521
2962
  */
1522
2963
  async function generateMysqlCredential(options = {}) {
1523
- const password = randomPassword$1(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
2964
+ const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
1524
2965
  return {
1525
2966
  password,
1526
2967
  verifier: await mysqlNativePasswordVerifier(password)
@@ -1618,7 +3059,6 @@ const LOG = /* @__PURE__ */ new Uint8Array(256);
1618
3059
  }
1619
3060
  const SALT_LENGTH = 16;
1620
3061
  const DEFAULT_PASSWORD_LENGTH = 32;
1621
- const PASSWORD_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1622
3062
  async function hmacSha256(key, message) {
1623
3063
  const k = await crypto.subtle.importKey("raw", key, {
1624
3064
  name: "HMAC",
@@ -1639,17 +3079,6 @@ async function saltPassword(password, salt, iterations) {
1639
3079
  }, material, 256);
1640
3080
  return new Uint8Array(bits);
1641
3081
  }
1642
- function randomPassword(length) {
1643
- let out = "";
1644
- while (out.length < length) {
1645
- const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
1646
- for (const byte of bytes) {
1647
- if (byte < 248) out += PASSWORD_ALPHABET[byte % 62];
1648
- if (out.length === length) break;
1649
- }
1650
- }
1651
- return out;
1652
- }
1653
3082
  /**
1654
3083
  * Compute the `SCRAM-SHA-256$<i>:<salt>$<StoredKey>:<ServerKey>` verifier for a
1655
3084
  * known password. Pass the result straight to `CREATE ROLE … PASSWORD`.
@@ -1667,7 +3096,7 @@ async function scramSha256Verifier(password, options = {}) {
1667
3096
  * client-side half of a Vault-style dynamic Postgres credential.
1668
3097
  */
1669
3098
  async function generatePostgresCredential(options = {}) {
1670
- const password = randomPassword(options.length ?? DEFAULT_PASSWORD_LENGTH);
3099
+ const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH);
1671
3100
  const iterations = options.iterations ?? 4096;
1672
3101
  return {
1673
3102
  password,
@@ -1796,7 +3225,7 @@ function isServiceToken(value) {
1796
3225
  }
1797
3226
  //#endregion
1798
3227
  //#region ../cli/package.json
1799
- var version$1 = "0.32.0";
3228
+ var version$1 = "0.46.0";
1800
3229
  const PROJECT_FILE = "seekrit.json";
1801
3230
  function globalConfigPath() {
1802
3231
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -1997,6 +3426,10 @@ var SeekritClient = class {
1997
3426
  getEnv(orgId, envId) {
1998
3427
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}`);
1999
3428
  }
3429
+ /** Rename an environment (display name only — the slug is immutable). */
3430
+ updateEnv(orgId, envId, input) {
3431
+ return this.request("PATCH", `/v1/orgs/${orgId}/envs/${envId}`, input);
3432
+ }
2000
3433
  deleteEnv(orgId, envId) {
2001
3434
  return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}`);
2002
3435
  }
@@ -2150,6 +3583,10 @@ var SeekritClient = class {
2150
3583
  createToken(orgId, input) {
2151
3584
  return this.request("POST", `/v1/orgs/${orgId}/tokens`, input);
2152
3585
  }
3586
+ /** Rename a token. Role, environment binding, and expiry are immutable. */
3587
+ updateToken(orgId, tokenId, input) {
3588
+ return this.request("PATCH", `/v1/orgs/${orgId}/tokens/${tokenId}`, input);
3589
+ }
2153
3590
  revokeToken(orgId, tokenId) {
2154
3591
  return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
2155
3592
  }
@@ -2157,6 +3594,125 @@ var SeekritClient = class {
2157
3594
  deleteToken(orgId, tokenId) {
2158
3595
  return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}/permanent`);
2159
3596
  }
3597
+ listHoneyTokens(orgId) {
3598
+ return this.request("GET", `/v1/orgs/${orgId}/honey-tokens`);
3599
+ }
3600
+ createHoneyToken(orgId, input) {
3601
+ return this.request("POST", `/v1/orgs/${orgId}/honey-tokens`, input);
3602
+ }
3603
+ /** Delete a decoy outright — there is no access to revoke first. */
3604
+ deleteHoneyToken(orgId, honeyTokenId) {
3605
+ return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
3606
+ }
3607
+ listAgents(orgId) {
3608
+ return this.request("GET", `/v1/orgs/${orgId}/agents`);
3609
+ }
3610
+ createAgent(orgId, input) {
3611
+ return this.request("POST", `/v1/orgs/${orgId}/agents`, input);
3612
+ }
3613
+ getAgent(orgId, agentId) {
3614
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}`);
3615
+ }
3616
+ updateAgent(orgId, agentId, input) {
3617
+ return this.request("PATCH", `/v1/orgs/${orgId}/agents/${agentId}`, input);
3618
+ }
3619
+ deleteAgent(orgId, agentId) {
3620
+ return this.request("DELETE", `/v1/orgs/${orgId}/agents/${agentId}`);
3621
+ }
3622
+ /** Published versions, newest first. Append-only; nothing here is rewritten. */
3623
+ listAgentPolicies(orgId, agentId) {
3624
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/policies`);
3625
+ }
3626
+ /**
3627
+ * Publish a bundle signed in the browser.
3628
+ *
3629
+ * The signing happens client-side (`signAgentPolicy` in `@seekrit/core`) with
3630
+ * the publishing admin's own key, so the API receives an opaque envelope it
3631
+ * cannot forge. A version mismatch answers `409`: the version is inside the
3632
+ * signature, so a concurrent publish has to be re-signed, not patched.
3633
+ */
3634
+ publishAgentPolicy(orgId, agentId, bundle) {
3635
+ return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies`, { bundle });
3636
+ }
3637
+ /** Republish an earlier version's bundle as the newest version. */
3638
+ rollbackAgentPolicy(orgId, agentId, version) {
3639
+ return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies/${version}/rollback`);
3640
+ }
3641
+ /** The caller's own signing thumbprint, for the trust-anchor snippet. */
3642
+ getMyPolicySigner(orgId) {
3643
+ return this.request("GET", `/v1/orgs/${orgId}/agents/signers/me`);
3644
+ }
3645
+ /**
3646
+ * The bundle a proxy would see — `GET /v1/agents/:ref/policy`, the same route
3647
+ * `seekrit-proxy` polls, resolved by agent id or slug.
3648
+ *
3649
+ * Not org-scoped, because the caller is not: a proxy holds a service token that
3650
+ * knows an agent slug and nothing about org ids. Reachable with any service
3651
+ * token bound to the agent's org (or a user session), which is what lets
3652
+ * `seekrit proxy init` generate a config on the machine that holds the proxy's
3653
+ * own token rather than requiring an admin credential there.
3654
+ *
3655
+ * The `bundle` is signed and opaque to the API. Anything that *acts* on it must
3656
+ * verify the signature against locally pinned signers; decoding it for display
3657
+ * or to name a route is not acting on it.
3658
+ */
3659
+ getAgentPolicyBundle(agentRef) {
3660
+ return this.request("GET", `/v1/agents/${encodeURIComponent(agentRef)}/policy`);
3661
+ }
3662
+ /**
3663
+ * Dispatch a task for one agent run.
3664
+ *
3665
+ * The caller mints the token (`createAgentTaskToken` in `@seekrit/crypto`) and
3666
+ * sends only its hash plus the public `skd_…` segment, so no presentable
3667
+ * credential ever reaches this API — the same shape as service-token and CLI
3668
+ * session creation. `scopes` may only narrow what the agent's published policy
3669
+ * already permits; a name outside it is refused rather than dropped.
3670
+ *
3671
+ * Not org-scoped, because an orchestrator is not: it knows an agent slug.
3672
+ */
3673
+ dispatchAgentTask(agentRef, input) {
3674
+ return this.request("POST", `/v1/agents/${encodeURIComponent(agentRef)}/dispatch`, input);
3675
+ }
3676
+ /**
3677
+ * Exchange a presented token for the session it authorizes — what an
3678
+ * enforcement point calls once per task and caches until expiry.
3679
+ *
3680
+ * A POST because the token is a credential and must not land in a URL or an
3681
+ * access log. Fails closed and says which way: revoked, expired, or a disabled
3682
+ * identity are three different answers.
3683
+ */
3684
+ introspectAgentTask(token) {
3685
+ return this.request("POST", "/v1/tasks/introspect", { token });
3686
+ }
3687
+ /** End a run's authority now. Idempotent. */
3688
+ revokeAgentTask(taskId) {
3689
+ return this.request("POST", `/v1/tasks/${taskId}/revoke`);
3690
+ }
3691
+ getAgentTask(taskId) {
3692
+ return this.request("GET", `/v1/tasks/${taskId}`);
3693
+ }
3694
+ /** Runs dispatched for one identity, newest first (admin). */
3695
+ listAgentTasks(orgId, agentId) {
3696
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/tasks`);
3697
+ }
3698
+ /**
3699
+ * Report aggregate decisions. Called by an enforcement point, not a person.
3700
+ *
3701
+ * Counts only — hosts, methods, secret *names*, decisions, and rule indices.
3702
+ * Never a request path: see the module comment in `agent-activity.ts` for why
3703
+ * that line is drawn where it is.
3704
+ */
3705
+ reportAgentActivity(agentRef, input) {
3706
+ return this.request("POST", `/v1/agents/${encodeURIComponent(agentRef)}/activity`, input);
3707
+ }
3708
+ /**
3709
+ * What an agent actually did, collapsed onto its dimensions — the evidence a
3710
+ * grant review reasons over. The proposals themselves are computed client-side
3711
+ * (`reviewPolicy` in `@seekrit/core`), so the API never opines on policy.
3712
+ */
3713
+ getAgentActivity(orgId, agentId, days = 14) {
3714
+ return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/activity?days=${encodeURIComponent(String(days))}`);
3715
+ }
2160
3716
  /** Keys the caller can see: all org keys for admins, granted keys otherwise. */
2161
3717
  listKmsKeys(orgId) {
2162
3718
  return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
@@ -2267,6 +3823,39 @@ var SeekritClient = class {
2267
3823
  revokeLease(orgId, leaseId) {
2268
3824
  return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
2269
3825
  }
3826
+ /**
3827
+ * The rotator public key (the broker DO's), plus the environments that have
3828
+ * already granted it. Wrap an environment's DEK to this key client-side before
3829
+ * configuring rotation — that wrap IS the grant, and the server can't make it.
3830
+ */
3831
+ getRotatorKey(orgId) {
3832
+ return this.request("GET", `/v1/orgs/${orgId}/rotation/rotator-key`);
3833
+ }
3834
+ listRotations(orgId) {
3835
+ return this.request("GET", `/v1/orgs/${orgId}/rotation`);
3836
+ }
3837
+ getRotation(orgId, rotationId) {
3838
+ return this.request("GET", `/v1/orgs/${orgId}/rotation/${rotationId}`);
3839
+ }
3840
+ /**
3841
+ * Configure (or replace) a secret's rotation policy. `version` comes back only
3842
+ * when `rotateNow` was set — a rotated secret's new version number, never its
3843
+ * value.
3844
+ */
3845
+ configureRotation(orgId, input) {
3846
+ return this.request("POST", `/v1/orgs/${orgId}/rotation`, input);
3847
+ }
3848
+ updateRotation(orgId, rotationId, input) {
3849
+ return this.request("PATCH", `/v1/orgs/${orgId}/rotation/${rotationId}`, input);
3850
+ }
3851
+ /** Rotate now. Returns the new version — the value stays where it belongs. */
3852
+ rotateSecretNow(orgId, rotationId) {
3853
+ return this.request("POST", `/v1/orgs/${orgId}/rotation/${rotationId}/rotate`);
3854
+ }
3855
+ /** Disable rotation. `rotatorRevoked` reports whether the broker's key grant went too. */
3856
+ disableRotation(orgId, rotationId) {
3857
+ return this.request("DELETE", `/v1/orgs/${orgId}/rotation/${rotationId}`);
3858
+ }
2270
3859
  listAudit(orgId, query = {}) {
2271
3860
  const params = new URLSearchParams();
2272
3861
  if (query.cursor) params.set("cursor", query.cursor);
@@ -2276,6 +3865,17 @@ var SeekritClient = class {
2276
3865
  const qs = params.size > 0 ? `?${params}` : "";
2277
3866
  return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
2278
3867
  }
3868
+ /**
3869
+ * Export the org as one signed archive: every row seekrit holds for it, with
3870
+ * ciphertext still ciphertext (docs/break-glass-export.md).
3871
+ *
3872
+ * The archive comes back inline rather than as a job handle, and it can be
3873
+ * megabytes — buffer it to a file rather than holding several copies. Requires
3874
+ * admin; deliberately not entitlement-gated.
3875
+ */
3876
+ exportArchive(orgId, input = {}) {
3877
+ return this.request("POST", `/v1/orgs/${orgId}/export`, input);
3878
+ }
2279
3879
  getLogSink(orgId) {
2280
3880
  return this.request("GET", `/v1/orgs/${orgId}/log-sink`);
2281
3881
  }
@@ -2323,6 +3923,19 @@ var SeekritClient = class {
2323
3923
  cancelSubscription(orgId) {
2324
3924
  return this.request("POST", `/v1/orgs/${orgId}/billing/cancel`);
2325
3925
  }
3926
+ /**
3927
+ * Redeem a promo code, comping the org onto the plan the code grants.
3928
+ * Admin-only. Casing, spaces, and dashes are normalized server-side, so pass
3929
+ * the code as the user typed it. Returns the refreshed billing view.
3930
+ *
3931
+ * Every invalid code fails the same way regardless of why (unknown, expired,
3932
+ * fully redeemed, already used by this org) — the API deliberately won't
3933
+ * confirm that a code exists. Show the returned message as-is rather than
3934
+ * guessing at a more specific one.
3935
+ */
3936
+ redeemPromoCode(orgId, input) {
3937
+ return this.request("POST", `/v1/orgs/${orgId}/billing/promo`, input);
3938
+ }
2326
3939
  };
2327
3940
  //#endregion
2328
3941
  //#region ../cli/src/io.ts
@@ -3891,7 +5504,7 @@ async function runMcpServer(options = {}) {
3891
5504
  * `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
3892
5505
  * published package is self-contained and needs no `@seekrit/cli` install.
3893
5506
  */
3894
- runMcpServer({ version: "0.6.1" }).catch((err) => {
5507
+ runMcpServer({ version: "0.8.0" }).catch((err) => {
3895
5508
  const message = err instanceof Error ? err.message : String(err);
3896
5509
  process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
3897
5510
  process.exit(1);