@seekrit/cli 0.35.0 → 0.36.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 +174 -10
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -669,7 +669,7 @@ const redisSha256VerifierSchema = z.string().regex(/^[0-9a-f]{64}$/, "must be a
669
669
  */
670
670
  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>)");
671
671
  /** An AWS region id, e.g. `us-east-1`, `eu-west-2`, `us-gov-west-1`. */
672
- const awsRegionSchema = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be an AWS region id (e.g. us-east-1)");
672
+ const awsRegionSchema$1 = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be an AWS region id (e.g. us-east-1)");
673
673
  /**
674
674
  * An STS external id — the shared string a role's trust policy can require so a
675
675
  * confused-deputy can't assume it. AWS allows a broad charset; we keep to the
@@ -811,7 +811,7 @@ const awsTargetConfigSchema = z.object({
811
811
  provider: z.literal("aws"),
812
812
  executor: z.literal("in_do"),
813
813
  roleArn: awsRoleArnSchema,
814
- region: awsRegionSchema,
814
+ region: awsRegionSchema$1,
815
815
  externalId: awsExternalIdSchema.optional(),
816
816
  sessionPolicy: z.string().min(1).max(4e3).optional(),
817
817
  maxTtlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS).optional()
@@ -1518,7 +1518,9 @@ const SYNC_PROVIDER_KINDS = [
1518
1518
  "cloudflare-workers",
1519
1519
  "cloudflare-pages",
1520
1520
  "cloudflare-secrets-store",
1521
- "railway"
1521
+ "railway",
1522
+ "aws-secrets-manager",
1523
+ "aws-parameter-store"
1522
1524
  ];
1523
1525
  z.enum(SYNC_PROVIDER_KINDS);
1524
1526
  /**
@@ -1600,12 +1602,58 @@ const railwayConnectionConfigSchema = z.object({
1600
1602
  provider: z.literal("railway"),
1601
1603
  tokenKind: z.enum(RAILWAY_TOKEN_KINDS).default("account")
1602
1604
  });
1605
+ /**
1606
+ * An AWS region id (`us-east-1`, `eu-central-1`, `us-gov-west-1`).
1607
+ *
1608
+ * Validated by shape rather than against a list, because AWS adds regions
1609
+ * faster than we ship. The endpoint host is built from this string, so a typo
1610
+ * would otherwise surface as a DNS failure inside an alarm with nobody
1611
+ * watching — which is a much worse place to learn about it than this form.
1612
+ */
1613
+ const awsRegionSchema = z.string().trim().regex(/^[a-z]{2}(-[a-z]+)+-\d$/, "must be an AWS region ID, e.g. us-east-1");
1614
+ /**
1615
+ * The IAM access key id seekrit signs with.
1616
+ *
1617
+ * This lives in `config` — the *non-secret* half — on purpose: an access key id
1618
+ * is an identifier, not a credential. It appears in CloudTrail, in the IAM
1619
+ * console, and in the `Authorization` header of every signed request; only the
1620
+ * **secret access key** is secret, and that is what gets wrapped to the
1621
+ * connection's public key. Keeping the id here also lets the dashboard say
1622
+ * which key a connection is using, which is the first thing you want to know
1623
+ * when a connection starts failing after a key rotation.
1624
+ *
1625
+ * Long-lived IAM user keys only. `ASIA…` session credentials from STS expire
1626
+ * within hours, and a sync connection has to keep working unattended.
1627
+ */
1628
+ const awsAccessKeyIdSchema = z.string().trim().regex(/^[A-Z0-9]{16,128}$/, "must be an AWS access key ID, e.g. AKIAIOSFODNN7EXAMPLE");
1629
+ /**
1630
+ * AWS account scope, shared by both AWS providers: which region to call and
1631
+ * which key to sign with. There is no account id — every endpoint seekrit calls
1632
+ * is reached through the regional host and authorizes off the signature, so the
1633
+ * account is whichever one the key belongs to.
1634
+ *
1635
+ * Two providers rather than one `aws` with a mode field, for the same reason
1636
+ * the three Cloudflare kinds are separate: different APIs, different
1637
+ * destinations, different IAM actions.
1638
+ */
1639
+ const awsSecretsManagerConnectionConfigSchema = z.object({
1640
+ provider: z.literal("aws-secrets-manager"),
1641
+ region: awsRegionSchema,
1642
+ accessKeyId: awsAccessKeyIdSchema
1643
+ });
1644
+ const awsParameterStoreConnectionConfigSchema = z.object({
1645
+ provider: z.literal("aws-parameter-store"),
1646
+ region: awsRegionSchema,
1647
+ accessKeyId: awsAccessKeyIdSchema
1648
+ });
1603
1649
  const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1604
1650
  vercelConnectionConfigSchema,
1605
1651
  cloudflareWorkersConnectionConfigSchema,
1606
1652
  cloudflarePagesConnectionConfigSchema,
1607
1653
  cloudflareSecretsStoreConnectionConfigSchema,
1608
- railwayConnectionConfigSchema
1654
+ railwayConnectionConfigSchema,
1655
+ awsSecretsManagerConnectionConfigSchema,
1656
+ awsParameterStoreConnectionConfigSchema
1609
1657
  ]);
1610
1658
  /** Vercel's three deployment targets. A binding writes to one or more. */
1611
1659
  const VERCEL_TARGETS = [
@@ -1696,12 +1744,90 @@ const railwayDestinationSchema = z.object({
1696
1744
  */
1697
1745
  skipDeploys: z.boolean().optional()
1698
1746
  });
1747
+ /**
1748
+ * A customer-managed KMS key to encrypt with, as a key id, ARN, or alias
1749
+ * (`alias/seekrit`). Omitted means the AWS-managed default for that service
1750
+ * (`aws/secretsmanager`, `aws/ssm`), which is what most accounts want.
1751
+ *
1752
+ * Deliberately loose: a KMS key can be named five different ways, half of them
1753
+ * cross-account ARNs, and rejecting a valid one here would be worse than
1754
+ * letting KMS give its own (very clear) error.
1755
+ */
1756
+ const awsKmsKeyIdSchema = z.string().trim().min(1).max(2048);
1757
+ /**
1758
+ * How a binding lays its secrets out in Secrets Manager.
1759
+ *
1760
+ * - `secret-per-name` — one AWS secret per seekrit secret. The direct
1761
+ * translation, and what you want if consumers read secrets individually.
1762
+ * - `json-bundle` — every value as one JSON object in a single AWS secret. The
1763
+ * shape ECS task definitions and Lambda read with `secret-arn:json-key::`,
1764
+ * and the reason it exists is billing: Secrets Manager charges per secret per
1765
+ * month, so fifty names cost fifty times as much stored separately.
1766
+ */
1767
+ const AWS_SECRETS_MANAGER_LAYOUTS = ["secret-per-name", "json-bundle"];
1768
+ /**
1769
+ * Where in Secrets Manager a binding writes.
1770
+ *
1771
+ * `pathPrefix` exists rather than reusing {@link NameTransform}'s `prefix`
1772
+ * because the two answer different questions: a name transform produces a
1773
+ * *variable name* (`[A-Za-z0-9_]`, no slashes), while this produces a
1774
+ * *namespace* — `prod/storefront/` — and slashes are the whole point of it.
1775
+ */
1776
+ const awsSecretsManagerDestinationSchema = z.object({
1777
+ provider: z.literal("aws-secrets-manager"),
1778
+ layout: z.enum(AWS_SECRETS_MANAGER_LAYOUTS).default("secret-per-name"),
1779
+ /**
1780
+ * `secret-per-name` only: prepended to every secret's name, e.g.
1781
+ * `prod/storefront/`. Optional, but strongly advised in an account that
1782
+ * holds anything else — without it a binding writes at the root of a
1783
+ * namespace it does not own.
1784
+ */
1785
+ pathPrefix: z.string().trim().max(400).regex(/^[A-Za-z0-9/_+=.@-]*$/, "may contain letters, digits, and / _ + = . @ -").optional(),
1786
+ /** `json-bundle` only: the one secret that holds every value, e.g. `prod/storefront/env`. */
1787
+ secretName: z.string().trim().min(1).max(512).regex(/^[A-Za-z0-9/_+=.@-]+$/, "may contain letters, digits, and / _ + = . @ -").optional(),
1788
+ kmsKeyId: awsKmsKeyIdSchema.optional()
1789
+ }).refine((d) => d.layout !== "json-bundle" || d.secretName !== void 0, {
1790
+ message: "a json-bundle destination needs the name of the secret to write",
1791
+ path: ["secretName"]
1792
+ });
1793
+ /** Parameter Store's two value types. `SecureString` is KMS-encrypted; `String` is not. */
1794
+ const AWS_PARAMETER_TYPES = ["SecureString", "String"];
1795
+ /** Parameter Store's storage tiers. Standard is free and caps values at 4KB. */
1796
+ const AWS_PARAMETER_TIERS = [
1797
+ "Standard",
1798
+ "Advanced",
1799
+ "Intelligent-Tiering"
1800
+ ];
1801
+ /**
1802
+ * The Parameter Store hierarchy a binding owns, e.g. `/prod/storefront/`.
1803
+ *
1804
+ * A path rather than a free-form prefix because that is what the API is built
1805
+ * around: `GetParametersByPath` is how an application reads a whole
1806
+ * environment in one call, and it only works on `/`-delimited names. Leading
1807
+ * and trailing slashes are required so the binding's names concatenate
1808
+ * unambiguously — `/prod/storefront/` + `DB_URL`.
1809
+ */
1810
+ const awsParameterStoreDestinationSchema = z.object({
1811
+ provider: z.literal("aws-parameter-store"),
1812
+ /** Must start and end with `/`. `aws`/`ssm` are reserved by AWS as the first segment. */
1813
+ path: z.string().trim().max(1011).regex(/^\/([A-Za-z0-9_.-]+\/)*$/, "must be a parameter path like /prod/storefront/"),
1814
+ type: z.enum(AWS_PARAMETER_TYPES).default("SecureString"),
1815
+ /**
1816
+ * Standard caps a value at 4KB and costs nothing; Advanced raises that to 8KB
1817
+ * and is billed per parameter per month. `Intelligent-Tiering` lets AWS pick,
1818
+ * upgrading only the parameters that need it.
1819
+ */
1820
+ tier: z.enum(AWS_PARAMETER_TIERS).default("Standard"),
1821
+ kmsKeyId: awsKmsKeyIdSchema.optional()
1822
+ });
1699
1823
  const syncDestinationSchema = z.discriminatedUnion("provider", [
1700
1824
  vercelDestinationSchema,
1701
1825
  cloudflareWorkersDestinationSchema,
1702
1826
  cloudflarePagesDestinationSchema,
1703
1827
  cloudflareSecretsStoreDestinationSchema,
1704
- railwayDestinationSchema
1828
+ railwayDestinationSchema,
1829
+ awsSecretsManagerDestinationSchema,
1830
+ awsParameterStoreDestinationSchema
1705
1831
  ]);
1706
1832
  /**
1707
1833
  * How seekrit secret names become destination key names. Applied in order:
@@ -2906,7 +3032,7 @@ function isCliSessionToken(value) {
2906
3032
  }
2907
3033
  //#endregion
2908
3034
  //#region package.json
2909
- var version = "0.35.0";
3035
+ var version = "0.36.0";
2910
3036
  //#endregion
2911
3037
  //#region ../../packages/api-client/src/index.ts
2912
3038
  var SeekritApiError = class extends Error {
@@ -6728,6 +6854,12 @@ function assertRailwayId(value, flag) {
6728
6854
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) fail(`${flag} should be a Railway UUID, not "${id}"`);
6729
6855
  return id;
6730
6856
  }
6857
+ /** Reject a single value outside a known set, naming the choices. */
6858
+ function assertMember(value, allowed, flag, fallback) {
6859
+ if (value === void 0) return fallback;
6860
+ if (!allowed.includes(value)) fail(`unknown ${flag} "${value}" — one of: ${allowed.join(", ")}`);
6861
+ return value;
6862
+ }
6731
6863
  function assertProvider(value) {
6732
6864
  if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
6733
6865
  return value;
@@ -6755,6 +6887,15 @@ function buildConfig(provider, options) {
6755
6887
  tokenKind
6756
6888
  };
6757
6889
  }
6890
+ case "aws-secrets-manager":
6891
+ case "aws-parameter-store":
6892
+ if (!options.region) fail(`--region is required for ${provider} — an AWS region ID, e.g. us-east-1`);
6893
+ if (!options.accessKeyId) fail(`--access-key-id is required for ${provider} — the IAM access key ID (AKIA…)`);
6894
+ return {
6895
+ provider,
6896
+ region: options.region,
6897
+ accessKeyId: options.accessKeyId
6898
+ };
6758
6899
  }
6759
6900
  }
6760
6901
  /** Where inside the platform a binding writes. */
@@ -6808,6 +6949,26 @@ function buildDestination(provider, options) {
6808
6949
  ...options.skipDeploys ? { skipDeploys: true } : {}
6809
6950
  };
6810
6951
  }
6952
+ case "aws-secrets-manager": {
6953
+ const layout = assertMember(options.layout, AWS_SECRETS_MANAGER_LAYOUTS, "--layout", "secret-per-name");
6954
+ if (layout === "json-bundle" && !options.secretName) fail("--secret-name is required for --layout json-bundle (the one secret to write)");
6955
+ return {
6956
+ provider: "aws-secrets-manager",
6957
+ layout,
6958
+ ...options.path ? { pathPrefix: options.path } : {},
6959
+ ...options.secretName ? { secretName: options.secretName } : {},
6960
+ ...options.kmsKeyId ? { kmsKeyId: options.kmsKeyId } : {}
6961
+ };
6962
+ }
6963
+ case "aws-parameter-store":
6964
+ if (!options.path) fail("--path is required for aws-parameter-store, e.g. /prod/storefront/");
6965
+ return {
6966
+ provider: "aws-parameter-store",
6967
+ path: options.path,
6968
+ type: assertMember(options.paramType, AWS_PARAMETER_TYPES, "--param-type", "SecureString"),
6969
+ tier: assertMember(options.tier, AWS_PARAMETER_TIERS, "--tier", "Standard"),
6970
+ ...options.kmsKeyId ? { kmsKeyId: options.kmsKeyId } : {}
6971
+ };
6811
6972
  }
6812
6973
  }
6813
6974
  /** One-line description of a destination, for list output. */
@@ -6818,6 +6979,8 @@ function describeDestination(destination) {
6818
6979
  case "cloudflare-pages": return `${destination.projectName} (${destination.environments.join(", ")})`;
6819
6980
  case "cloudflare-secrets-store": return `store ${destination.storeId} (${destination.scopes.join(", ")})`;
6820
6981
  case "railway": return `${destination.projectId} / ${destination.environmentId} (${destination.serviceId ? `service ${destination.serviceId}` : "shared"})`;
6982
+ case "aws-secrets-manager": return destination.layout === "json-bundle" ? `${destination.secretName} (json bundle)` : `${destination.pathPrefix ?? ""}* (secret per name)`;
6983
+ case "aws-parameter-store": return `${destination.path}* (${destination.type})`;
6821
6984
  }
6822
6985
  }
6823
6986
  /**
@@ -6825,7 +6988,7 @@ function describeDestination(destination) {
6825
6988
  * into accepting different ways of naming the same destination.
6826
6989
  */
6827
6990
  function destinationOptions(command) {
6828
- 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");
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");
6829
6992
  }
6830
6993
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
6831
6994
  async function resolveConnection(ctx, orgId, ref) {
@@ -6849,12 +7012,13 @@ function registerSyncCommands(program) {
6849
7012
  col("id", (c) => c.id)
6850
7013
  ], "no connections — add one with `seekrit sync connect`"));
6851
7014
  });
6852
- 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)").action(async (options) => {
7015
+ 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) => {
6853
7016
  const provider = assertProvider(options.provider);
6854
7017
  const ctx = buildContext();
6855
7018
  const ref = await resolveOrg(ctx, options.org);
6856
- const credential = (process.stdin.isTTY ? await promptHidden(`${provider} API token: `) : await readStdin()).trim();
6857
- if (!credential) fail("no API token given");
7019
+ const noun = provider.startsWith("aws-") ? "secret access key" : "API token";
7020
+ const credential = (process.stdin.isTTY ? await promptHidden(`${provider} ${noun}: `) : await readStdin()).trim();
7021
+ if (!credential) fail(`no ${noun} given`);
6858
7022
  const id = randomId("syc");
6859
7023
  const { publicKeyJwk } = await ctx.client.getSyncConnectionKey(ref.id, id);
6860
7024
  const created = await ctx.client.createSyncConnection(ref.id, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.35.0",
3
+ "version": "0.36.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,8 +27,8 @@
27
27
  "@types/node": "^26.1.0",
28
28
  "tsdown": "^0.22.3",
29
29
  "vitest": "^4.1.9",
30
- "@seekrit/core": "0.0.1",
31
30
  "@seekrit/api-client": "0.0.1",
31
+ "@seekrit/core": "0.0.1",
32
32
  "@seekrit/crypto": "0.0.1"
33
33
  },
34
34
  "scripts": {