@seekrit/cli 0.14.0 → 0.16.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 +502 -7
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -12,7 +12,9 @@ z.enum([
12
12
  "mysql",
13
13
  "ssh",
14
14
  "redis",
15
- "aws"
15
+ "aws",
16
+ "gcp",
17
+ "mongodb"
16
18
  ]);
17
19
  const executorModeSchema = z.enum(["in_do", "remote"]);
18
20
  /**
@@ -83,6 +85,22 @@ const awsRegionSchema = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be a
83
85
  const awsExternalIdSchema = z.string().regex(/^[\w+=,.@:/-]{2,1224}$/, "must be a valid STS external id");
84
86
  z.string().regex(/^[\w+=,.@-]{2,64}$/, "must be 2–64 chars of [A-Za-z0-9_+=,.@-]");
85
87
  /**
88
+ * A GCP service-account email the broker is allowed to impersonate (or that
89
+ * appears in a delegation chain). Structurally validated and bounded: it is
90
+ * interpolated into the IAM Credentials API URL path, so the charset excludes
91
+ * anything that could break out of a path segment. Covers user-managed
92
+ * (`name@<project>.iam.gserviceaccount.com`) and Google-managed
93
+ * (`<project-number>-compute@developer.gserviceaccount.com`) forms.
94
+ */
95
+ const gcpServiceAccountEmailSchema = z.string().max(256).regex(/^[a-z0-9-]+@[a-z0-9.-]+\.gserviceaccount\.com$/, "must be a service-account email (…@….gserviceaccount.com)");
96
+ /**
97
+ * An OAuth 2.0 scope granted to the minted access token, e.g.
98
+ * `https://www.googleapis.com/auth/cloud-platform`. Bounded and whitespace-free
99
+ * (scopes are space-delimited); passed to the IAM Credentials API in a JSON body
100
+ * array, not a URL, so this is sanity/DoS hardening rather than an injection gate.
101
+ */
102
+ const gcpOauthScopeSchema = z.string().min(1).max(256).regex(/^\S+$/, "must be a single OAuth scope with no whitespace");
103
+ /**
86
104
  * The consumer's ephemeral P-256 public key (JWK-serialized) that a tier-2
87
105
  * credential is wrapped to before it is returned. Validated structurally here;
88
106
  * the executor imports it defensively before wrapping. Bounded so a giant blob
@@ -96,6 +114,22 @@ const p256PublicKeyJwkSchema = z.string().max(2048).refine((s) => {
96
114
  return false;
97
115
  }
98
116
  }, "must be a JWK-serialized P-256 public key");
117
+ z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1–64 chars of [A-Za-z0-9_-]");
118
+ /**
119
+ * A MongoDB database name — the db a preset role is granted on, or the
120
+ * authentication database a leased user is created in. MongoDB forbids
121
+ * `/\. "$*<>:|?` and the empty string in db names; we keep to a safe subset.
122
+ */
123
+ const mongoDatabaseNameSchema = z.string().regex(/^[A-Za-z0-9_-]{1,63}$/, "must be 1–63 chars of [A-Za-z0-9_-]");
124
+ /**
125
+ * A single MongoDB role grant `{ role, db }` for a `custom` target — e.g.
126
+ * `{ role: "readWrite", db: "app" }` or a user-defined role. Admin-supplied
127
+ * trusted input, set once at registration; still bounded structurally.
128
+ */
129
+ const mongoRoleSchema = z.object({
130
+ role: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be a role name"),
131
+ db: mongoDatabaseNameSchema
132
+ });
99
133
  const postgresAccessLevelSchema = z.enum([
100
134
  "readonly",
101
135
  "readwrite",
@@ -116,6 +150,11 @@ const redisAccessLevelSchema = z.enum([
116
150
  "readwrite",
117
151
  "custom"
118
152
  ]);
153
+ const mongoAccessLevelSchema = z.enum([
154
+ "readonly",
155
+ "readwrite",
156
+ "custom"
157
+ ]);
119
158
  const connectionSchema = z.object({
120
159
  host: z.string().min(1),
121
160
  port: z.number().int().min(1).max(65535),
@@ -184,12 +223,33 @@ const awsTargetConfigSchema = z.object({
184
223
  sessionPolicy: z.string().min(1).max(4e3).optional(),
185
224
  maxTtlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS).optional()
186
225
  });
226
+ const GCP_MAX_TTL_SECONDS = 3600 * 12;
227
+ const gcpTargetConfigSchema = z.object({
228
+ provider: z.literal("gcp"),
229
+ executor: z.literal("in_do"),
230
+ serviceAccount: gcpServiceAccountEmailSchema,
231
+ scopes: z.array(gcpOauthScopeSchema).min(1).max(32).optional(),
232
+ delegates: z.array(gcpServiceAccountEmailSchema).max(8).optional(),
233
+ maxTtlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
234
+ });
235
+ const mongoTargetConfigSchema = z.object({
236
+ provider: z.literal("mongodb"),
237
+ executor: z.literal("in_do"),
238
+ accessLevel: mongoAccessLevelSchema.optional(),
239
+ connection: connectionSchema,
240
+ authSource: mongoDatabaseNameSchema.optional(),
241
+ roles: z.array(mongoRoleSchema).min(1).max(32).optional(),
242
+ tls: z.boolean().optional(),
243
+ maxTtlSeconds: z.number().int().min(60).max(3600 * 24 * 7).optional()
244
+ });
187
245
  const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
188
246
  postgresTargetConfigSchema,
189
247
  mysqlTargetConfigSchema,
190
248
  redisTargetConfigSchema,
191
249
  sshTargetConfigSchema,
192
- awsTargetConfigSchema
250
+ awsTargetConfigSchema,
251
+ gcpTargetConfigSchema,
252
+ mongoTargetConfigSchema
193
253
  ]);
194
254
  z.object({
195
255
  name: z.string().trim().min(1).max(128),
@@ -266,12 +326,43 @@ const mintAwsLeaseSchema = z.object({
266
326
  recipientPublicKey: p256PublicKeyJwkSchema,
267
327
  ttlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS)
268
328
  });
329
+ /**
330
+ * Client → API: mint a GCP lease. Like AWS (tier 2): the client generates an
331
+ * ephemeral P-256 keypair locally and sends only the public key; the IAM
332
+ * Credentials API mints the access token and the broker returns it wrapped to
333
+ * that key. The private key never leaves the requesting machine, so the plaintext
334
+ * token is only decryptable there.
335
+ *
336
+ * TTL bounds are GCP's `generateAccessToken` limits (1 min – 12 h); tokens over
337
+ * 1 h require the credential-lifetime-extension org policy.
338
+ */
339
+ const mintGcpLeaseSchema = z.object({
340
+ provider: z.literal("gcp"),
341
+ targetId: z.string().min(1),
342
+ recipientPublicKey: p256PublicKeyJwkSchema,
343
+ ttlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
344
+ });
345
+ /**
346
+ * Client → API: mint a MongoDB lease. Like AWS (tier 2), the client generates
347
+ * an ephemeral P-256 keypair locally and sends only the public key; the broker
348
+ * generates the password, runs `createUser`, and returns the credential wrapped
349
+ * to that key. The private key never leaves the requesting machine, so the
350
+ * plaintext credential is only decryptable there.
351
+ */
352
+ const mintMongoLeaseSchema = z.object({
353
+ provider: z.literal("mongodb"),
354
+ targetId: z.string().min(1),
355
+ recipientPublicKey: p256PublicKeyJwkSchema,
356
+ ttlSeconds: ttlSecondsSchema
357
+ });
269
358
  z.discriminatedUnion("provider", [
270
359
  mintPostgresLeaseSchema,
271
360
  mintMysqlLeaseSchema,
272
361
  mintRedisLeaseSchema,
273
362
  mintSshLeaseSchema,
274
- mintAwsLeaseSchema
363
+ mintAwsLeaseSchema,
364
+ mintGcpLeaseSchema,
365
+ mintMongoLeaseSchema
275
366
  ]);
276
367
  //#endregion
277
368
  //#region ../../packages/core/src/providers/aws.ts
@@ -302,6 +393,55 @@ function awsTrustPolicyInstructions(config) {
302
393
  ].join("\n");
303
394
  }
304
395
  //#endregion
396
+ //#region ../../packages/core/src/providers/gcp.ts
397
+ /**
398
+ * The one-time IAM setup an admin performs so seekrit can impersonate the target
399
+ * service account. Analogue of `awsTrustPolicyInstructions` — printed for the
400
+ * admin to apply, never executed by seekrit. Grants the *source* service account
401
+ * (the one whose key was registered as the admin secret) the token-creator role
402
+ * on the target service account.
403
+ */
404
+ function gcpSetupInstructions(config) {
405
+ return [
406
+ "# Grant the service account whose key you registered as the admin secret",
407
+ "# permission to mint tokens for the target service account. Replace",
408
+ "# <SOURCE_SA_EMAIL> with the client_email from that key JSON.",
409
+ `gcloud iam service-accounts add-iam-policy-binding ${config.serviceAccount} \\`,
410
+ " --member=\"serviceAccount:<SOURCE_SA_EMAIL>\" \\",
411
+ " --role=\"roles/iam.serviceAccountTokenCreator\"",
412
+ "",
413
+ "# The source service account's project must have the IAM Service Account",
414
+ "# Credentials API enabled (seekrit only ever calls generateAccessToken):",
415
+ "gcloud services enable iamcredentials.googleapis.com"
416
+ ].join("\n");
417
+ }
418
+ //#endregion
419
+ //#region ../../packages/core/src/providers/mongodb.ts
420
+ /**
421
+ * The one-time setup an admin performs so seekrit can provision users: create a
422
+ * dedicated provisioning user with `userAdmin` on the target database (or
423
+ * `userAdminAnyDatabase` if leases span databases). Printed for the admin to run
424
+ * in `mongosh`, never executed by seekrit — the analogue of
425
+ * `awsTrustPolicyInstructions` / `sshHostSetupInstructions`.
426
+ */
427
+ function mongoAdminSetupInstructions(config) {
428
+ const authSource = config.authSource ?? "admin";
429
+ const db = config.connection.database;
430
+ return [
431
+ `// Run once in mongosh against the "${authSource}" database. Creates the`,
432
+ "// provisioning user whose connection string you register as the admin secret.",
433
+ `use ${authSource}`,
434
+ "db.createUser({",
435
+ " user: \"seekrit_provisioner\",",
436
+ " pwd: passwordPrompt(),",
437
+ ` roles: [{ role: "userAdmin", db: "${db}" }],`,
438
+ "})",
439
+ "",
440
+ `// Register it: seekrit mongodb target add --name ${db} \\`,
441
+ `// --uri "mongodb://seekrit_provisioner:<pwd>@${config.connection.host}:${config.connection.port}/?authSource=${authSource}"`
442
+ ].join("\n");
443
+ }
444
+ //#endregion
305
445
  //#region ../../packages/core/src/providers/postgres.ts
306
446
  /**
307
447
  * The one-time setup SQL an admin runs to create the shared group role that a
@@ -378,6 +518,8 @@ const NOTIFICATION_TYPES = [
378
518
  /** URL-safe identifier segment: `my-app`, `production`, … */
379
519
  const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
380
520
  const nameSchema = z.string().trim().min(1).max(128);
521
+ /** An email address, normalized to trimmed lowercase before validation. */
522
+ const emailSchema = z.string().trim().toLowerCase().pipe(z.email().max(320));
381
523
  /** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
382
524
  const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
383
525
  z.enum([
@@ -385,6 +527,8 @@ z.enum([
385
527
  "admin",
386
528
  "member"
387
529
  ]);
530
+ /** Role a person may be invited at — never `owner` (ownership isn't invitable). */
531
+ const inviteRoleSchema = z.enum(["admin", "member"]);
388
532
  const principalTypeSchema = z.enum(["user", "service_token"]);
389
533
  /** Org-level capability a service token can hold (never `owner`). */
390
534
  const serviceTokenRoleSchema = z.enum(["admin", "member"]);
@@ -396,6 +540,10 @@ z.object({
396
540
  name: nameSchema,
397
541
  slug: slugSchema
398
542
  });
543
+ z.object({
544
+ email: emailSchema,
545
+ role: inviteRoleSchema.default("member")
546
+ });
399
547
  z.object({
400
548
  name: nameSchema,
401
549
  slug: slugSchema
@@ -690,6 +838,68 @@ async function unwrapAwsCredential(wrapped, privateKeyJwk) {
690
838
  };
691
839
  }
692
840
  //#endregion
841
+ //#region ../../packages/crypto/src/gcp.ts
842
+ /** Generate the ephemeral P-256 keypair a client uses to receive one GCP lease. */
843
+ async function generateGcpRecipientKeyPair() {
844
+ return generateKeyPair();
845
+ }
846
+ /**
847
+ * Unwrap the `wd1.` blob returned by a GCP lease mint using the ephemeral
848
+ * private key generated for that lease, yielding the plaintext access token.
849
+ */
850
+ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
851
+ const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
852
+ let parsed;
853
+ try {
854
+ parsed = JSON.parse(utf8Decode(bytes));
855
+ } catch {
856
+ throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential was not valid JSON");
857
+ }
858
+ const c = parsed;
859
+ if (typeof c.accessToken !== "string" || typeof c.expiration !== "string" || typeof c.serviceAccount !== "string" || !Array.isArray(c.scopes) || !c.scopes.every((s) => typeof s === "string")) throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential is missing fields");
860
+ return {
861
+ accessToken: c.accessToken,
862
+ expiration: c.expiration,
863
+ serviceAccount: c.serviceAccount,
864
+ scopes: c.scopes
865
+ };
866
+ }
867
+ //#endregion
868
+ //#region ../../packages/crypto/src/mongodb.ts
869
+ /** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
870
+ async function generateMongoRecipientKeyPair() {
871
+ return generateKeyPair();
872
+ }
873
+ /**
874
+ * Unwrap the `wd1.` blob returned by a MongoDB lease mint using the ephemeral
875
+ * private key generated for that lease, yielding the plaintext credential.
876
+ */
877
+ async function unwrapMongoCredential(wrapped, privateKeyJwk) {
878
+ const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
879
+ let parsed;
880
+ try {
881
+ parsed = JSON.parse(utf8Decode(bytes));
882
+ } catch {
883
+ throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential was not valid JSON");
884
+ }
885
+ const c = parsed;
886
+ if (typeof c.username !== "string" || typeof c.password !== "string" || typeof c.database !== "string" || typeof c.authSource !== "string" || typeof c.host !== "string" || typeof c.port !== "number" || typeof c.expiration !== "string") throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential is missing fields");
887
+ return {
888
+ username: c.username,
889
+ password: c.password,
890
+ database: c.database,
891
+ authSource: c.authSource,
892
+ host: c.host,
893
+ port: c.port,
894
+ expiration: c.expiration,
895
+ uri: mongoConnectionUri(c)
896
+ };
897
+ }
898
+ /** Build a `mongodb://user:pass@host:port/db?authSource=…` URI from a credential. */
899
+ function mongoConnectionUri(cred) {
900
+ return `mongodb://${encodeURIComponent(cred.username)}:${encodeURIComponent(cred.password)}@${cred.host}:${cred.port}/${cred.database}?authSource=${cred.authSource}`;
901
+ }
902
+ //#endregion
693
903
  //#region ../../packages/crypto/src/mysql.ts
694
904
  /**
695
905
  * Client-side construction of a MySQL/MariaDB `mysql_native_password`
@@ -1134,7 +1344,7 @@ function isServiceToken(value) {
1134
1344
  }
1135
1345
  //#endregion
1136
1346
  //#region package.json
1137
- var version = "0.14.0";
1347
+ var version = "0.16.0";
1138
1348
  //#endregion
1139
1349
  //#region ../../packages/api-client/src/index.ts
1140
1350
  var SeekritApiError = class extends Error {
@@ -1207,6 +1417,15 @@ var SeekritClient = class {
1207
1417
  listMembers(orgId) {
1208
1418
  return this.request("GET", `/v1/orgs/${orgId}/members`);
1209
1419
  }
1420
+ listInvites(orgId) {
1421
+ return this.request("GET", `/v1/orgs/${orgId}/invites`);
1422
+ }
1423
+ createInvite(orgId, input) {
1424
+ return this.request("POST", `/v1/orgs/${orgId}/invites`, input);
1425
+ }
1426
+ revokeInvite(orgId, inviteId) {
1427
+ return this.request("DELETE", `/v1/orgs/${orgId}/invites/${inviteId}`);
1428
+ }
1210
1429
  listApps(orgId) {
1211
1430
  return this.request("GET", `/v1/orgs/${orgId}/apps`);
1212
1431
  }
@@ -1583,7 +1802,7 @@ async function resolveGroup(ctx, opts) {
1583
1802
  * `sts:AssumeRole` on the target role.
1584
1803
  */
1585
1804
  /** Parse a duration like `30m`, `1h`, or a bare seconds count. */
1586
- function parseTtlSeconds$4(input) {
1805
+ function parseTtlSeconds$6(input) {
1587
1806
  const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
1588
1807
  if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
1589
1808
  return Number(m[1]) * ({
@@ -1623,7 +1842,7 @@ function registerAwsCommands(program) {
1623
1842
  region: options.region,
1624
1843
  ...options.externalId ? { externalId: options.externalId } : {},
1625
1844
  ...sessionPolicy ? { sessionPolicy } : {},
1626
- ...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$4(options.maxTtl) } : {}
1845
+ ...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
1627
1846
  };
1628
1847
  const baseCredential = resolveBaseCredential(options);
1629
1848
  const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
@@ -1671,7 +1890,7 @@ function registerAwsCommands(program) {
1671
1890
  if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
1672
1891
  const cfg = t.config;
1673
1892
  if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
1674
- const ttlSeconds = parseTtlSeconds$4(options.ttl);
1893
+ const ttlSeconds = parseTtlSeconds$6(options.ttl);
1675
1894
  if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
1676
1895
  const recipient = await generateAwsRecipientKeyPair();
1677
1896
  const { aws: leased } = await ctx.client.mintLease(org.id, {
@@ -1761,6 +1980,280 @@ function formatSecrets(values, format) {
1761
1980
  }
1762
1981
  }
1763
1982
  //#endregion
1983
+ //#region src/gcp.ts
1984
+ /**
1985
+ * `seekrit gcp` — temporary GCP credentials via IAM Credentials
1986
+ * `generateAccessToken` (Vault-style dynamic secrets, the tier-2 sibling of
1987
+ * `seekrit aws`).
1988
+ *
1989
+ * Zero-knowledge for the leased credential: minting generates an ephemeral P-256
1990
+ * keypair on THIS machine and sends only the public key; GCP mints the token and
1991
+ * the broker returns it wrapped to that key, so the control plane only ever
1992
+ * relays ciphertext and only this machine can unwrap it. Registering a target
1993
+ * wraps the service-account key JSON to the broker's public key locally, so the
1994
+ * control plane never sees it either — the source service account needs only
1995
+ * `roles/iam.serviceAccountTokenCreator` on the target.
1996
+ */
1997
+ /** Parse a duration like `30m`, `1h`, or a bare seconds count. */
1998
+ function parseTtlSeconds$5(input) {
1999
+ const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
2000
+ if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
2001
+ return Number(m[1]) * ({
2002
+ s: 1,
2003
+ m: 60,
2004
+ h: 3600,
2005
+ d: 86400
2006
+ }[m[2] || "s"] ?? 1);
2007
+ }
2008
+ /** Collect a repeatable flag (e.g. --scope) into a list. */
2009
+ function collectList$1(value, acc = []) {
2010
+ acc.push(value);
2011
+ return acc;
2012
+ }
2013
+ /**
2014
+ * The service-account key JSON the broker impersonates with. From --key-file or
2015
+ * GOOGLE_APPLICATION_CREDENTIALS. Never leaves this machine unwrapped — it is
2016
+ * wrapped to the broker key before upload.
2017
+ */
2018
+ function resolveServiceAccountKey(opts) {
2019
+ const path = opts.keyFile ?? process.env.GOOGLE_APPLICATION_CREDENTIALS;
2020
+ if (!path) fail("provide the source service-account key JSON via --key-file or GOOGLE_APPLICATION_CREDENTIALS (it needs roles/iam.serviceAccountTokenCreator on the target)");
2021
+ const raw = readFileSync(path, "utf8").trim();
2022
+ try {
2023
+ const parsed = JSON.parse(raw);
2024
+ if (typeof parsed.client_email !== "string" || typeof parsed.private_key !== "string") fail(`${path} is not a service-account key JSON (missing client_email/private_key)`);
2025
+ } catch {
2026
+ fail(`${path} is not valid JSON`);
2027
+ }
2028
+ return raw;
2029
+ }
2030
+ function registerGcpCommands(program) {
2031
+ const gcp = program.command("gcp").description("temporary GCP credentials (IAM generateAccessToken, zero-knowledge)");
2032
+ const target = gcp.command("target").description("manage GCP service-account targets");
2033
+ target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
2034
+ const ctx = buildContext();
2035
+ const org = await resolveOrg(ctx, options.org);
2036
+ const config = {
2037
+ provider: "gcp",
2038
+ executor: "in_do",
2039
+ serviceAccount: options.serviceAccount,
2040
+ ...options.scope?.length ? { scopes: options.scope } : {},
2041
+ ...options.delegate?.length ? { delegates: options.delegate } : {},
2042
+ ...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$5(options.maxTtl) } : {}
2043
+ };
2044
+ const keyJson = resolveServiceAccountKey(options);
2045
+ const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
2046
+ const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(keyJson), publicKeyJwk);
2047
+ const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
2048
+ name: options.name,
2049
+ config,
2050
+ wrappedAdminSecret
2051
+ });
2052
+ console.error(`registered GCP target ${created.name} (${created.id})`);
2053
+ console.error("\nGrant the source SA the token-creator role, then `seekrit gcp lease`:\n");
2054
+ console.log(gcpSetupInstructions(config));
2055
+ });
2056
+ target.command("list").description("list GCP service-account targets").option("--org <slug>").action(async (options) => {
2057
+ const ctx = buildContext();
2058
+ const org = await resolveOrg(ctx, options.org);
2059
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
2060
+ for (const t of targets) {
2061
+ const cfg = t.config;
2062
+ if (cfg.provider !== "gcp") continue;
2063
+ console.log(`${t.id}\t${t.name}\t${cfg.serviceAccount}`);
2064
+ }
2065
+ });
2066
+ target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>").action(async (targetId, options) => {
2067
+ const ctx = buildContext();
2068
+ const org = await resolveOrg(ctx, options.org);
2069
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
2070
+ const t = targets.find((x) => x.id === targetId || x.name === targetId);
2071
+ if (!t) fail(`no target "${targetId}" in ${org.slug}`);
2072
+ const cfg = t.config;
2073
+ if (cfg.provider !== "gcp") fail("not a gcp target (see `seekrit aws`/`seekrit ssh`)");
2074
+ console.log(gcpSetupInstructions(cfg));
2075
+ });
2076
+ target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>").action(async (targetId, options) => {
2077
+ const ctx = buildContext();
2078
+ const org = await resolveOrg(ctx, options.org);
2079
+ await ctx.client.deleteLeaseTarget(org.id, targetId);
2080
+ console.error(`deleted ${targetId}`);
2081
+ });
2082
+ gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
2083
+ const ctx = buildContext();
2084
+ const org = await resolveOrg(ctx, options.org);
2085
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
2086
+ const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
2087
+ if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
2088
+ if (t.config.provider !== "gcp") fail(`"${t.name}" is not a gcp target (see \`seekrit aws\`)`);
2089
+ const ttlSeconds = parseTtlSeconds$5(options.ttl);
2090
+ if (ttlSeconds < 60) fail(`--ttl must be at least 60s`);
2091
+ if (ttlSeconds > 43200) fail(`--ttl must be at most ${GCP_MAX_TTL_SECONDS / 3600}h`);
2092
+ const recipient = await generateGcpRecipientKeyPair();
2093
+ const { gcp: leased } = await ctx.client.mintLease(org.id, {
2094
+ provider: "gcp",
2095
+ targetId: t.id,
2096
+ recipientPublicKey: recipient.publicKeyJwk,
2097
+ ttlSeconds
2098
+ });
2099
+ const cred = await unwrapGcpCredential(leased.wrappedCredential, recipient.privateKeyJwk);
2100
+ console.error(`leased ${cred.serviceAccount} — expires ${cred.expiration}`);
2101
+ if (options.json) console.log(JSON.stringify(cred, null, 2));
2102
+ else {
2103
+ console.log(`export CLOUDSDK_AUTH_ACCESS_TOKEN=${cred.accessToken}`);
2104
+ console.log(`export GOOGLE_OAUTH_ACCESS_TOKEN=${cred.accessToken}`);
2105
+ }
2106
+ });
2107
+ gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
2108
+ const ctx = buildContext();
2109
+ const org = await resolveOrg(ctx, options.org);
2110
+ const { leases } = await ctx.client.listLeases(org.id);
2111
+ for (const l of leases) {
2112
+ if (l.provider !== "gcp") continue;
2113
+ console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
2114
+ }
2115
+ });
2116
+ gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
2117
+ const ctx = buildContext();
2118
+ const org = await resolveOrg(ctx, options.org);
2119
+ await ctx.client.revokeLease(org.id, leaseId);
2120
+ console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
2121
+ });
2122
+ }
2123
+ //#endregion
2124
+ //#region src/mongodb.ts
2125
+ /**
2126
+ * `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
2127
+ * secrets), the tier-2 sibling of `seekrit aws`.
2128
+ *
2129
+ * MongoDB hashes the password server-side (no verifier injection), so minting
2130
+ * generates an ephemeral P-256 keypair on THIS machine and sends only the public
2131
+ * key; the broker creates the user and returns the credential wrapped to that
2132
+ * key, so the control plane only ever relays ciphertext and only this machine
2133
+ * can unwrap it. Registering a target wraps the admin connection string to the
2134
+ * broker's public key locally, so the control plane never sees it either.
2135
+ */
2136
+ /** Parse a duration like `30m`, `1h`, or a bare seconds count. */
2137
+ function parseTtlSeconds$4(input) {
2138
+ const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
2139
+ if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 8h)`);
2140
+ return Number(m[1]) * ({
2141
+ s: 1,
2142
+ m: 60,
2143
+ h: 3600,
2144
+ d: 86400
2145
+ }[m[2] || "s"] ?? 1);
2146
+ }
2147
+ /** Collect a repeatable option into an array. */
2148
+ function collect$4(value, previous) {
2149
+ return [...previous, value];
2150
+ }
2151
+ /** Parse `readWrite@app` → { role, db } for a custom target. */
2152
+ function parseRole(spec) {
2153
+ const [role, db] = spec.split("@");
2154
+ if (!role || !db) fail(`invalid --role "${spec}" (use role@database, e.g. readWrite@app)`);
2155
+ return {
2156
+ role,
2157
+ db
2158
+ };
2159
+ }
2160
+ /**
2161
+ * The admin connection string the broker provisions with. From `--uri` or
2162
+ * SEEKRIT_MONGODB_ADMIN_URL; never leaves this machine unwrapped — it is wrapped
2163
+ * to the broker key. Needs `userAdmin` on the target database.
2164
+ */
2165
+ function resolveAdminUri(uri) {
2166
+ const value = uri ?? process.env.SEEKRIT_MONGODB_ADMIN_URL;
2167
+ if (!value) fail("provide the admin connection string via --uri or SEEKRIT_MONGODB_ADMIN_URL (mongodb://user:pass@host:port/?authSource=admin; needs userAdmin on the database)");
2168
+ return value;
2169
+ }
2170
+ function registerMongoCommands(program) {
2171
+ const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
2172
+ const target = mongo.command("target").description("manage MongoDB targets");
2173
+ target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
2174
+ const ctx = buildContext();
2175
+ const org = await resolveOrg(ctx, options.org);
2176
+ const adminUri = resolveAdminUri(options.uri);
2177
+ const url = new URL(adminUri);
2178
+ const accessLevel = options.access;
2179
+ if (accessLevel === "custom" && options.role.length === 0) fail("--access custom requires at least one --role role@db");
2180
+ const config = {
2181
+ provider: "mongodb",
2182
+ executor: "in_do",
2183
+ accessLevel,
2184
+ connection: {
2185
+ host: url.hostname,
2186
+ port: url.port ? Number(url.port) : 27017,
2187
+ database: options.database
2188
+ },
2189
+ ...options.authSource ? { authSource: options.authSource } : {},
2190
+ ...accessLevel === "custom" ? { roles: options.role.map(parseRole) } : {},
2191
+ ...options.tls ? {} : { tls: false }
2192
+ };
2193
+ const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
2194
+ const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminUri), publicKeyJwk);
2195
+ const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
2196
+ name: options.name,
2197
+ config,
2198
+ wrappedAdminSecret
2199
+ });
2200
+ console.error(`registered MongoDB target ${created.name} (${created.id})`);
2201
+ console.error("\nEnsure a provisioning user exists, then `seekrit mongodb lease`:\n");
2202
+ console.log(mongoAdminSetupInstructions(config));
2203
+ });
2204
+ target.command("list").description("list MongoDB targets").option("--org <slug>").action(async (options) => {
2205
+ const ctx = buildContext();
2206
+ const org = await resolveOrg(ctx, options.org);
2207
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
2208
+ for (const t of targets) {
2209
+ const cfg = t.config;
2210
+ if (cfg.provider !== "mongodb") continue;
2211
+ const host = `${cfg.connection.host}:${cfg.connection.port}`;
2212
+ console.log(`${t.id}\t${t.name}\t${host}\t${cfg.connection.database}\t${cfg.accessLevel ?? "readonly"}`);
2213
+ }
2214
+ });
2215
+ target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>").action(async (targetId, options) => {
2216
+ const ctx = buildContext();
2217
+ const org = await resolveOrg(ctx, options.org);
2218
+ await ctx.client.deleteLeaseTarget(org.id, targetId);
2219
+ console.error(`deleted ${targetId}`);
2220
+ });
2221
+ mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
2222
+ const ctx = buildContext();
2223
+ const org = await resolveOrg(ctx, options.org);
2224
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
2225
+ const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
2226
+ if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
2227
+ if (t.config.provider !== "mongodb") fail(`"${t?.name}" is not a mongodb target (see \`seekrit aws\`)`);
2228
+ const recipient = await generateMongoRecipientKeyPair();
2229
+ const { mongodb: leased } = await ctx.client.mintLease(org.id, {
2230
+ provider: "mongodb",
2231
+ targetId: t.id,
2232
+ recipientPublicKey: recipient.publicKeyJwk,
2233
+ ttlSeconds: parseTtlSeconds$4(options.ttl)
2234
+ });
2235
+ const cred = await unwrapMongoCredential(leased.wrappedCredential, recipient.privateKeyJwk);
2236
+ console.error(`leased ${cred.username} on ${cred.host}:${cred.port}/${cred.database} — expires ${cred.expiration}`);
2237
+ if (options.json) console.log(JSON.stringify(cred, null, 2));
2238
+ else console.log(`export MONGODB_URI='${cred.uri}'`);
2239
+ });
2240
+ mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
2241
+ const ctx = buildContext();
2242
+ const org = await resolveOrg(ctx, options.org);
2243
+ const { leases } = await ctx.client.listLeases(org.id);
2244
+ for (const l of leases) {
2245
+ if (l.provider !== "mongodb") continue;
2246
+ console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
2247
+ }
2248
+ });
2249
+ mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>").action(async (leaseId, options) => {
2250
+ const ctx = buildContext();
2251
+ const org = await resolveOrg(ctx, options.org);
2252
+ await ctx.client.revokeLease(org.id, leaseId);
2253
+ console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
2254
+ });
2255
+ }
2256
+ //#endregion
1764
2257
  //#region src/provisioner.ts
1765
2258
  /**
1766
2259
  * `seekrit provisioner` — helpers for the self-hosted **remote executor**
@@ -2851,6 +3344,8 @@ registerRedisCommands(program);
2851
3344
  registerProvisionerCommands(program);
2852
3345
  registerSshCommands(program);
2853
3346
  registerAwsCommands(program);
3347
+ registerGcpCommands(program);
3348
+ registerMongoCommands(program);
2854
3349
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
2855
3350
  const { runMcpServer } = await import("./mcp-ARBuneH3.js");
2856
3351
  await runMcpServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,9 +23,9 @@
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.1.0",
25
25
  "tsdown": "^0.22.3",
26
- "@seekrit/api-client": "0.0.1",
27
26
  "@seekrit/core": "0.0.1",
28
- "@seekrit/crypto": "0.0.1"
27
+ "@seekrit/crypto": "0.0.1",
28
+ "@seekrit/api-client": "0.0.1"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsdown",