@seekrit/cli 0.39.0 → 0.40.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 +546 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,6 +28,12 @@ const ENTITLEMENT_KEYS = Object.keys({
28
28
  description: "Vault-style short-lived database and cloud credentials.",
29
29
  default: true
30
30
  },
31
+ "feature.rotation": {
32
+ kind: "feature",
33
+ label: "Secret rotation",
34
+ description: "Managed, scheduled rotation of stored credentials.",
35
+ default: true
36
+ },
31
37
  "feature.log_sink": {
32
38
  kind: "feature",
33
39
  label: "Audit log export (SIEM)",
@@ -106,6 +112,12 @@ const ENTITLEMENT_KEYS = Object.keys({
106
112
  description: "Maximum registered third-party sync destinations.",
107
113
  default: null
108
114
  },
115
+ "rotation.policies.max": {
116
+ kind: "limit",
117
+ label: "Rotation policies",
118
+ description: "Maximum secrets with managed rotation configured.",
119
+ default: null
120
+ },
109
121
  members: {
110
122
  kind: "metered",
111
123
  label: "Members",
@@ -754,7 +766,7 @@ const connectionSchema = z.object({
754
766
  database: z.string().min(1)
755
767
  });
756
768
  /** A `{{name}}`/`{{verifier}}`/`{{valid_until}}` templated SQL statement. */
757
- const statementSchema = z.string().min(1).max(4e3);
769
+ const statementSchema$1 = z.string().min(1).max(4e3);
758
770
  /** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
759
771
  const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
760
772
  const postgresTargetConfigSchema = z.object({
@@ -764,20 +776,20 @@ const postgresTargetConfigSchema = z.object({
764
776
  schema: identifierSchema.optional(),
765
777
  connection: connectionSchema,
766
778
  provisionerUrl: z.url().optional(),
767
- createStatements: z.array(statementSchema).max(16).optional(),
768
- revokeStatements: z.array(statementSchema).max(16).optional()
779
+ createStatements: z.array(statementSchema$1).max(16).optional(),
780
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
769
781
  });
770
782
  /** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
771
- const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
783
+ const mysqlHostSchema$1 = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
772
784
  const mysqlTargetConfigSchema = z.object({
773
785
  provider: z.literal("mysql"),
774
786
  executor: executorModeSchema,
775
787
  accessLevel: mysqlAccessLevelSchema.optional(),
776
788
  connection: connectionSchema,
777
- userHost: mysqlHostSchema.optional(),
789
+ userHost: mysqlHostSchema$1.optional(),
778
790
  provisionerUrl: z.url().optional(),
779
- createStatements: z.array(statementSchema).max(16).optional(),
780
- revokeStatements: z.array(statementSchema).max(16).optional()
791
+ createStatements: z.array(statementSchema$1).max(16).optional(),
792
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
781
793
  });
782
794
  const redisConnectionSchema = z.object({
783
795
  host: z.string().min(1),
@@ -791,8 +803,8 @@ const redisTargetConfigSchema = z.object({
791
803
  accessLevel: redisAccessLevelSchema.optional(),
792
804
  connection: redisConnectionSchema,
793
805
  provisionerUrl: z.url().optional(),
794
- createStatements: z.array(statementSchema).max(16).optional(),
795
- revokeStatements: z.array(statementSchema).max(16).optional()
806
+ createStatements: z.array(statementSchema$1).max(16).optional(),
807
+ revokeStatements: z.array(statementSchema$1).max(16).optional()
796
808
  });
797
809
  const sshTargetConfigSchema = z.object({
798
810
  provider: z.literal("ssh"),
@@ -1122,6 +1134,11 @@ const AUDIT_ACTIONS = [
1122
1134
  "secret.deleted",
1123
1135
  "secret.read",
1124
1136
  "secret.restored",
1137
+ "secret.rotation_configured",
1138
+ "secret.rotation_updated",
1139
+ "secret.rotation_disabled",
1140
+ "secret.rotated",
1141
+ "secret.rotation_failed",
1125
1142
  "token.created",
1126
1143
  "token.revoked",
1127
1144
  "token.deleted",
@@ -1147,6 +1164,9 @@ const AUDIT_ACTIONS = [
1147
1164
  "entitlement.override_cleared",
1148
1165
  "billing.checkout_started",
1149
1166
  "billing.portal_opened",
1167
+ "promo.created",
1168
+ "promo.updated",
1169
+ "promo.redeemed",
1150
1170
  "kms.key_created",
1151
1171
  "kms.key_granted",
1152
1172
  "kms.key_revoked",
@@ -1187,7 +1207,8 @@ const NOTIFICATION_TYPES = [
1187
1207
  "token_expiring",
1188
1208
  "lease_expired",
1189
1209
  "sync_failed",
1190
- "honey_token_tripped"
1210
+ "honey_token_tripped",
1211
+ "secret_rotation_failed"
1191
1212
  ];
1192
1213
  /** UI-facing copy + default for each notification type. */
1193
1214
  const NOTIFICATION_TYPE_META = {
@@ -1240,6 +1261,11 @@ const NOTIFICATION_TYPE_META = {
1240
1261
  label: "Honey token tripped",
1241
1262
  description: "Someone tried to use one of your organization's decoy credentials (throttled per token). Nothing legitimate holds one, so every trip is worth reading.",
1242
1263
  defaultEnabled: true
1264
+ },
1265
+ secret_rotation_failed: {
1266
+ label: "Secret rotation failed",
1267
+ description: "A scheduled secret rotation could not complete — the stored value may be out of step with the system it authenticates to.",
1268
+ defaultEnabled: true
1243
1269
  }
1244
1270
  };
1245
1271
  //#endregion
@@ -1467,6 +1493,25 @@ z.object({
1467
1493
  note: z.string().max(500).nullish(),
1468
1494
  expiresAt: z.iso.datetime().nullish()
1469
1495
  });
1496
+ z.object({
1497
+ code: z.string().min(4).max(40),
1498
+ family: planFamilySchema,
1499
+ version: z.number().int().positive().optional(),
1500
+ durationDays: z.number().int().positive().max(3650).nullish(),
1501
+ maxRedemptions: z.number().int().positive().nullish(),
1502
+ startsAt: z.iso.datetime().nullish(),
1503
+ endsAt: z.iso.datetime().nullish(),
1504
+ note: z.string().max(500).nullish()
1505
+ });
1506
+ z.object({
1507
+ maxRedemptions: z.number().int().positive().nullish(),
1508
+ startsAt: z.iso.datetime().nullish(),
1509
+ endsAt: z.iso.datetime().nullish(),
1510
+ note: z.string().max(500).nullish(),
1511
+ /** Kill switch. Disabling stops new redemptions; live grants are untouched. */
1512
+ disabled: z.boolean().optional()
1513
+ });
1514
+ z.object({ code: z.string().min(1).max(40) });
1470
1515
  z.object({ family: planFamilySchema });
1471
1516
  z.object({
1472
1517
  sessionId: z.string().regex(/^skc_[0-9A-Za-z]+$/),
@@ -1484,6 +1529,99 @@ z.object({
1484
1529
  action: z.string().optional(),
1485
1530
  resourceType: z.string().optional()
1486
1531
  });
1532
+ z.enum([
1533
+ "generated",
1534
+ "postgres",
1535
+ "mysql",
1536
+ "redis"
1537
+ ]);
1538
+ z.enum([
1539
+ "active",
1540
+ "paused",
1541
+ "failed"
1542
+ ]);
1543
+ /** Statuses an admin may set directly (`failed` is only reached by the sweep). */
1544
+ const settableRotationStatusSchema = z.enum(["active", "paused"]);
1545
+ const rotationAlphabetSchema = z.enum([
1546
+ "alphanumeric",
1547
+ "hex",
1548
+ "base64url",
1549
+ "printable"
1550
+ ]);
1551
+ const rotationIntervalSchema = z.number().int().min(300).max(3600 * 24 * 365);
1552
+ /**
1553
+ * A database user name we are willing to *re-key*. Deliberately more permissive
1554
+ * than the lease providers' name schemas — those name accounts seekrit creates,
1555
+ * whereas this names an account the customer's DBA created years ago, which may
1556
+ * be mixed-case or contain dots or dashes.
1557
+ *
1558
+ * It stays injection-safe for every place it is interpolated: a double-quoted
1559
+ * Postgres identifier, a single-quoted MySQL literal, and a bare Redis command
1560
+ * token. The charset excludes both quote characters, backslash, whitespace, and
1561
+ * `;`, so there is no way out of the surrounding quoting, and no whitespace to
1562
+ * split one Redis argument into two.
1563
+ */
1564
+ const rotationUsernameSchema = z.string().regex(/^[A-Za-z0-9_$.-]{1,63}$/, "must be 1–63 chars of letters, digits, underscore, dollar, dot or dash");
1565
+ /** A `{{name}}`/`{{host}}`/`{{verifier}}` templated statement or command line. */
1566
+ const statementSchema = z.string().min(1).max(4e3);
1567
+ const passwordLengthSchema = z.number().int().min(16).max(256);
1568
+ /** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
1569
+ const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
1570
+ const generatedRotationConfigSchema = z.object({
1571
+ kind: z.literal("generated"),
1572
+ length: passwordLengthSchema.optional(),
1573
+ alphabet: rotationAlphabetSchema.optional()
1574
+ });
1575
+ const postgresRotationConfigSchema = z.object({
1576
+ kind: z.literal("postgres"),
1577
+ username: rotationUsernameSchema,
1578
+ passwordLength: passwordLengthSchema.optional(),
1579
+ statements: z.array(statementSchema).max(16).optional()
1580
+ });
1581
+ const mysqlRotationConfigSchema = z.object({
1582
+ kind: z.literal("mysql"),
1583
+ username: rotationUsernameSchema,
1584
+ userHost: mysqlHostSchema.optional(),
1585
+ passwordLength: passwordLengthSchema.optional(),
1586
+ statements: z.array(statementSchema).max(16).optional()
1587
+ });
1588
+ const redisRotationConfigSchema = z.object({
1589
+ kind: z.literal("redis"),
1590
+ username: rotationUsernameSchema,
1591
+ passwordLength: passwordLengthSchema.optional(),
1592
+ statements: z.array(statementSchema).max(16).optional()
1593
+ });
1594
+ const rotationConfigSchema = z.discriminatedUnion("kind", [
1595
+ generatedRotationConfigSchema,
1596
+ postgresRotationConfigSchema,
1597
+ mysqlRotationConfigSchema,
1598
+ redisRotationConfigSchema
1599
+ ]);
1600
+ z.object({
1601
+ environmentId: z.string().min(1),
1602
+ /** The secret whose value rotates. It must already exist. */
1603
+ secretName: secretNameSchema,
1604
+ config: rotationConfigSchema,
1605
+ intervalSeconds: rotationIntervalSchema,
1606
+ /**
1607
+ * The registered lease target supplying the connection, executor mode, and
1608
+ * wrapped admin credential. Required for every kind but `generated`.
1609
+ */
1610
+ targetId: z.string().min(1).optional(),
1611
+ /** Environment DEK wrapped to the rotator public key (`wd1.` blob). */
1612
+ wrappedDek: z.string().min(1).optional(),
1613
+ /** Rotate once immediately instead of waiting for the first interval. */
1614
+ rotateNow: z.boolean().optional()
1615
+ });
1616
+ z.object({
1617
+ intervalSeconds: rotationIntervalSchema.optional(),
1618
+ config: rotationConfigSchema.optional(),
1619
+ /**
1620
+ * `paused` stops the sweep; `active` resumes it and clears the failure
1621
+ * streak, which is also how a `failed` policy is recovered.
1622
+ */
1623
+ status: settableRotationStatusSchema.optional()
1624
+ }).refine((v) => v.intervalSeconds !== void 0 || v.config !== void 0 || v.status !== void 0, "provide at least one of intervalSeconds, config, or status");
1487
1625
  //#endregion
1488
1626
  //#region ../../packages/core/src/sync.ts
1489
1627
  /**
@@ -1525,7 +1663,8 @@ const SYNC_PROVIDER_KINDS = [
1525
1663
  "fly",
1526
1664
  "northflank",
1527
1665
  "digitalocean",
1528
- "heroku"
1666
+ "heroku",
1667
+ "netlify"
1529
1668
  ];
1530
1669
  z.enum(SYNC_PROVIDER_KINDS);
1531
1670
  /**
@@ -1711,6 +1850,25 @@ const digitalOceanConnectionConfigSchema = z.object({ provider: z.literal("digit
1711
1850
  * not.
1712
1851
  */
1713
1852
  const herokuConnectionConfigSchema = z.object({ provider: z.literal("heroku") });
1853
+ /**
1854
+ * Netlify team scope — the one thing a Netlify token cannot tell us itself.
1855
+ *
1856
+ * Every environment variable endpoint is account-scoped
1857
+ * (`/accounts/{account_id}/env`), and a personal access token belongs to a
1858
+ * *user*, who may sit in several teams. So unlike Fly's or Heroku's, this
1859
+ * config is not empty: the token says who you are, and this says which team's
1860
+ * variables to write.
1861
+ *
1862
+ * Netlify treats the team's id and its slug as interchangeable wherever
1863
+ * `{account_id}` appears, so both are accepted. The slug is the one an operator
1864
+ * can find without an API call — it is in the dashboard URL
1865
+ * (`app.netlify.com/teams/<slug>`) and under Team settings → General.
1866
+ */
1867
+ const netlifyConnectionConfigSchema = z.object({
1868
+ provider: z.literal("netlify"),
1869
+ /** Netlify team slug (`acme`) or account id — `{account_id}` accepts either. */
1870
+ 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")
1871
+ });
1714
1872
  const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1715
1873
  vercelConnectionConfigSchema,
1716
1874
  cloudflareWorkersConnectionConfigSchema,
@@ -1723,7 +1881,8 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1723
1881
  flyConnectionConfigSchema,
1724
1882
  northflankConnectionConfigSchema,
1725
1883
  digitalOceanConnectionConfigSchema,
1726
- herokuConnectionConfigSchema
1884
+ herokuConnectionConfigSchema,
1885
+ netlifyConnectionConfigSchema
1727
1886
  ]);
1728
1887
  /** Vercel's three deployment targets. A binding writes to one or more. */
1729
1888
  const VERCEL_TARGETS = [
@@ -2078,6 +2237,76 @@ const herokuDestinationSchema = z.object({
2078
2237
  /** App name as `heroku apps` prints it, or the app's UUID. */
2079
2238
  app: herokuAppSchema
2080
2239
  });
2240
+ /**
2241
+ * The deploy contexts a Netlify value can be set for.
2242
+ *
2243
+ * These are Netlify's own, minus two. `all` is missing deliberately: Netlify
2244
+ * requires a **secret** value to be set against explicit contexts, and its
2245
+ * `setEnvVarValue` endpoint is reported to fail outright on `context: "all"` —
2246
+ * so the union offers only contexts that work under both. Naming the contexts
2247
+ * you mean is what you want here anyway; a binding already exists to map one
2248
+ * seekrit environment onto one deploy context. `dev-server` (Preview Server) is
2249
+ * left out for want of anyone asking.
2250
+ *
2251
+ * `branch` is the odd one: it needs a branch name alongside it, which the
2252
+ * destination carries as {@link netlifyDestinationSchema}'s `branch`.
2253
+ */
2254
+ const NETLIFY_CONTEXTS = [
2255
+ "production",
2256
+ "deploy-preview",
2257
+ "branch-deploy",
2258
+ "branch",
2259
+ "dev"
2260
+ ];
2261
+ /**
2262
+ * A Netlify site, by its **API ID** — the UUID under Project configuration →
2263
+ * General → Project information.
2264
+ *
2265
+ * Netlify accepts a site's domain in place of its id where a site appears in a
2266
+ * *path* (`/sites/{site_id}`), but the environment variable endpoints take the
2267
+ * site as a `?site_id=` **query parameter** instead, and Netlify documents no
2268
+ * name resolution there. That asymmetry is why this is strict where the Heroku
2269
+ * and Fly destinations are permissive: a `site_id` Netlify does not resolve
2270
+ * does not 404 — the write lands on the *team*, as a shared variable inherited
2271
+ * by every site in it. Refusing anything but the UUID keeps a slip from turning
2272
+ * into a much wider blast radius than the operator asked for.
2273
+ */
2274
+ 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");
2275
+ /**
2276
+ * The Netlify site, and which of its deploy contexts a binding owns.
2277
+ *
2278
+ * Netlify keys a value by (site, context), so the destination is that pair.
2279
+ * Contexts are a list rather than a single value for the same reason Vercel's
2280
+ * `targets` is one: a binding is unique per (connection, environment), so an
2281
+ * environment that feeds both production and deploy previews has to say so in
2282
+ * one destination or not at all.
2283
+ *
2284
+ * A push writes **only** the contexts listed here. Other contexts of the same
2285
+ * variable — and every variable this binding does not manage — are left as they
2286
+ * are, which is what makes it safe to point at a site that already has
2287
+ * variables set by hand.
2288
+ *
2289
+ * `secret` marks what seekrit creates as a Netlify **secret**: write-only, and
2290
+ * unreadable afterwards through the UI, CLI, and API. On by default, since that
2291
+ * is the whole point of pushing from a secrets manager. It applies only to
2292
+ * variables seekrit *creates* — Netlify will not let a flag be added to an
2293
+ * existing variable, or removed from one ever — and it needs a plan that
2294
+ * includes Secrets Controller.
2295
+ */
2296
+ const netlifyDestinationSchema = z.object({
2297
+ provider: z.literal("netlify"),
2298
+ /** Site API ID (a UUID), from Project configuration → General. */
2299
+ siteId: netlifySiteIdSchema,
2300
+ /** Which deploy contexts receive these values. At least one. */
2301
+ contexts: z.array(z.enum(NETLIFY_CONTEXTS)).min(1),
2302
+ /** Branch name, required when `contexts` includes `branch`; ignored otherwise. */
2303
+ branch: z.string().trim().min(1).max(255).optional(),
2304
+ /** Create variables as Netlify secrets (default true). */
2305
+ secret: z.boolean().optional()
2306
+ }).refine((d) => !d.contexts.includes("branch") || d.branch !== void 0, {
2307
+ message: "a branch context needs the branch name it applies to",
2308
+ path: ["branch"]
2309
+ });
2081
2310
  const syncDestinationSchema = z.discriminatedUnion("provider", [
2082
2311
  vercelDestinationSchema,
2083
2312
  cloudflareWorkersDestinationSchema,
@@ -2090,7 +2319,8 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
2090
2319
  flyDestinationSchema,
2091
2320
  northflankDestinationSchema,
2092
2321
  digitalOceanDestinationSchema,
2093
- herokuDestinationSchema
2322
+ herokuDestinationSchema,
2323
+ netlifyDestinationSchema
2094
2324
  ]);
2095
2325
  /**
2096
2326
  * How seekrit secret names become destination key names. Applied in order:
@@ -2539,6 +2769,43 @@ async function decryptDataKey(material, wrapped) {
2539
2769
  }
2540
2770
  }
2541
2771
  //#endregion
2772
+ //#region ../../packages/crypto/src/random.ts
2773
+ const ALPHABETS = {
2774
+ alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
2775
+ hex: "0123456789abcdef",
2776
+ base64url: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
2777
+ /**
2778
+ * Alphanumerics plus punctuation chosen to survive being pasted anywhere a
2779
+ * secret goes: no quote of either kind, no backslash, backtick, `$`, or
2780
+ * whitespace, so the value can't break out of a shell word, a SQL literal, a
2781
+ * URL component, or a `.env` line.
2782
+ */
2783
+ printable: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!*+="
2784
+ };
2785
+ /**
2786
+ * A cryptographically random string of `length` characters drawn uniformly from
2787
+ * `alphabet` (default `alphanumeric`, ≈5.95 bits/char — 32 chars ≈ 190 bits).
2788
+ *
2789
+ * Uses rejection sampling: bytes at or above the largest multiple of the
2790
+ * alphabet size are discarded rather than folded, so `% n` introduces no modulo
2791
+ * bias toward the low end of the alphabet.
2792
+ */
2793
+ function generateSecretValue(length, alphabet = "alphanumeric") {
2794
+ if (!Number.isInteger(length) || length < 1) throw new RangeError("length must be a positive integer");
2795
+ const chars = ALPHABETS[alphabet];
2796
+ const n = chars.length;
2797
+ const limit = 256 - 256 % n;
2798
+ let out = "";
2799
+ while (out.length < length) {
2800
+ const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
2801
+ for (const byte of bytes) {
2802
+ if (byte < limit) out += chars[byte % n];
2803
+ if (out.length === length) break;
2804
+ }
2805
+ }
2806
+ return out;
2807
+ }
2808
+ //#endregion
2542
2809
  //#region ../../packages/crypto/src/mongodb.ts
2543
2810
  /** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
2544
2811
  async function generateMongoRecipientKeyPair() {
@@ -2603,7 +2870,6 @@ function mongoConnectionUri(cred) {
2603
2870
  * SCRAM helper does, with no hand-rolled hash primitive.
2604
2871
  */
2605
2872
  const DEFAULT_PASSWORD_LENGTH$2 = 32;
2606
- const PASSWORD_ALPHABET$2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2607
2873
  async function sha1(data) {
2608
2874
  return new Uint8Array(await crypto.subtle.digest("SHA-1", data));
2609
2875
  }
@@ -2612,17 +2878,6 @@ function toUpperHex(bytes) {
2612
2878
  for (const b of bytes) hex += b.toString(16).padStart(2, "0");
2613
2879
  return hex.toUpperCase();
2614
2880
  }
2615
- function randomPassword$2(length) {
2616
- let out = "";
2617
- while (out.length < length) {
2618
- const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
2619
- for (const byte of bytes) {
2620
- if (byte < 248) out += PASSWORD_ALPHABET$2[byte % 62];
2621
- if (out.length === length) break;
2622
- }
2623
- }
2624
- return out;
2625
- }
2626
2881
  /**
2627
2882
  * Compute the `mysql_native_password` auth string `*<UPPER(HEX(SHA1(SHA1(pw))))>`
2628
2883
  * for a known password. Pass the result straight to
@@ -2636,7 +2891,7 @@ async function mysqlNativePasswordVerifier(password) {
2636
2891
  * — the client-side half of a Vault-style dynamic MySQL credential.
2637
2892
  */
2638
2893
  async function generateMysqlCredential(options = {}) {
2639
- const password = randomPassword$2(options.length ?? DEFAULT_PASSWORD_LENGTH$2);
2894
+ const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH$2);
2640
2895
  return {
2641
2896
  password,
2642
2897
  verifier: await mysqlNativePasswordVerifier(password)
@@ -2916,7 +3171,6 @@ async function combineRecoveryShares(shareBytes) {
2916
3171
  * helper does, with no hand-rolled hash primitive.
2917
3172
  */
2918
3173
  const DEFAULT_PASSWORD_LENGTH$1 = 32;
2919
- const PASSWORD_ALPHABET$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2920
3174
  async function sha256$1(data) {
2921
3175
  return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
2922
3176
  }
@@ -2925,17 +3179,6 @@ function toLowerHex(bytes) {
2925
3179
  for (const b of bytes) hex += b.toString(16).padStart(2, "0");
2926
3180
  return hex;
2927
3181
  }
2928
- function randomPassword$1(length) {
2929
- let out = "";
2930
- while (out.length < length) {
2931
- const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
2932
- for (const byte of bytes) {
2933
- if (byte < 248) out += PASSWORD_ALPHABET$1[byte % 62];
2934
- if (out.length === length) break;
2935
- }
2936
- }
2937
- return out;
2938
- }
2939
3182
  /**
2940
3183
  * Compute the Redis ACL password verifier `LOWER(HEX(SHA256(password)))` for a
2941
3184
  * known password. Pass the result straight to `ACL SETUSER … on #<verifier>`.
@@ -2948,7 +3191,7 @@ async function redisSha256Verifier(password) {
2948
3191
  * client-side half of a Vault-style dynamic Redis credential.
2949
3192
  */
2950
3193
  async function generateRedisCredential(options = {}) {
2951
- const password = randomPassword$1(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
3194
+ const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
2952
3195
  return {
2953
3196
  password,
2954
3197
  verifier: await redisSha256Verifier(password)
@@ -2956,7 +3199,6 @@ async function generateRedisCredential(options = {}) {
2956
3199
  }
2957
3200
  const SALT_LENGTH = 16;
2958
3201
  const DEFAULT_PASSWORD_LENGTH = 32;
2959
- const PASSWORD_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2960
3202
  async function hmacSha256(key, message) {
2961
3203
  const k = await crypto.subtle.importKey("raw", key, {
2962
3204
  name: "HMAC",
@@ -2977,17 +3219,6 @@ async function saltPassword(password, salt, iterations) {
2977
3219
  }, material, 256);
2978
3220
  return new Uint8Array(bits);
2979
3221
  }
2980
- function randomPassword(length) {
2981
- let out = "";
2982
- while (out.length < length) {
2983
- const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
2984
- for (const byte of bytes) {
2985
- if (byte < 248) out += PASSWORD_ALPHABET[byte % 62];
2986
- if (out.length === length) break;
2987
- }
2988
- }
2989
- return out;
2990
- }
2991
3222
  /**
2992
3223
  * Compute the `SCRAM-SHA-256$<i>:<salt>$<StoredKey>:<ServerKey>` verifier for a
2993
3224
  * known password. Pass the result straight to `CREATE ROLE … PASSWORD`.
@@ -3005,7 +3236,7 @@ async function scramSha256Verifier(password, options = {}) {
3005
3236
  * client-side half of a Vault-style dynamic Postgres credential.
3006
3237
  */
3007
3238
  async function generatePostgresCredential(options = {}) {
3008
- const password = randomPassword(options.length ?? DEFAULT_PASSWORD_LENGTH);
3239
+ const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH);
3009
3240
  const iterations = options.iterations ?? 4096;
3010
3241
  return {
3011
3242
  password,
@@ -3295,7 +3526,7 @@ function isCliSessionToken(value) {
3295
3526
  }
3296
3527
  //#endregion
3297
3528
  //#region package.json
3298
- var version = "0.39.0";
3529
+ var version = "0.40.0";
3299
3530
  //#endregion
3300
3531
  //#region ../../packages/api-client/src/index.ts
3301
3532
  var SeekritApiError = class extends Error {
@@ -3727,6 +3958,39 @@ var SeekritClient = class {
3727
3958
  revokeLease(orgId, leaseId) {
3728
3959
  return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
3729
3960
  }
3961
+ /**
3962
+ * The rotator public key (the broker DO's), plus the environments that have
3963
+ * already granted it. Wrap an environment's DEK to this key client-side before
3964
+ * configuring rotation — that wrap IS the grant, and the server can't make it.
3965
+ */
3966
+ getRotatorKey(orgId) {
3967
+ return this.request("GET", `/v1/orgs/${orgId}/rotation/rotator-key`);
3968
+ }
3969
+ listRotations(orgId) {
3970
+ return this.request("GET", `/v1/orgs/${orgId}/rotation`);
3971
+ }
3972
+ getRotation(orgId, rotationId) {
3973
+ return this.request("GET", `/v1/orgs/${orgId}/rotation/${rotationId}`);
3974
+ }
3975
+ /**
3976
+ * Configure (or replace) a secret's rotation policy. `version` comes back only
3977
+ * when `rotateNow` was set — a rotated secret's new version number, never its
3978
+ * value.
3979
+ */
3980
+ configureRotation(orgId, input) {
3981
+ return this.request("POST", `/v1/orgs/${orgId}/rotation`, input);
3982
+ }
3983
+ updateRotation(orgId, rotationId, input) {
3984
+ return this.request("PATCH", `/v1/orgs/${orgId}/rotation/${rotationId}`, input);
3985
+ }
3986
+ /** Rotate now. Returns the new version — the value stays where it belongs. */
3987
+ rotateSecretNow(orgId, rotationId) {
3988
+ return this.request("POST", `/v1/orgs/${orgId}/rotation/${rotationId}/rotate`);
3989
+ }
3990
+ /** Disable rotation. `rotatorRevoked` reports whether the broker's key grant went too. */
3991
+ disableRotation(orgId, rotationId) {
3992
+ return this.request("DELETE", `/v1/orgs/${orgId}/rotation/${rotationId}`);
3993
+ }
3730
3994
  listAudit(orgId, query = {}) {
3731
3995
  const params = new URLSearchParams();
3732
3996
  if (query.cursor) params.set("cursor", query.cursor);
@@ -3783,6 +4047,19 @@ var SeekritClient = class {
3783
4047
  cancelSubscription(orgId) {
3784
4048
  return this.request("POST", `/v1/orgs/${orgId}/billing/cancel`);
3785
4049
  }
4050
+ /**
4051
+ * Redeem a promo code, comping the org onto the plan the code grants.
4052
+ * Admin-only. Casing, spaces, and dashes are normalized server-side, so pass
4053
+ * the code as the user typed it. Returns the refreshed billing view.
4054
+ *
4055
+ * Every invalid code fails the same way regardless of why (unknown, expired,
4056
+ * fully redeemed, already used by this org) — the API deliberately won't
4057
+ * confirm that a code exists. Show the returned message as-is rather than
4058
+ * guessing at a more specific one.
4059
+ */
4060
+ redeemPromoCode(orgId, input) {
4061
+ return this.request("POST", `/v1/orgs/${orgId}/billing/promo`, input);
4062
+ }
3786
4063
  };
3787
4064
  async function unauthenticatedPost(baseUrl, path, body, fetchImpl, client) {
3788
4065
  const headers = {
@@ -6770,6 +7047,184 @@ function collect$1(value, acc) {
6770
7047
  acc.push(value);
6771
7048
  return acc;
6772
7049
  }
7050
+ //#endregion
7051
+ //#region src/rotation.ts
7052
+ /**
7053
+ * `seekrit rotation` — managed, scheduled rotation of a stored secret's value.
7054
+ *
7055
+ * Zero-knowledge: enabling rotation unwraps this environment's DEK **on this
7056
+ * machine** and re-wraps it to the rotator public key (the per-org broker
7057
+ * Durable Object's), so the control plane only ever relays ciphertext. That wrap
7058
+ * is what lets the broker write a new value in place — and it is exactly why the
7059
+ * server can't enable rotation on its own.
7060
+ *
7061
+ * Rotated values are never printed here. Read them like any other secret
7062
+ * (`seekrit secrets get NAME`), which decrypts locally.
7063
+ */
7064
+ /** Parse a duration like `30m`, `24h`, `90d`, or a bare seconds count. */
7065
+ function parseDurationSeconds(input, flag) {
7066
+ const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
7067
+ if (!m) fail(`invalid ${flag} "${input}" (try 12h, 7d, 90d)`);
7068
+ return Number(m[1]) * ({
7069
+ s: 1,
7070
+ m: 60,
7071
+ h: 3600,
7072
+ d: 86400
7073
+ }[m[2] || "s"] ?? 1);
7074
+ }
7075
+ function formatInterval(seconds) {
7076
+ if (seconds % 86400 === 0) return `${seconds / 86400}d`;
7077
+ if (seconds % 3600 === 0) return `${seconds / 3600}h`;
7078
+ if (seconds % 60 === 0) return `${seconds / 60}m`;
7079
+ return `${seconds}s`;
7080
+ }
7081
+ /** One policy as a tab-separated line: name, kind, cadence, status, next run. */
7082
+ function rotationLine(r) {
7083
+ const status = r.failureCount > 0 ? `${r.status} (${r.failureCount} failed)` : r.status;
7084
+ return [
7085
+ r.id,
7086
+ r.secretName,
7087
+ r.kind,
7088
+ `every ${formatInterval(r.intervalSeconds)}`,
7089
+ status,
7090
+ `next ${r.nextRotateAt}`
7091
+ ].join(" ");
7092
+ }
7093
+ /** Find a policy by id, or by secret name when it is unambiguous in the org. */
7094
+ async function resolveRotation(ctx, orgId, ref) {
7095
+ const { rotations } = await ctx.client.listRotations(orgId);
7096
+ const byId = rotations.find((r) => r.id === ref);
7097
+ if (byId) return byId;
7098
+ const byName = rotations.filter((r) => r.secretName === ref);
7099
+ if (byName.length === 1) return byName[0];
7100
+ if (byName.length > 1) fail(`"${ref}" rotates in ${byName.length} environments — pass the rotation id instead:\n${byName.map((r) => ` ${r.id}\t${r.environmentId}`).join("\n")}`);
7101
+ fail(`no rotation policy "${ref}"`);
7102
+ }
7103
+ /** Build the rotation config from the CLI flags for a given kind. */
7104
+ function buildConfig$1(kind, options) {
7105
+ const length = Number.parseInt(options.length, 10);
7106
+ if (!Number.isFinite(length)) fail(`invalid --length "${options.length}"`);
7107
+ if (kind === "generated") {
7108
+ const alphabets = [
7109
+ "alphanumeric",
7110
+ "hex",
7111
+ "base64url",
7112
+ "printable"
7113
+ ];
7114
+ if (!alphabets.includes(options.alphabet)) fail(`--alphabet must be one of ${alphabets.join(", ")}`);
7115
+ return {
7116
+ kind: "generated",
7117
+ length,
7118
+ alphabet: options.alphabet
7119
+ };
7120
+ }
7121
+ if (!options.username) fail(`--username is required for a ${kind} rotation`);
7122
+ const username = options.username;
7123
+ if (kind === "postgres") return {
7124
+ kind: "postgres",
7125
+ username,
7126
+ passwordLength: length
7127
+ };
7128
+ if (kind === "mysql") return {
7129
+ kind: "mysql",
7130
+ username,
7131
+ passwordLength: length,
7132
+ ...options.userHost ? { userHost: options.userHost } : {}
7133
+ };
7134
+ if (kind === "redis") return {
7135
+ kind: "redis",
7136
+ username,
7137
+ passwordLength: length
7138
+ };
7139
+ return fail(`--kind must be generated, postgres, mysql, or redis (got "${kind}")`);
7140
+ }
7141
+ function registerRotationCommands(program) {
7142
+ const rotation = program.command("rotation").description("managed rotation of stored secret values (scheduled, zero-knowledge)");
7143
+ rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>").option("--app <slug>").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
7144
+ const ctx = buildContext();
7145
+ const target = await resolveEnvTarget(ctx, options);
7146
+ const config = buildConfig$1(options.kind, options);
7147
+ const intervalSeconds = parseDurationSeconds(options.every, "--every");
7148
+ let targetId;
7149
+ if (config.kind !== "generated") {
7150
+ if (!options.target) fail(`--target is required for a ${config.kind} rotation`);
7151
+ const { targets } = await ctx.client.listLeaseTargets(target.orgId);
7152
+ const t = targets.find((x) => x.id === options.target || x.name === options.target);
7153
+ if (!t) fail(`no lease target "${options.target}" — register one with \`seekrit ${config.kind === "postgres" ? "pg" : config.kind} target add\``);
7154
+ targetId = t.id;
7155
+ }
7156
+ const { publicKeyJwk, grantedEnvironmentIds } = await ctx.client.getRotatorKey(target.orgId);
7157
+ let wrappedDek;
7158
+ if (!grantedEnvironmentIds.includes(target.envId)) wrappedDek = await wrapDek(await getDek(ctx, target.orgId, target.envId), publicKeyJwk);
7159
+ const { rotation: created, version } = await ctx.client.configureRotation(target.orgId, {
7160
+ environmentId: target.envId,
7161
+ secretName,
7162
+ config,
7163
+ intervalSeconds,
7164
+ ...targetId ? { targetId } : {},
7165
+ ...wrappedDek ? { wrappedDek } : {},
7166
+ ...options.now ? { rotateNow: true } : {}
7167
+ });
7168
+ console.error(`rotating ${created.secretName} in ${target.label} every ${formatInterval(created.intervalSeconds)} (${created.kind})${wrappedDek ? " — rotator key granted" : ""}`);
7169
+ if (version !== void 0) console.error(`rotated now — ${created.secretName} is at version ${version}`);
7170
+ else console.error(`first rotation: ${created.nextRotateAt}`);
7171
+ console.log(created.id);
7172
+ });
7173
+ rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>").option("--json", "print the full policies as JSON").action(async (options) => {
7174
+ const ctx = buildContext();
7175
+ const org = await resolveOrg(ctx, options.org);
7176
+ const { rotations } = await ctx.client.listRotations(org.id);
7177
+ if (options.json) {
7178
+ console.log(JSON.stringify(rotations, null, 2));
7179
+ return;
7180
+ }
7181
+ if (rotations.length === 0) {
7182
+ console.error(`no rotation policies in ${org.slug}`);
7183
+ return;
7184
+ }
7185
+ for (const r of rotations) console.log(rotationLine(r));
7186
+ });
7187
+ rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>").action(async (ref, options) => {
7188
+ const ctx = buildContext();
7189
+ const r = await resolveRotation(ctx, (await resolveOrg(ctx, options.org)).id, ref);
7190
+ console.log(JSON.stringify(r, null, 2));
7191
+ });
7192
+ rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>").action(async (ref, options) => {
7193
+ const ctx = buildContext();
7194
+ const org = await resolveOrg(ctx, options.org);
7195
+ const r = await resolveRotation(ctx, org.id, ref);
7196
+ const { version, rotatedAt } = await ctx.client.rotateSecretNow(org.id, r.id);
7197
+ console.error(`rotated ${r.secretName} at ${rotatedAt} — now at version ${version}. Read it with \`seekrit secrets get ${r.secretName}\`.`);
7198
+ });
7199
+ rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>").action(async (ref, options) => {
7200
+ const ctx = buildContext();
7201
+ const org = await resolveOrg(ctx, options.org);
7202
+ const r = await resolveRotation(ctx, org.id, ref);
7203
+ await ctx.client.updateRotation(org.id, r.id, { status: "paused" });
7204
+ console.error(`paused rotation of ${r.secretName}`);
7205
+ });
7206
+ rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>").action(async (ref, options) => {
7207
+ const ctx = buildContext();
7208
+ const org = await resolveOrg(ctx, options.org);
7209
+ const r = await resolveRotation(ctx, org.id, ref);
7210
+ const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { status: "active" });
7211
+ console.error(`resumed rotation of ${r.secretName} — next ${updated.nextRotateAt}`);
7212
+ });
7213
+ rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>").action(async (ref, options) => {
7214
+ const ctx = buildContext();
7215
+ const org = await resolveOrg(ctx, options.org);
7216
+ const r = await resolveRotation(ctx, org.id, ref);
7217
+ const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { intervalSeconds: parseDurationSeconds(options.every, "--every") });
7218
+ console.error(`${updated.secretName} now rotates every ${formatInterval(updated.intervalSeconds)} — next ${updated.nextRotateAt}`);
7219
+ });
7220
+ rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>").action(async (ref, options) => {
7221
+ const ctx = buildContext();
7222
+ const org = await resolveOrg(ctx, options.org);
7223
+ const r = await resolveRotation(ctx, org.id, ref);
7224
+ const { rotatorRevoked } = await ctx.client.disableRotation(org.id, r.id);
7225
+ console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
7226
+ });
7227
+ }
6773
7228
  /**
6774
7229
  * Fetch + decrypt every secret in a single environment.
6775
7230
  *
@@ -7172,6 +7627,22 @@ function assertProvider(value) {
7172
7627
  if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
7173
7628
  return value;
7174
7629
  }
7630
+ /**
7631
+ * Netlify names a site by its **API ID**, and only that.
7632
+ *
7633
+ * Netlify does accept a site's domain where a site appears in a URL path, which
7634
+ * makes the strictness here look gratuitous — but the environment variable
7635
+ * endpoints take the site as a `?site_id=` query parameter, where Netlify
7636
+ * documents no name resolution. A site id it cannot resolve does not fail: the
7637
+ * variables land on the **team**, shared by every site in it. Refusing anything
7638
+ * but the UUID keeps a slip from writing much wider than was asked.
7639
+ */
7640
+ function assertNetlifySite(value) {
7641
+ if (!value) fail("--netlify-site is required for netlify (the site's API ID, a UUID)");
7642
+ const site = value.trim();
7643
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(site)) fail(`--netlify-site "${site}" is not a site API ID — Netlify shows it under Project configuration → General → Project information, and it is a UUID, not the site name or its .netlify.app address`);
7644
+ return site;
7645
+ }
7175
7646
  /** Account-scope config for a connection (never the credential itself). */
7176
7647
  function buildConfig(provider, options) {
7177
7648
  switch (provider) {
@@ -7209,6 +7680,12 @@ function buildConfig(provider, options) {
7209
7680
  case "northflank": return { provider: "northflank" };
7210
7681
  case "digitalocean": return { provider: "digitalocean" };
7211
7682
  case "heroku": return { provider: "heroku" };
7683
+ case "netlify":
7684
+ if (!options.accountId) fail("--account-id is required for netlify — the team slug from app.netlify.com/teams/<slug>, or the account ID");
7685
+ return {
7686
+ provider: "netlify",
7687
+ accountId: options.accountId
7688
+ };
7212
7689
  }
7213
7690
  }
7214
7691
  /** Where inside the platform a binding writes. */
@@ -7330,6 +7807,18 @@ function buildDestination(provider, options) {
7330
7807
  provider: "heroku",
7331
7808
  app: assertHerokuApp(options.herokuApp)
7332
7809
  };
7810
+ case "netlify": {
7811
+ const contexts = assertMembers(list(options.target, "production"), NETLIFY_CONTEXTS, "--target");
7812
+ const branch = options.gitBranch?.trim();
7813
+ if (contexts.includes("branch") && !branch) fail("--git-branch is required with `--target branch` — the branch the values apply to");
7814
+ return {
7815
+ provider: "netlify",
7816
+ siteId: assertNetlifySite(options.netlifySite),
7817
+ contexts,
7818
+ ...contexts.includes("branch") && branch ? { branch } : {},
7819
+ secret: options.netlifySecret !== false
7820
+ };
7821
+ }
7333
7822
  }
7334
7823
  }
7335
7824
  /** One-line description of a destination, for list output. */
@@ -7347,6 +7836,7 @@ function describeDestination(destination) {
7347
7836
  case "northflank": return `${destination.projectId} / ${destination.secretGroupId}`;
7348
7837
  case "digitalocean": return `${destination.appId}${destination.kind === "component" ? ` / ${destination.componentName}` : ""} (${destination.scope})`;
7349
7838
  case "heroku": return destination.app;
7839
+ case "netlify": return `${destination.siteId} (${destination.contexts.map((context) => context === "branch" ? `branch @${destination.branch}` : context).join(", ")})`;
7350
7840
  }
7351
7841
  }
7352
7842
  /**
@@ -7357,7 +7847,7 @@ function describeDestination(destination) {
7357
7847
  * application whose environment the binding reads from.
7358
7848
  */
7359
7849
  function destinationOptions(command) {
7360
- return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug").option("--target <list>", "vercel / cloudflare-pages: comma-separated deployment targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--script <name>", "cloudflare-workers: Worker script name").option("--store-id <id>", "cloudflare-secrets-store: store ID (32 hex)").option("--scopes <list>", "cloudflare-secrets-store: comma-separated scopes", "workers").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)");
7850
+ return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug").option("--target <list>", "vercel / cloudflare-pages: comma-separated deployment targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--script <name>", "cloudflare-workers: Worker script name").option("--store-id <id>", "cloudflare-secrets-store: store ID (32 hex)").option("--scopes <list>", "cloudflare-secrets-store: comma-separated scopes", "workers").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)").option("--netlify-site <id>", "netlify: site API ID (the UUID under Project configuration)").option("--no-netlify-secret", "netlify: create readable variables instead of Netlify secrets (write-only)");
7361
7851
  }
7362
7852
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
7363
7853
  async function resolveConnection(ctx, orgId, ref) {
@@ -7381,7 +7871,7 @@ function registerSyncCommands(program) {
7381
7871
  col("id", (c) => c.id)
7382
7872
  ], "no connections — add one with `seekrit sync connect`"));
7383
7873
  });
7384
- sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar)").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").action(async (options) => {
7874
+ sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").action(async (options) => {
7385
7875
  const provider = assertProvider(options.provider);
7386
7876
  const ctx = buildContext();
7387
7877
  const ref = await resolveOrg(ctx, options.org);
@@ -8249,6 +8739,7 @@ registerAwsCommands(program);
8249
8739
  registerGcpCommands(program);
8250
8740
  registerMongoCommands(program);
8251
8741
  registerKmsCommands(program);
8742
+ registerRotationCommands(program);
8252
8743
  registerRecoveryCommands(program);
8253
8744
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
8254
8745
  const { runMcpServer } = await import("./mcp-DR-zla_u.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {