@seekrit/cli 0.38.0 → 0.39.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 +193 -5
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1523,7 +1523,9 @@ const SYNC_PROVIDER_KINDS = [
1523
1523
  "aws-parameter-store",
1524
1524
  "render",
1525
1525
  "fly",
1526
- "northflank"
1526
+ "northflank",
1527
+ "digitalocean",
1528
+ "heroku"
1527
1529
  ];
1528
1530
  z.enum(SYNC_PROVIDER_KINDS);
1529
1531
  /**
@@ -1688,6 +1690,27 @@ const northflankIdSchema = z.string().trim().min(3).max(100).regex(/^[a-zA-Z0-9]
1688
1690
  * address. The kind exists so the discriminated union stays uniform.
1689
1691
  */
1690
1692
  const northflankConnectionConfigSchema = z.object({ provider: z.literal("northflank") });
1693
+ /**
1694
+ * DigitalOcean account scope — deliberately empty, as Render's and Fly's are.
1695
+ *
1696
+ * A DigitalOcean personal access token belongs to one account (or one team, if
1697
+ * it was issued inside one) and carries that scope itself, and every endpoint
1698
+ * this connector calls addresses its app by id. There is no team id to
1699
+ * disambiguate the way Vercel needs one: the token plus the destination's app
1700
+ * id is the whole address.
1701
+ */
1702
+ const digitalOceanConnectionConfigSchema = z.object({ provider: z.literal("digitalocean") });
1703
+ /**
1704
+ * Heroku account scope — empty, as Fly's and Render's are.
1705
+ *
1706
+ * A Heroku API token carries its user's access to every app and team they can
1707
+ * reach, and app names are globally unique, so the destination's app is the
1708
+ * whole address. There is no team id to state: unlike Vercel, where a personal
1709
+ * token 403s a team-owned project without `teamId`, Heroku resolves
1710
+ * `/apps/{app_id_or_name}` against everything the token can see, team-owned or
1711
+ * not.
1712
+ */
1713
+ const herokuConnectionConfigSchema = z.object({ provider: z.literal("heroku") });
1691
1714
  const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1692
1715
  vercelConnectionConfigSchema,
1693
1716
  cloudflareWorkersConnectionConfigSchema,
@@ -1698,7 +1721,9 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
1698
1721
  awsParameterStoreConnectionConfigSchema,
1699
1722
  renderConnectionConfigSchema,
1700
1723
  flyConnectionConfigSchema,
1701
- northflankConnectionConfigSchema
1724
+ northflankConnectionConfigSchema,
1725
+ digitalOceanConnectionConfigSchema,
1726
+ herokuConnectionConfigSchema
1702
1727
  ]);
1703
1728
  /** Vercel's three deployment targets. A binding writes to one or more. */
1704
1729
  const VERCEL_TARGETS = [
@@ -1942,6 +1967,117 @@ const northflankDestinationSchema = z.object({
1942
1967
  /** Secret group id — the slug in the group's URL (`example-secret-group`). */
1943
1968
  secretGroupId: northflankIdSchema
1944
1969
  });
1970
+ /**
1971
+ * When App Platform makes a variable visible. DigitalOcean's enum also has
1972
+ * `UNSET`, which is not offered: it means "no scope stated", and a secrets
1973
+ * manager that writes a value should say when that value applies.
1974
+ *
1975
+ * The default here is `RUN_TIME` rather than DigitalOcean's own
1976
+ * `RUN_AND_BUILD_TIME`, and the difference is deliberate. A build-time variable
1977
+ * is visible to every build command, every buildpack, and anything they print;
1978
+ * a secret only the running process needs has no business being there. Binding
1979
+ * a value a build genuinely needs — a private registry token, a sourcemap
1980
+ * upload key — is a decision worth making explicitly.
1981
+ */
1982
+ const DIGITALOCEAN_ENV_SCOPES = [
1983
+ "RUN_TIME",
1984
+ "BUILD_TIME",
1985
+ "RUN_AND_BUILD_TIME"
1986
+ ];
1987
+ /**
1988
+ * A DigitalOcean app id — the UUID in the app's dashboard URL
1989
+ * (`cloud.digitalocean.com/apps/<id>`), and what `doctl apps list` prints.
1990
+ *
1991
+ * DigitalOcean's own spec types this as a bare string, but every app id it
1992
+ * issues is a UUID, and the slip worth catching is the one the API cannot tell
1993
+ * from a typo: pasting the app's *name* (`storefront`), which `GET /v2/apps/{id}`
1994
+ * answers with a flat 404 hours later inside an alarm, with nobody watching.
1995
+ */
1996
+ const digitalOceanAppIdSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a DigitalOcean app ID (the UUID in the app's URL), not its name");
1997
+ /**
1998
+ * A component name, matching App Platform's own pattern for one. Names are
1999
+ * unique within an app, which is what makes a name — rather than an index into
2000
+ * `services` — the stable way to address a component's variables.
2001
+ */
2002
+ const digitalOceanComponentNameSchema = z.string().trim().regex(/^[a-z][a-z0-9-]{0,30}[a-z0-9]$/, "must be an App Platform component name (lowercase letters, numbers, and dashes)");
2003
+ /**
2004
+ * Where inside a DigitalOcean app a binding writes.
2005
+ *
2006
+ * ## A push is a deployment
2007
+ *
2008
+ * App Platform has no per-variable endpoint. Environment variables live in the
2009
+ * app spec, and the only way to change one is to submit a new spec — which
2010
+ * starts a **new deployment** of the app. That is not a side effect this
2011
+ * connector chose and there is no flag to suppress it; it is what "set an
2012
+ * environment variable" means on this platform, in the control panel and in
2013
+ * `doctl` alike.
2014
+ *
2015
+ * The deployment reuses each component's current source (seekrit never sends
2016
+ * `update_all_source_versions`), so it redeploys the code already running
2017
+ * rather than pulling a newer commit or image. It is still a real deployment:
2018
+ * a build, a health check, and a rollout. Bind an environment here knowing that
2019
+ * changing a secret in it will roll the app.
2020
+ *
2021
+ * ## Values are written encrypted
2022
+ *
2023
+ * Everything seekrit writes goes in as `type: SECRET`, so App Platform encrypts
2024
+ * it at rest and hands it back as an opaque `EV[1:…]` blob rather than as
2025
+ * plaintext. That is also why this connector cannot tell whether a value it is
2026
+ * about to write is already there — see
2027
+ * `apps/api/src/lib/sync/connectors/digitalocean.ts`.
2028
+ */
2029
+ const digitalOceanAppDestinationSchema = z.object({
2030
+ provider: z.literal("digitalocean"),
2031
+ kind: z.literal("app"),
2032
+ /** App id — the UUID in `cloud.digitalocean.com/apps/<id>`. */
2033
+ appId: digitalOceanAppIdSchema,
2034
+ scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
2035
+ });
2036
+ /**
2037
+ * One component's own environment variables. Narrower than the app-level list:
2038
+ * only this service, worker, job, static site, or function sees them, and a key
2039
+ * here wins over the same key at app level.
2040
+ */
2041
+ const digitalOceanComponentDestinationSchema = z.object({
2042
+ provider: z.literal("digitalocean"),
2043
+ kind: z.literal("component"),
2044
+ appId: digitalOceanAppIdSchema,
2045
+ /** Component name, as it appears in the app spec — not its type. */
2046
+ componentName: digitalOceanComponentNameSchema,
2047
+ scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
2048
+ });
2049
+ const digitalOceanDestinationSchema = z.discriminatedUnion("kind", [digitalOceanAppDestinationSchema, digitalOceanComponentDestinationSchema]);
2050
+ /**
2051
+ * A Heroku app, named the way `/apps/{app_id_or_name}` names one: either the
2052
+ * app name or its UUID id. Both are accepted because both work, and the id is
2053
+ * the durable one — renaming an app in the dashboard breaks a binding that
2054
+ * holds its name, and does not break one that holds its id.
2055
+ *
2056
+ * The name pattern is Heroku's own (`^[a-z][a-z0-9-]{1,28}[a-z0-9]$`): 3–30
2057
+ * characters, starting with a letter and ending alphanumeric. Checking it here
2058
+ * turns the habitual slip — pasting `example.herokuapp.com`, or a name with
2059
+ * capitals — into a message at the form rather than a bare 404 from an alarm
2060
+ * with nobody watching.
2061
+ */
2062
+ const herokuAppSchema = z.string().trim().refine((value) => /^[a-z][a-z0-9-]{1,28}[a-z0-9]$/.test(value) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), "must be a Heroku app name (lowercase letters, numbers, and dashes) or its UUID");
2063
+ /**
2064
+ * The Heroku app whose config vars a binding owns.
2065
+ *
2066
+ * A Heroku app has **one** set of config vars, shared by every dyno and every
2067
+ * process type — there is no per-target split to state the way Vercel and Pages
2068
+ * have one. Heroku's convention is that staging and production are separate
2069
+ * *apps* (`storefront`, `storefront-staging`), so pointing at an environment
2070
+ * means naming that app, exactly as it does on Fly.
2071
+ *
2072
+ * Unlike Fly, values take effect **immediately**: setting config vars cuts a new
2073
+ * release and restarts the app's dynos, which is why a run sends exactly one
2074
+ * request — see the note in `apps/api/src/lib/sync/connectors/heroku.ts`.
2075
+ */
2076
+ const herokuDestinationSchema = z.object({
2077
+ provider: z.literal("heroku"),
2078
+ /** App name as `heroku apps` prints it, or the app's UUID. */
2079
+ app: herokuAppSchema
2080
+ });
1945
2081
  const syncDestinationSchema = z.discriminatedUnion("provider", [
1946
2082
  vercelDestinationSchema,
1947
2083
  cloudflareWorkersDestinationSchema,
@@ -1952,7 +2088,9 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
1952
2088
  awsParameterStoreDestinationSchema,
1953
2089
  renderDestinationSchema,
1954
2090
  flyDestinationSchema,
1955
- northflankDestinationSchema
2091
+ northflankDestinationSchema,
2092
+ digitalOceanDestinationSchema,
2093
+ herokuDestinationSchema
1956
2094
  ]);
1957
2095
  /**
1958
2096
  * How seekrit secret names become destination key names. Applied in order:
@@ -3157,7 +3295,7 @@ function isCliSessionToken(value) {
3157
3295
  }
3158
3296
  //#endregion
3159
3297
  //#region package.json
3160
- var version = "0.38.0";
3298
+ var version = "0.39.0";
3161
3299
  //#endregion
3162
3300
  //#region ../../packages/api-client/src/index.ts
3163
3301
  var SeekritApiError = class extends Error {
@@ -6998,6 +7136,19 @@ function assertFlyApp(value) {
6998
7136
  return appName;
6999
7137
  }
7000
7138
  /**
7139
+ * Heroku takes an app name or the app's UUID, so both are accepted here. The
7140
+ * habitual slips are the hostname (`storefront.herokuapp.com`) and capitals;
7141
+ * both are a 404 from Heroku much later, so they are caught here with a message
7142
+ * that says which.
7143
+ */
7144
+ function assertHerokuApp(value) {
7145
+ if (!value) fail("--heroku-app is required for heroku (the app name, e.g. storefront-production)");
7146
+ const app = value.trim();
7147
+ if (/\.(herokuapp\.com|herokudns\.com)$/.test(app)) fail(`--heroku-app takes the app name, not its hostname — try "${app.split(".")[0]}"`);
7148
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(app) && !/^[a-z][a-z0-9-]{1,28}[a-z0-9]$/.test(app)) fail(`--heroku-app "${app}" is not a Heroku app name — 3 to 30 characters, starting with a lowercase letter, made of lowercase letters, numbers, and dashes (or the app's UUID)`);
7149
+ return app;
7150
+ }
7151
+ /**
7001
7152
  * Northflank ids are slugs from the resource's URL, so the slip to catch is the
7002
7153
  * *display name* — "App Secrets" where "app-secrets" belongs.
7003
7154
  */
@@ -7007,6 +7158,16 @@ function assertNorthflankId(value, flag) {
7007
7158
  if (!/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/.test(id)) fail(`${flag} should be a Northflank ID like "app-secrets", not "${id}"`);
7008
7159
  return id;
7009
7160
  }
7161
+ /**
7162
+ * DigitalOcean app ids are UUIDs, and the slip to catch is the app *name* —
7163
+ * what the dashboard headline shows, and what the API answers with a bare 404.
7164
+ */
7165
+ function assertDigitalOceanApp(value) {
7166
+ if (!value) fail("--do-app is required for digitalocean (the UUID in the app's URL)");
7167
+ const appId = value.trim();
7168
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(appId)) fail(`--do-app should be the app's UUID, from its URL — not "${appId}"`);
7169
+ return appId;
7170
+ }
7010
7171
  function assertProvider(value) {
7011
7172
  if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
7012
7173
  return value;
@@ -7046,6 +7207,8 @@ function buildConfig(provider, options) {
7046
7207
  case "render": return { provider: "render" };
7047
7208
  case "fly": return { provider: "fly" };
7048
7209
  case "northflank": return { provider: "northflank" };
7210
+ case "digitalocean": return { provider: "digitalocean" };
7211
+ case "heroku": return { provider: "heroku" };
7049
7212
  }
7050
7213
  }
7051
7214
  /** Where inside the platform a binding writes. */
@@ -7144,6 +7307,29 @@ function buildDestination(provider, options) {
7144
7307
  projectId: assertNorthflankId(options.project, "--project"),
7145
7308
  secretGroupId: assertNorthflankId(options.secretGroup, "--secret-group")
7146
7309
  };
7310
+ case "digitalocean": {
7311
+ const appId = assertDigitalOceanApp(options.doApp);
7312
+ const scope = assertMember(options.envScope, DIGITALOCEAN_ENV_SCOPES, "--env-scope", "RUN_TIME");
7313
+ const componentName = options.component?.trim();
7314
+ if (!componentName) return {
7315
+ provider: "digitalocean",
7316
+ kind: "app",
7317
+ appId,
7318
+ scope
7319
+ };
7320
+ if (!/^[a-z][a-z0-9-]{0,30}[a-z0-9]$/.test(componentName)) fail(`--component "${componentName}" is not an App Platform component name (lowercase letters, numbers, and dashes)`);
7321
+ return {
7322
+ provider: "digitalocean",
7323
+ kind: "component",
7324
+ appId,
7325
+ componentName,
7326
+ scope
7327
+ };
7328
+ }
7329
+ case "heroku": return {
7330
+ provider: "heroku",
7331
+ app: assertHerokuApp(options.herokuApp)
7332
+ };
7147
7333
  }
7148
7334
  }
7149
7335
  /** One-line description of a destination, for list output. */
@@ -7159,6 +7345,8 @@ function describeDestination(destination) {
7159
7345
  case "render": return destination.kind === "service" ? destination.serviceId : `env group ${destination.envGroupId}`;
7160
7346
  case "fly": return destination.appName;
7161
7347
  case "northflank": return `${destination.projectId} / ${destination.secretGroupId}`;
7348
+ case "digitalocean": return `${destination.appId}${destination.kind === "component" ? ` / ${destination.componentName}` : ""} (${destination.scope})`;
7349
+ case "heroku": return destination.app;
7162
7350
  }
7163
7351
  }
7164
7352
  /**
@@ -7169,7 +7357,7 @@ function describeDestination(destination) {
7169
7357
  * application whose environment the binding reads from.
7170
7358
  */
7171
7359
  function destinationOptions(command) {
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)");
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)");
7173
7361
  }
7174
7362
  /** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
7175
7363
  async function resolveConnection(ctx, orgId, ref) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.38.0",
3
+ "version": "0.39.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",
31
30
  "@seekrit/core": "0.0.1",
32
- "@seekrit/crypto": "0.0.1"
31
+ "@seekrit/crypto": "0.0.1",
32
+ "@seekrit/api-client": "0.0.1"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsdown",