@seekrit/cli 0.36.0 → 0.38.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 +186 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1520,7 +1520,10 @@ const SYNC_PROVIDER_KINDS = [
1520
1520
  "cloudflare-secrets-store",
1521
1521
  "railway",
1522
1522
  "aws-secrets-manager",
1523
- "aws-parameter-store"
1523
+ "aws-parameter-store",
1524
+ "render",
1525
+ "fly",
1526
+ "northflank"
1524
1527
  ];
1525
1528
  z.enum(SYNC_PROVIDER_KINDS);
1526
1529
  /**
@@ -1646,6 +1649,45 @@ const awsParameterStoreConnectionConfigSchema = z.object({
1646
1649
  region: awsRegionSchema,
1647
1650
  accessKeyId: awsAccessKeyIdSchema
1648
1651
  });
1652
+ /**
1653
+ * Render account scope — deliberately empty.
1654
+ *
1655
+ * Like Railway's, and unlike Vercel (which 403s team-owned resources without
1656
+ * `teamId`) or Cloudflare (whose every endpoint is account-scoped): a Render
1657
+ * API key is issued to a user, and every endpoint seekrit calls addresses its
1658
+ * resource by id — `srv-…`, `crn-…`, `evg-…`. There is nothing to scope, so
1659
+ * nothing is stored. The connection's `name` is what tells an operator which Render
1660
+ * workspace it belongs to.
1661
+ */
1662
+ const renderConnectionConfigSchema = z.object({ provider: z.literal("render") });
1663
+ /**
1664
+ * Fly.io account scope — empty, as Render's is.
1665
+ *
1666
+ * Neither half of "which account, which thing" needs stating: Fly app names are
1667
+ * globally unique, so the destination names its app and that is the whole
1668
+ * address. Nor is there a token kind to declare the way Railway's `tokenKind`
1669
+ * is — Fly's two token shapes do take different auth schemes, but
1670
+ * `flyAuthorization` in the connector reads which one from the token itself.
1671
+ */
1672
+ const flyConnectionConfigSchema = z.object({ provider: z.literal("fly") });
1673
+ /**
1674
+ * A Northflank id — projects and secret groups are both slugs derived from the
1675
+ * name they were created with (`default-project`, `example-secret-group`), and
1676
+ * both appear in the resource's URL. Validated against Northflank's own pattern
1677
+ * so the common slip — pasting the *display name*, spaces and all — fails here
1678
+ * rather than as a bare 404 inside an alarm with nobody watching.
1679
+ */
1680
+ 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");
1681
+ /**
1682
+ * Northflank account scope — deliberately empty.
1683
+ *
1684
+ * A Northflank API token is issued by exactly one team (or org-owned team) and
1685
+ * carries that scope itself; `GET /v1/auth` reports which. There is no team id
1686
+ * to disambiguate the way Vercel needs one, and no account id the way
1687
+ * Cloudflare does: the token plus the destination's project is the whole
1688
+ * address. The kind exists so the discriminated union stays uniform.
1689
+ */
1690
+ const northflankConnectionConfigSchema = z.object({ provider: z.literal("northflank") });
1649
1691
  const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1650
1692
  vercelConnectionConfigSchema,
1651
1693
  cloudflareWorkersConnectionConfigSchema,
@@ -1653,7 +1695,10 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1653
1695
  cloudflareSecretsStoreConnectionConfigSchema,
1654
1696
  railwayConnectionConfigSchema,
1655
1697
  awsSecretsManagerConnectionConfigSchema,
1656
- awsParameterStoreConnectionConfigSchema
1698
+ awsParameterStoreConnectionConfigSchema,
1699
+ renderConnectionConfigSchema,
1700
+ flyConnectionConfigSchema,
1701
+ northflankConnectionConfigSchema
1657
1702
  ]);
1658
1703
  /** Vercel's three deployment targets. A binding writes to one or more. */
1659
1704
  const VERCEL_TARGETS = [
@@ -1820,6 +1865,83 @@ const awsParameterStoreDestinationSchema = z.object({
1820
1865
  tier: z.enum(AWS_PARAMETER_TIERS).default("Standard"),
1821
1866
  kmsKeyId: awsKmsKeyIdSchema.optional()
1822
1867
  });
1868
+ /**
1869
+ * Render resource ids are `<prefix>-<slug>`, and the two prefixes below are the
1870
+ * documented ones: `srv-` for every service type, `crn-` for cron jobs, `evg-`
1871
+ * for an environment group.
1872
+ *
1873
+ * The patterns reject the *other* kind's prefix rather than requiring their own.
1874
+ * The mistake worth catching is pasting an env-group id into the service field
1875
+ * (or the reverse) — which is otherwise a 404 hours later inside an alarm, with
1876
+ * nobody watching. Requiring the positive prefix would also reject a valid id
1877
+ * the day Render introduces a new resource prefix, which is not our call to
1878
+ * make.
1879
+ */
1880
+ 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");
1881
+ 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");
1882
+ /** Environment variables set directly on one service. */
1883
+ const renderServiceDestinationSchema = z.object({
1884
+ provider: z.literal("render"),
1885
+ kind: z.literal("service"),
1886
+ /** Service id (`srv-…`, or `crn-…` for a cron job), from its dashboard URL. */
1887
+ serviceId: renderServiceIdSchema
1888
+ });
1889
+ /**
1890
+ * Environment variables in a shared environment group. Every service linked to
1891
+ * the group sees them, which is the point — and the reason a group binding is
1892
+ * worth thinking about twice: its blast radius is the link list, not one
1893
+ * service.
1894
+ */
1895
+ const renderEnvGroupDestinationSchema = z.object({
1896
+ provider: z.literal("render"),
1897
+ kind: z.literal("env-group"),
1898
+ /** Environment group id (`evg-…`), from its dashboard URL. */
1899
+ envGroupId: renderEnvGroupIdSchema
1900
+ });
1901
+ const renderDestinationSchema = z.discriminatedUnion("kind", [renderServiceDestinationSchema, renderEnvGroupDestinationSchema]);
1902
+ /**
1903
+ * The Fly app whose secret set a binding owns.
1904
+ *
1905
+ * A Fly app has **one** secret set, shared by every Machine in every region —
1906
+ * there is no per-target split to state, the way Vercel and Pages have one.
1907
+ * Fly's convention is that staging and production are separate *apps*
1908
+ * (`storefront`, `storefront-staging`), so pointing at an environment means
1909
+ * naming that app, exactly as a Wrangler environment means naming its own
1910
+ * Worker.
1911
+ *
1912
+ * Values land **staged**: Fly injects secrets when a Machine boots, so already
1913
+ * running Machines keep what they started with until the app is deployed or its
1914
+ * Machines are updated (`fly secrets deploy -a <app>`), while Machines created
1915
+ * after the push get them straight away. The connector deliberately restarts
1916
+ * nothing — see the note in `apps/api/src/lib/sync/connectors/fly.ts`.
1917
+ */
1918
+ const flyDestinationSchema = z.object({
1919
+ provider: z.literal("fly"),
1920
+ /** Fly app name, as `fly apps list` prints it. */
1921
+ 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)")
1922
+ });
1923
+ /**
1924
+ * Where inside Northflank a binding writes: one **secret group** in one
1925
+ * project.
1926
+ *
1927
+ * A secret group is Northflank's unit of injection — services and jobs in the
1928
+ * project inherit its variables, subject to the group's own restrictions and
1929
+ * priority. Those settings belong to the operator, not to seekrit: a binding
1930
+ * names an existing group and only ever writes its `variables` map, so
1931
+ * restrictions, priority, secret type, and any secret *files* stay as they were
1932
+ * configured.
1933
+ *
1934
+ * There is no environment field. Northflank has no per-group environment axis —
1935
+ * separate environments are separate projects (or separate groups restricted to
1936
+ * a stage), so the binding's seekrit environment maps to a group, one to one.
1937
+ */
1938
+ const northflankDestinationSchema = z.object({
1939
+ provider: z.literal("northflank"),
1940
+ /** Project id — the slug in the project URL (`default-project`). */
1941
+ projectId: northflankIdSchema,
1942
+ /** Secret group id — the slug in the group's URL (`example-secret-group`). */
1943
+ secretGroupId: northflankIdSchema
1944
+ });
1823
1945
  const syncDestinationSchema = z.discriminatedUnion("provider", [
1824
1946
  vercelDestinationSchema,
1825
1947
  cloudflareWorkersDestinationSchema,
@@ -1827,7 +1949,10 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
1827
1949
  cloudflareSecretsStoreDestinationSchema,
1828
1950
  railwayDestinationSchema,
1829
1951
  awsSecretsManagerDestinationSchema,
1830
- awsParameterStoreDestinationSchema
1952
+ awsParameterStoreDestinationSchema,
1953
+ renderDestinationSchema,
1954
+ flyDestinationSchema,
1955
+ northflankDestinationSchema
1831
1956
  ]);
1832
1957
  /**
1833
1958
  * How seekrit secret names become destination key names. Applied in order:
@@ -3032,7 +3157,7 @@ function isCliSessionToken(value) {
3032
3157
  }
3033
3158
  //#endregion
3034
3159
  //#region package.json
3035
- var version = "0.36.0";
3160
+ var version = "0.38.0";
3036
3161
  //#endregion
3037
3162
  //#region ../../packages/api-client/src/index.ts
3038
3163
  var SeekritApiError = class extends Error {
@@ -6860,6 +6985,28 @@ function assertMember(value, allowed, flag, fallback) {
6860
6985
  if (!allowed.includes(value)) fail(`unknown ${flag} "${value}" — one of: ${allowed.join(", ")}`);
6861
6986
  return value;
6862
6987
  }
6988
+ /**
6989
+ * Fly app names are DNS labels, and the two habitual slips are pasting the
6990
+ * hostname (`storefront.fly.dev`) or a name with capitals in it. Both are a 404
6991
+ * from Fly much later, so they are caught here with a message that says which.
6992
+ */
6993
+ function assertFlyApp(value) {
6994
+ if (!value) fail("--fly-app is required for fly (the app name, e.g. storefront-production)");
6995
+ const appName = value.trim();
6996
+ if (/\.(fly\.dev|internal)$/.test(appName)) fail(`--fly-app takes the app name, not its hostname — try "${appName.split(".")[0]}"`);
6997
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(appName) || appName.length > 63) fail(`--fly-app "${appName}" is not a Fly app name (lowercase letters, numbers, and dashes)`);
6998
+ return appName;
6999
+ }
7000
+ /**
7001
+ * Northflank ids are slugs from the resource's URL, so the slip to catch is the
7002
+ * *display name* — "App Secrets" where "app-secrets" belongs.
7003
+ */
7004
+ function assertNorthflankId(value, flag) {
7005
+ if (!value) fail(`${flag} is required for northflank (the slug from its URL)`);
7006
+ const id = value.trim();
7007
+ if (!/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/.test(id)) fail(`${flag} should be a Northflank ID like "app-secrets", not "${id}"`);
7008
+ return id;
7009
+ }
6863
7010
  function assertProvider(value) {
6864
7011
  if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
6865
7012
  return value;
@@ -6896,6 +7043,9 @@ function buildConfig(provider, options) {
6896
7043
  region: options.region,
6897
7044
  accessKeyId: options.accessKeyId
6898
7045
  };
7046
+ case "render": return { provider: "render" };
7047
+ case "fly": return { provider: "fly" };
7048
+ case "northflank": return { provider: "northflank" };
6899
7049
  }
6900
7050
  }
6901
7051
  /** Where inside the platform a binding writes. */
@@ -6969,6 +7119,31 @@ function buildDestination(provider, options) {
6969
7119
  tier: assertMember(options.tier, AWS_PARAMETER_TIERS, "--tier", "Standard"),
6970
7120
  ...options.kmsKeyId ? { kmsKeyId: options.kmsKeyId } : {}
6971
7121
  };
7122
+ case "render": {
7123
+ const serviceId = options.service?.trim();
7124
+ const envGroupId = options.envGroup?.trim();
7125
+ if (serviceId && envGroupId) fail("pass --service or --env-group for render, not both — a binding writes to one");
7126
+ if (serviceId) return {
7127
+ provider: "render",
7128
+ kind: "service",
7129
+ serviceId
7130
+ };
7131
+ if (envGroupId) return {
7132
+ provider: "render",
7133
+ kind: "env-group",
7134
+ envGroupId
7135
+ };
7136
+ return fail("--service (srv-…, or crn-… for a cron job) or --env-group (evg-…) is required for render");
7137
+ }
7138
+ case "fly": return {
7139
+ provider: "fly",
7140
+ appName: assertFlyApp(options.flyApp)
7141
+ };
7142
+ case "northflank": return {
7143
+ provider: "northflank",
7144
+ projectId: assertNorthflankId(options.project, "--project"),
7145
+ secretGroupId: assertNorthflankId(options.secretGroup, "--secret-group")
7146
+ };
6972
7147
  }
6973
7148
  }
6974
7149
  /** One-line description of a destination, for list output. */
@@ -6981,14 +7156,20 @@ function describeDestination(destination) {
6981
7156
  case "railway": return `${destination.projectId} / ${destination.environmentId} (${destination.serviceId ? `service ${destination.serviceId}` : "shared"})`;
6982
7157
  case "aws-secrets-manager": return destination.layout === "json-bundle" ? `${destination.secretName} (json bundle)` : `${destination.pathPrefix ?? ""}* (secret per name)`;
6983
7158
  case "aws-parameter-store": return `${destination.path}* (${destination.type})`;
7159
+ case "render": return destination.kind === "service" ? destination.serviceId : `env group ${destination.envGroupId}`;
7160
+ case "fly": return destination.appName;
7161
+ case "northflank": return `${destination.projectId} / ${destination.secretGroupId}`;
6984
7162
  }
6985
7163
  }
6986
7164
  /**
6987
7165
  * Destination flags, shared by `verify` and `enable` so the two can never drift
6988
7166
  * into accepting different ways of naming the same destination.
7167
+ *
7168
+ * Fly's is `--fly-app`, not `--app`: `--app` already names the seekrit
7169
+ * application whose environment the binding reads from.
6989
7170
  */
6990
7171
  function destinationOptions(command) {
6991
- 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").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)").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");
7172
+ 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)");
6992
7173
  }
6993
7174
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
6994
7175
  async function resolveConnection(ctx, orgId, ref) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {