@seekrit/cli 0.33.0 → 0.34.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 +146 -8
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1513,7 +1513,12 @@ z.object({
1513
1513
  * See `docs/third-party-sync.md`.
1514
1514
  */
1515
1515
  /** Platforms seekrit can push to. Append-only — persisted in `sync_connections.provider`. */
1516
- const SYNC_PROVIDER_KINDS = ["vercel"];
1516
+ const SYNC_PROVIDER_KINDS = [
1517
+ "vercel",
1518
+ "cloudflare-workers",
1519
+ "cloudflare-pages",
1520
+ "cloudflare-secrets-store"
1521
+ ];
1517
1522
  z.enum(SYNC_PROVIDER_KINDS);
1518
1523
  /**
1519
1524
  * Vercel account scope. The API token itself is never here — it is wrapped to
@@ -1528,7 +1533,44 @@ const vercelConnectionConfigSchema = z.object({
1528
1533
  /** Vercel Team id (`team_…`). Omit for a personal account. */
1529
1534
  teamId: z.string().trim().min(1).max(128).optional()
1530
1535
  });
1531
- const syncConnectionConfigSchema = z.discriminatedUnion("provider", [vercelConnectionConfigSchema]);
1536
+ /**
1537
+ * A Cloudflare account id — 32 lowercase hex characters, found in the sidebar
1538
+ * of any account's dashboard. Every Cloudflare endpoint seekrit calls is
1539
+ * account-scoped, so this is the account half of "which account, which thing".
1540
+ *
1541
+ * Validated by shape because the alternative is a bare 400 from Cloudflare
1542
+ * hours later inside an alarm, with nobody watching. It does not catch pasting
1543
+ * a *zone* id, which has the same shape — only the API can tell those apart.
1544
+ */
1545
+ const cloudflareAccountIdSchema = z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Cloudflare account ID (lowercase hex)");
1546
+ /**
1547
+ * Cloudflare account scope, shared by all three Cloudflare providers. The API
1548
+ * token is never here — it is wrapped to the connection's public key and
1549
+ * stored as ciphertext, exactly as Vercel's is.
1550
+ *
1551
+ * The three providers are deliberately separate kinds rather than one
1552
+ * `cloudflare` with a mode field: they target different APIs, take different
1553
+ * destinations, and fail in different ways. Splitting them keeps the
1554
+ * exhaustiveness guard in `connectorFor` meaningful.
1555
+ */
1556
+ const cloudflareWorkersConnectionConfigSchema = z.object({
1557
+ provider: z.literal("cloudflare-workers"),
1558
+ accountId: cloudflareAccountIdSchema
1559
+ });
1560
+ const cloudflarePagesConnectionConfigSchema = z.object({
1561
+ provider: z.literal("cloudflare-pages"),
1562
+ accountId: cloudflareAccountIdSchema
1563
+ });
1564
+ const cloudflareSecretsStoreConnectionConfigSchema = z.object({
1565
+ provider: z.literal("cloudflare-secrets-store"),
1566
+ accountId: cloudflareAccountIdSchema
1567
+ });
1568
+ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1569
+ vercelConnectionConfigSchema,
1570
+ cloudflareWorkersConnectionConfigSchema,
1571
+ cloudflarePagesConnectionConfigSchema,
1572
+ cloudflareSecretsStoreConnectionConfigSchema
1573
+ ]);
1532
1574
  /** Vercel's three deployment targets. A binding writes to one or more. */
1533
1575
  const VERCEL_TARGETS = [
1534
1576
  "production",
@@ -1547,7 +1589,51 @@ const vercelDestinationSchema = z.object({
1547
1589
  */
1548
1590
  gitBranch: z.string().trim().min(1).max(255).optional()
1549
1591
  });
1550
- const syncDestinationSchema = z.discriminatedUnion("provider", [vercelDestinationSchema]);
1592
+ /**
1593
+ * The Worker whose secrets a binding owns. Wrangler *environments* are not a
1594
+ * separate field because they are not a separate concept at the API: deploying
1595
+ * `my-api` with `--env staging` creates a Worker literally named
1596
+ * `my-api-staging`, so pointing at an environment means naming that script.
1597
+ */
1598
+ const cloudflareWorkersDestinationSchema = z.object({
1599
+ provider: z.literal("cloudflare-workers"),
1600
+ /** Worker script name, as shown in the dashboard (`my-api`). */
1601
+ scriptName: z.string().trim().min(1).max(63).regex(/^[A-Za-z0-9_][A-Za-z0-9_-]*$/, "must be a Worker script name")
1602
+ });
1603
+ /** The two deployment configs a Pages project keeps env vars under. */
1604
+ const CLOUDFLARE_PAGES_ENVIRONMENTS = ["production", "preview"];
1605
+ const cloudflarePagesDestinationSchema = z.object({
1606
+ provider: z.literal("cloudflare-pages"),
1607
+ /** Pages project name (`my-site`) — Pages has no separate project id. */
1608
+ projectName: z.string().trim().min(1).max(58).regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/, "must be a Pages project name"),
1609
+ /** Which deployment configs receive these values. At least one. */
1610
+ environments: z.array(z.enum(CLOUDFLARE_PAGES_ENVIRONMENTS)).min(1)
1611
+ });
1612
+ /**
1613
+ * Scopes a Secrets Store secret may be used by. Cloudflare requires at least
1614
+ * one at creation and they cannot be inferred, so a binding states them.
1615
+ */
1616
+ const CLOUDFLARE_SECRETS_STORE_SCOPES = [
1617
+ "workers",
1618
+ "ai_gateway",
1619
+ "dex",
1620
+ "access",
1621
+ "containers",
1622
+ "websearch"
1623
+ ];
1624
+ const cloudflareSecretsStoreDestinationSchema = z.object({
1625
+ provider: z.literal("cloudflare-secrets-store"),
1626
+ /** Store id (32 hex). An account has exactly one store today. */
1627
+ storeId: z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Secrets Store ID (lowercase hex)"),
1628
+ /** Scopes applied to secrets this binding creates. At least one. */
1629
+ scopes: z.array(z.enum(CLOUDFLARE_SECRETS_STORE_SCOPES)).min(1)
1630
+ });
1631
+ const syncDestinationSchema = z.discriminatedUnion("provider", [
1632
+ vercelDestinationSchema,
1633
+ cloudflareWorkersDestinationSchema,
1634
+ cloudflarePagesDestinationSchema,
1635
+ cloudflareSecretsStoreDestinationSchema
1636
+ ]);
1551
1637
  /**
1552
1638
  * How seekrit secret names become destination key names. Applied in order:
1553
1639
  * explicit `rename` (wins outright), then `prefix`/`suffix`, then `case`.
@@ -2751,7 +2837,7 @@ function isCliSessionToken(value) {
2751
2837
  }
2752
2838
  //#endregion
2753
2839
  //#region package.json
2754
- var version = "0.33.0";
2840
+ var version = "0.34.0";
2755
2841
  //#endregion
2756
2842
  //#region ../../packages/api-client/src/index.ts
2757
2843
  var SeekritApiError = class extends Error {
@@ -6556,6 +6642,16 @@ function collect(value, acc) {
6556
6642
  }
6557
6643
  //#endregion
6558
6644
  //#region src/sync.ts
6645
+ /** Split a `--flag a,b` list into trimmed, non-empty members. */
6646
+ function list(raw, fallback) {
6647
+ return (raw ?? fallback).split(",").map((item) => item.trim()).filter(Boolean);
6648
+ }
6649
+ /** Reject list members outside a known set, naming both the strays and the choices. */
6650
+ function assertMembers(values, allowed, flag) {
6651
+ const unknown = values.filter((v) => !allowed.includes(v));
6652
+ if (unknown.length > 0) fail(`unknown ${flag} ${unknown.join(", ")} — one of: ${allowed.join(", ")}`);
6653
+ return values;
6654
+ }
6559
6655
  function assertProvider(value) {
6560
6656
  if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
6561
6657
  return value;
@@ -6567,6 +6663,14 @@ function buildConfig(provider, options) {
6567
6663
  provider: "vercel",
6568
6664
  ...options.teamId ? { teamId: options.teamId } : {}
6569
6665
  };
6666
+ case "cloudflare-workers":
6667
+ case "cloudflare-pages":
6668
+ case "cloudflare-secrets-store":
6669
+ if (!options.accountId) fail(`--account-id is required for ${provider} — 32 hex characters, in the sidebar of any Cloudflare dashboard page`);
6670
+ return {
6671
+ provider,
6672
+ accountId: options.accountId
6673
+ };
6570
6674
  }
6571
6675
  }
6572
6676
  /** Where inside the platform a binding writes. */
@@ -6584,14 +6688,48 @@ function buildDestination(provider, options) {
6584
6688
  ...options.gitBranch ? { gitBranch: options.gitBranch } : {}
6585
6689
  };
6586
6690
  }
6691
+ case "cloudflare-workers":
6692
+ if (!options.script) fail("--script is required for cloudflare-workers (the Worker's name, e.g. my-api)");
6693
+ return {
6694
+ provider: "cloudflare-workers",
6695
+ scriptName: options.script
6696
+ };
6697
+ case "cloudflare-pages": {
6698
+ if (!options.project) fail("--project is required for cloudflare-pages (the project name)");
6699
+ const environments = assertMembers(list(options.target, "production"), CLOUDFLARE_PAGES_ENVIRONMENTS, "--target");
6700
+ return {
6701
+ provider: "cloudflare-pages",
6702
+ projectName: options.project,
6703
+ environments
6704
+ };
6705
+ }
6706
+ case "cloudflare-secrets-store": {
6707
+ if (!options.storeId) fail("--store-id is required for cloudflare-secrets-store (32 hex characters)");
6708
+ const scopes = assertMembers(list(options.scopes, "workers"), CLOUDFLARE_SECRETS_STORE_SCOPES, "--scopes");
6709
+ return {
6710
+ provider: "cloudflare-secrets-store",
6711
+ storeId: options.storeId,
6712
+ scopes
6713
+ };
6714
+ }
6587
6715
  }
6588
6716
  }
6589
6717
  /** One-line description of a destination, for list output. */
6590
6718
  function describeDestination(destination) {
6591
6719
  switch (destination.provider) {
6592
6720
  case "vercel": return `${destination.projectId} (${destination.targets.join(", ")}${destination.gitBranch ? ` @${destination.gitBranch}` : ""})`;
6721
+ case "cloudflare-workers": return destination.scriptName;
6722
+ case "cloudflare-pages": return `${destination.projectName} (${destination.environments.join(", ")})`;
6723
+ case "cloudflare-secrets-store": return `store ${destination.storeId} (${destination.scopes.join(", ")})`;
6593
6724
  }
6594
6725
  }
6726
+ /**
6727
+ * Destination flags, shared by `verify` and `enable` so the two can never drift
6728
+ * into accepting different ways of naming the same destination.
6729
+ */
6730
+ function destinationOptions(command) {
6731
+ return command.option("--project <id>", "vercel: project id or name · cloudflare-pages: project name").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");
6732
+ }
6595
6733
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
6596
6734
  async function resolveConnection(ctx, orgId, ref) {
6597
6735
  const { connections } = await ctx.client.listSyncConnections(orgId);
@@ -6601,7 +6739,7 @@ async function resolveConnection(ctx, orgId, ref) {
6601
6739
  return matches[0];
6602
6740
  }
6603
6741
  function registerSyncCommands(program) {
6604
- const sync = program.command("sync").description("push environments to a third-party platform (Vercel, …)");
6742
+ const sync = program.command("sync").description("push environments to a third-party platform (Vercel, Cloudflare, …)");
6605
6743
  sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
6606
6744
  const ctx = buildContext();
6607
6745
  const ref = await resolveOrg(ctx, options.org);
@@ -6614,7 +6752,7 @@ function registerSyncCommands(program) {
6614
6752
  col("id", (c) => c.id)
6615
6753
  ], "no connections — add one with `seekrit sync connect`"));
6616
6754
  });
6617
- 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)").action(async (options) => {
6755
+ 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("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar)").action(async (options) => {
6618
6756
  const provider = assertProvider(options.provider);
6619
6757
  const ctx = buildContext();
6620
6758
  const ref = await resolveOrg(ctx, options.org);
@@ -6630,7 +6768,7 @@ function registerSyncCommands(program) {
6630
6768
  });
6631
6769
  console.error(`connected ${created.connection.name} (${created.connection.id})`);
6632
6770
  });
6633
- sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--project <id>", "vercel: project id or name").option("--target <list>", "vercel: comma-separated targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--json", "print the raw API response").action(async (connection, options) => {
6771
+ destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
6634
6772
  const provider = assertProvider(options.provider);
6635
6773
  const ctx = buildContext();
6636
6774
  const ref = await resolveOrg(ctx, options.org);
@@ -6664,7 +6802,7 @@ function registerSyncCommands(program) {
6664
6802
  col("id", (b) => b.id)
6665
6803
  ], "nothing is syncing — enable it with `seekrit sync enable`"));
6666
6804
  });
6667
- sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--project <id>", "vercel: project id or name").option("--target <list>", "vercel: comma-separated targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
6805
+ destinationOptions(sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
6668
6806
  const provider = assertProvider(options.provider);
6669
6807
  if (options.onDelete !== "delete" && options.onDelete !== "retain") fail("--on-delete must be delete or retain");
6670
6808
  if (options.mode !== "auto" && options.mode !== "manual") fail("--mode must be auto or manual");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,9 +27,9 @@
27
27
  "@types/node": "^26.1.0",
28
28
  "tsdown": "^0.22.3",
29
29
  "vitest": "^4.1.9",
30
- "@seekrit/api-client": "0.0.1",
30
+ "@seekrit/core": "0.0.1",
31
31
  "@seekrit/crypto": "0.0.1",
32
- "@seekrit/core": "0.0.1"
32
+ "@seekrit/api-client": "0.0.1"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsdown",