@seekrit/cli 0.8.0 → 0.9.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.
package/dist/index.js CHANGED
@@ -140,6 +140,75 @@ async function importPrivateKeyPkcs8(pkcs8) {
140
140
  }, true, ["deriveBits"]);
141
141
  }
142
142
  //#endregion
143
+ //#region ../../packages/crypto/src/mysql.ts
144
+ /**
145
+ * Client-side construction of a MySQL/MariaDB `mysql_native_password`
146
+ * authentication string, for minting *temporary MySQL login credentials*
147
+ * without the password plaintext ever reaching seekrit's control plane OR
148
+ * MySQL itself.
149
+ *
150
+ * The trick mirrors the Postgres SCRAM one (scram.ts): the stored auth string
151
+ * is `*<UPPER(HEX(SHA1(SHA1(password))))>`, and
152
+ * `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'` stores that
153
+ * string verbatim — MySQL does NOT re-hash it. So the flow is:
154
+ *
155
+ * 1. the machine that will connect generates a random password locally,
156
+ * 2. computes this hash locally,
157
+ * 3. sends only the hash to the broker → `CREATE USER … AS '<hash>'`,
158
+ * 4. connects directly to MySQL with the plaintext it never shared.
159
+ *
160
+ * Zero-knowledge at both layers: the control plane relays only the hash, and
161
+ * the hash is NOT sufficient to authenticate. `mysql_native_password` login is
162
+ * a challenge-response — the server proves knowledge of `SHA1(SHA1(password))`
163
+ * against a fresh scramble, and verifying a client requires `SHA1(password)`
164
+ * (the preimage of the first inner hash), which the stored double-SHA1 does not
165
+ * reveal. A dump of `mysql.user` / the query log therefore cannot log in.
166
+ *
167
+ * SHA-1 is available via WebCrypto (`crypto.subtle.digest("SHA-1", …)`) in the
168
+ * browser, the CLI, the MCP server, and Workers — so this runs everywhere the
169
+ * SCRAM helper does, with no hand-rolled hash primitive.
170
+ */
171
+ const DEFAULT_PASSWORD_LENGTH$1 = 32;
172
+ const PASSWORD_ALPHABET$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
173
+ async function sha1(data) {
174
+ return new Uint8Array(await crypto.subtle.digest("SHA-1", data));
175
+ }
176
+ function toUpperHex(bytes) {
177
+ let hex = "";
178
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
179
+ return hex.toUpperCase();
180
+ }
181
+ function randomPassword$1(length) {
182
+ let out = "";
183
+ while (out.length < length) {
184
+ const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
185
+ for (const byte of bytes) {
186
+ if (byte < 248) out += PASSWORD_ALPHABET$1[byte % 62];
187
+ if (out.length === length) break;
188
+ }
189
+ }
190
+ return out;
191
+ }
192
+ /**
193
+ * Compute the `mysql_native_password` auth string `*<UPPER(HEX(SHA1(SHA1(pw))))>`
194
+ * for a known password. Pass the result straight to
195
+ * `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'`.
196
+ */
197
+ async function mysqlNativePasswordVerifier(password) {
198
+ return `*${toUpperHex(await sha1(await sha1(utf8Encode(password))))}`;
199
+ }
200
+ /**
201
+ * Mint a fresh random password and its `mysql_native_password` hash in one step
202
+ * — the client-side half of a Vault-style dynamic MySQL credential.
203
+ */
204
+ async function generateMysqlCredential(options = {}) {
205
+ const password = randomPassword$1(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
206
+ return {
207
+ password,
208
+ verifier: await mysqlNativePasswordVerifier(password)
209
+ };
210
+ }
211
+ //#endregion
143
212
  //#region ../../packages/crypto/src/passphrase.ts
144
213
  /**
145
214
  * User private keys are stored server-side encrypted under a key derived from
@@ -517,7 +586,7 @@ async function unwrapDek(wrapped, privateKey) {
517
586
  }
518
587
  //#endregion
519
588
  //#region package.json
520
- var version = "0.8.0";
589
+ var version = "0.9.0";
521
590
  const PROJECT_FILE = "seekrit.json";
522
591
  function globalConfigPath() {
523
592
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -606,6 +675,12 @@ var SeekritClient = class {
606
675
  setMyKeys(input) {
607
676
  return this.request("PUT", "/v1/me/keys", input);
608
677
  }
678
+ getMyNotificationPrefs() {
679
+ return this.request("GET", "/v1/me/notifications");
680
+ }
681
+ setMyNotificationPrefs(input) {
682
+ return this.request("PUT", "/v1/me/notifications", input);
683
+ }
609
684
  listOrgs() {
610
685
  return this.request("GET", "/v1/orgs");
611
686
  }
@@ -864,7 +939,234 @@ function formatSecrets(values, format) {
864
939
  case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
865
940
  }
866
941
  }
867
- z.enum(["postgres", "ssh"]);
942
+ //#endregion
943
+ //#region src/target.ts
944
+ /** Resolve the target org from a flag, the committed config, or a lone org. */
945
+ async function resolveOrg(ctx, orgSlug) {
946
+ const wanted = orgSlug ?? findProjectConfig()?.org;
947
+ const { orgs } = await ctx.client.listOrgs();
948
+ if (wanted) {
949
+ const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
950
+ if (!org) fail(`no accessible org "${wanted}"`);
951
+ return {
952
+ id: org.id,
953
+ slug: org.slug
954
+ };
955
+ }
956
+ const only = orgs[0];
957
+ if (orgs.length === 1 && only) return {
958
+ id: only.id,
959
+ slug: only.slug
960
+ };
961
+ fail("specify --org (or run `seekrit init`)");
962
+ }
963
+ /**
964
+ * Resolve an environment to operate on — an application env (`--app --env`,
965
+ * or the config's app + `--env`) or a group env (`--group --env`).
966
+ */
967
+ async function resolveEnvTarget(ctx, opts) {
968
+ const org = await resolveOrg(ctx, opts.org);
969
+ if (!opts.env) fail("specify --env");
970
+ if (opts.group) {
971
+ const { groups } = await ctx.client.listGroups(org.id);
972
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
973
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
974
+ const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
975
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
976
+ if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
977
+ return {
978
+ orgId: org.id,
979
+ envId: env.id,
980
+ label: `${group.slug}@${env.slug}`
981
+ };
982
+ }
983
+ const appSlug = opts.app ?? findProjectConfig()?.app;
984
+ if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
985
+ const { apps } = await ctx.client.listApps(org.id);
986
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
987
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
988
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
989
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
990
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
991
+ return {
992
+ orgId: org.id,
993
+ envId: env.id,
994
+ label: `${app.slug}/${env.slug}`
995
+ };
996
+ }
997
+ /** Resolve an application environment, keeping ids + slugs (for token binding). */
998
+ async function resolveAppEnv(ctx, opts) {
999
+ const org = await resolveOrg(ctx, opts.org);
1000
+ const appSlug = opts.app ?? findProjectConfig()?.app;
1001
+ if (!appSlug) fail("specify --app (or run `seekrit init`)");
1002
+ if (!opts.env) fail("specify --env");
1003
+ const { apps } = await ctx.client.listApps(org.id);
1004
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
1005
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
1006
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
1007
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1008
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
1009
+ return {
1010
+ orgId: org.id,
1011
+ appId: app.id,
1012
+ appSlug: app.slug,
1013
+ envId: env.id,
1014
+ envSlug: env.slug
1015
+ };
1016
+ }
1017
+ /** Resolve a group by slug within the target org. */
1018
+ async function resolveGroup(ctx, opts) {
1019
+ const org = await resolveOrg(ctx, opts.org);
1020
+ if (!opts.group) fail("specify --group");
1021
+ const { groups } = await ctx.client.listGroups(org.id);
1022
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
1023
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
1024
+ return {
1025
+ orgId: org.id,
1026
+ id: group.id,
1027
+ slug: group.slug
1028
+ };
1029
+ }
1030
+ //#endregion
1031
+ //#region src/mysql.ts
1032
+ /**
1033
+ * `seekrit mysql` — temporary MySQL/MariaDB credentials (Vault-style dynamic
1034
+ * secrets).
1035
+ *
1036
+ * Zero-knowledge: minting generates the password and its
1037
+ * `mysql_native_password` hash on THIS machine and sends only the hash; the
1038
+ * plaintext password never reaches the API or gets stored. Registering a target
1039
+ * wraps the admin connection string to the broker's public key locally, so the
1040
+ * control plane only ever stores ciphertext.
1041
+ */
1042
+ /** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
1043
+ function parseTtlSeconds$2(input) {
1044
+ const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
1045
+ if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
1046
+ return Number(m[1]) * ({
1047
+ s: 1,
1048
+ m: 60,
1049
+ h: 3600,
1050
+ d: 86400
1051
+ }[m[2] || "s"] ?? 1);
1052
+ }
1053
+ /** A fresh, valid MySQL user name: `tmp_` + lowercase alphanumerics. */
1054
+ function generateUserName(prefix = "tmp") {
1055
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
1056
+ let out = "";
1057
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
1058
+ for (const b of bytes) out += alphabet[b % 36];
1059
+ return `${prefix}_${out}`;
1060
+ }
1061
+ function registerMysqlCommands(program) {
1062
+ const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
1063
+ const target = mysql.command("target").description("manage provisioning targets");
1064
+ target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--admin-url <url>", "admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$2, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$2, []).action(async (options) => {
1065
+ const ctx = buildContext();
1066
+ const org = await resolveOrg(ctx, options.org);
1067
+ const executor = options.executor === "remote" ? "remote" : "in_do";
1068
+ if (executor === "remote" && !options.provisionerUrl) fail("--provisioner-url is required for the remote executor");
1069
+ if (![
1070
+ "readonly",
1071
+ "readwrite",
1072
+ "custom"
1073
+ ].includes(options.access)) fail("--access must be readonly, readwrite, or custom");
1074
+ const accessLevel = options.access;
1075
+ const adminSecret = options.adminUrl ?? process.env.SEEKRIT_MYSQL_ADMIN_URL;
1076
+ if (!adminSecret) fail(executor === "remote" ? "provide the shared HMAC key via --admin-url or SEEKRIT_MYSQL_ADMIN_URL" : "provide the admin connection string via --admin-url or SEEKRIT_MYSQL_ADMIN_URL");
1077
+ const config = {
1078
+ provider: "mysql",
1079
+ executor,
1080
+ accessLevel,
1081
+ connection: {
1082
+ host: options.host,
1083
+ port: Number.parseInt(options.port, 10),
1084
+ database: options.database
1085
+ },
1086
+ userHost: options.userHost,
1087
+ ...accessLevel === "custom" ? {
1088
+ ...options.createStatement.length ? { createStatements: options.createStatement } : {},
1089
+ ...options.revokeStatement.length ? { revokeStatements: options.revokeStatement } : {}
1090
+ } : {},
1091
+ ...options.provisionerUrl ? { provisionerUrl: options.provisionerUrl } : {}
1092
+ };
1093
+ const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
1094
+ const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminSecret), publicKeyJwk);
1095
+ const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
1096
+ name: options.name,
1097
+ config,
1098
+ wrappedAdminSecret
1099
+ });
1100
+ console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
1101
+ });
1102
+ target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
1103
+ const ctx = buildContext();
1104
+ const org = await resolveOrg(ctx, options.org);
1105
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
1106
+ for (const t of targets) {
1107
+ if (t.provider !== "mysql") continue;
1108
+ const cfg = t.config;
1109
+ console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
1110
+ }
1111
+ });
1112
+ target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
1113
+ const ctx = buildContext();
1114
+ const org = await resolveOrg(ctx, options.org);
1115
+ await ctx.client.deleteLeaseTarget(org.id, targetId);
1116
+ console.error(`removed ${targetId}`);
1117
+ });
1118
+ mysql.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--user <name>", "user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
1119
+ const ctx = buildContext();
1120
+ const org = await resolveOrg(ctx, options.org);
1121
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
1122
+ const target = targets.find((t) => t.id === targetRef || t.name === targetRef);
1123
+ if (!target) fail(`no target "${targetRef}" in ${org.slug}`);
1124
+ if (target.provider !== "mysql") fail(`target "${targetRef}" is not a MySQL target`);
1125
+ const userName = options.user ?? generateUserName();
1126
+ const ttlSeconds = parseTtlSeconds$2(options.ttl);
1127
+ const { password, verifier } = await generateMysqlCredential();
1128
+ const { connection } = await ctx.client.mintLease(org.id, {
1129
+ provider: "mysql",
1130
+ targetId: target.id,
1131
+ roleName: userName,
1132
+ verifier,
1133
+ ttlSeconds
1134
+ });
1135
+ const url = `mysql://${userName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
1136
+ console.error(`leased ${userName} on ${connection.host}/${connection.database} — expires ${connection.expiresAt}`);
1137
+ if (options.json) console.log(JSON.stringify({
1138
+ ...connection,
1139
+ password,
1140
+ url
1141
+ }, null, 2));
1142
+ else console.log(url);
1143
+ });
1144
+ mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
1145
+ const ctx = buildContext();
1146
+ const org = await resolveOrg(ctx, options.org);
1147
+ const { leases } = await ctx.client.listLeases(org.id);
1148
+ for (const l of leases) {
1149
+ if (l.provider !== "mysql") continue;
1150
+ console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
1151
+ }
1152
+ });
1153
+ mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>").action(async (leaseId, options) => {
1154
+ const ctx = buildContext();
1155
+ const org = await resolveOrg(ctx, options.org);
1156
+ await ctx.client.revokeLease(org.id, leaseId);
1157
+ console.error(`revoked ${leaseId}`);
1158
+ });
1159
+ }
1160
+ /** Collect a repeatable option into an array. */
1161
+ function collect$2(value, acc) {
1162
+ acc.push(value);
1163
+ return acc;
1164
+ }
1165
+ z.enum([
1166
+ "postgres",
1167
+ "mysql",
1168
+ "ssh"
1169
+ ]);
868
1170
  const executorModeSchema = z.enum(["in_do", "remote"]);
869
1171
  /**
870
1172
  * A Postgres role name we are willing to create. Deliberately strict — this
@@ -888,6 +1190,20 @@ const sshPrincipalSchema = z.string().regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1
888
1190
  const sshPublicKeySchema = z.string().max(2048).regex(/^ssh-ed25519 [A-Za-z0-9+/=]+( .*)?$/, "must be an ssh-ed25519 public key");
889
1191
  /** An SSH certificate extension name, e.g. `permit-pty`. */
890
1192
  const sshExtensionSchema = z.string().regex(/^[a-z0-9-]{1,64}$/);
1193
+ /**
1194
+ * A MySQL/MariaDB user name we are willing to create. Interpolated into a
1195
+ * quoted SQL literal (`'{{name}}'@'%'`), so it is kept strict — plain
1196
+ * alphanumerics/underscore, no quotes/whitespace/semicolons to break out.
1197
+ */
1198
+ const mysqlUserNameSchema = z.string().regex(/^[A-Za-z0-9_]{3,32}$/, "must be 3–32 chars, letters/digits/underscore");
1199
+ /**
1200
+ * A `mysql_native_password` authentication string — `*` followed by 40 upper
1201
+ * hex chars (`UPPER(HEX(SHA1(SHA1(password))))`), as produced by
1202
+ * @seekrit/crypto `mysqlNativePasswordVerifier`. Stored verbatim by
1203
+ * `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'`, and its
1204
+ * alphabet contains no single quote, so it is safe in a quoted SQL literal.
1205
+ */
1206
+ const mysqlNativeVerifierSchema = z.string().regex(/^\*[0-9A-F]{40}$/, "must be a mysql_native_password hash (*<40 hex>)");
891
1207
  const postgresAccessLevelSchema = z.enum([
892
1208
  "readonly",
893
1209
  "readwrite",
@@ -898,6 +1214,11 @@ const POSTGRES_GROUP_ROLES = {
898
1214
  readonly: "seekrit_readonly",
899
1215
  readwrite: "seekrit_readwrite"
900
1216
  };
1217
+ const mysqlAccessLevelSchema = z.enum([
1218
+ "readonly",
1219
+ "readwrite",
1220
+ "custom"
1221
+ ]);
901
1222
  const connectionSchema = z.object({
902
1223
  host: z.string().min(1),
903
1224
  port: z.number().int().min(1).max(65535),
@@ -917,6 +1238,18 @@ const postgresTargetConfigSchema = z.object({
917
1238
  createStatements: z.array(statementSchema).max(16).optional(),
918
1239
  revokeStatements: z.array(statementSchema).max(16).optional()
919
1240
  });
1241
+ /** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
1242
+ const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
1243
+ const mysqlTargetConfigSchema = z.object({
1244
+ provider: z.literal("mysql"),
1245
+ executor: executorModeSchema,
1246
+ accessLevel: mysqlAccessLevelSchema.optional(),
1247
+ connection: connectionSchema,
1248
+ userHost: mysqlHostSchema.optional(),
1249
+ provisionerUrl: z.url().optional(),
1250
+ createStatements: z.array(statementSchema).max(16).optional(),
1251
+ revokeStatements: z.array(statementSchema).max(16).optional()
1252
+ });
920
1253
  const sshTargetConfigSchema = z.object({
921
1254
  provider: z.literal("ssh"),
922
1255
  executor: z.literal("in_do"),
@@ -929,7 +1262,11 @@ const sshTargetConfigSchema = z.object({
929
1262
  user: sshPrincipalSchema.optional()
930
1263
  }).optional()
931
1264
  });
932
- const leaseTargetConfigSchema = z.discriminatedUnion("provider", [postgresTargetConfigSchema, sshTargetConfigSchema]);
1265
+ const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
1266
+ postgresTargetConfigSchema,
1267
+ mysqlTargetConfigSchema,
1268
+ sshTargetConfigSchema
1269
+ ]);
933
1270
  z.object({
934
1271
  name: z.string().trim().min(1).max(128),
935
1272
  config: leaseTargetConfigSchema,
@@ -955,6 +1292,18 @@ const mintPostgresLeaseSchema = z.object({
955
1292
  ttlSeconds: ttlSecondsSchema
956
1293
  });
957
1294
  /**
1295
+ * Client → API: mint a MySQL/MariaDB lease. The client generates the password
1296
+ * and its `mysql_native_password` hash locally and sends only the hash — the
1297
+ * plaintext password never leaves the requesting machine.
1298
+ */
1299
+ const mintMysqlLeaseSchema = z.object({
1300
+ provider: z.literal("mysql"),
1301
+ targetId: z.string().min(1),
1302
+ roleName: mysqlUserNameSchema,
1303
+ verifier: mysqlNativeVerifierSchema,
1304
+ ttlSeconds: ttlSecondsSchema
1305
+ });
1306
+ /**
958
1307
  * Client → API: mint an SSH lease. The client generates an ephemeral keypair
959
1308
  * locally and sends only the public key; the signed certificate comes back in
960
1309
  * the response. The private key never leaves the requesting machine.
@@ -966,7 +1315,11 @@ const mintSshLeaseSchema = z.object({
966
1315
  principals: z.array(sshPrincipalSchema).min(1).max(32),
967
1316
  ttlSeconds: ttlSecondsSchema
968
1317
  });
969
- z.discriminatedUnion("provider", [mintPostgresLeaseSchema, mintSshLeaseSchema]);
1318
+ z.discriminatedUnion("provider", [
1319
+ mintPostgresLeaseSchema,
1320
+ mintMysqlLeaseSchema,
1321
+ mintSshLeaseSchema
1322
+ ]);
970
1323
  //#endregion
971
1324
  //#region ../../packages/core/src/providers/postgres.ts
972
1325
  /**
@@ -1021,6 +1374,25 @@ function sshHostSetupInstructions(config) {
1021
1374
  ].join("\n");
1022
1375
  }
1023
1376
  //#endregion
1377
+ //#region ../../packages/core/src/types.ts
1378
+ /**
1379
+ * Transactional notification emails seekrit can send. Each id is one
1380
+ * user-facing on/off toggle (see `NOTIFICATION_TYPE_META`). These carry only
1381
+ * audit-grade metadata — never secret material — and every one is opt-out
1382
+ * (defaults on). Kept as a const array so the API, api-client, and dashboard
1383
+ * share a single source of truth (mirrors `AUDIT_ACTIONS`).
1384
+ */
1385
+ const NOTIFICATION_TYPES = [
1386
+ "token_created",
1387
+ "token_revoked",
1388
+ "env_access_granted",
1389
+ "env_access_revoked",
1390
+ "resolve_denied",
1391
+ "org_welcome",
1392
+ "token_expiring",
1393
+ "lease_expired"
1394
+ ];
1395
+ //#endregion
1024
1396
  //#region ../../packages/core/src/schemas.ts
1025
1397
  /** URL-safe identifier segment: `my-app`, `production`, … */
1026
1398
  const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
@@ -1093,6 +1465,7 @@ z.object({
1093
1465
  environmentId: z.string().min(1).nullish(),
1094
1466
  expiresAt: z.iso.datetime().nullish()
1095
1467
  });
1468
+ z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
1096
1469
  z.object({
1097
1470
  cursor: z.string().optional(),
1098
1471
  limit: z.coerce.number().int().min(1).max(200).default(50),
@@ -1100,94 +1473,6 @@ z.object({
1100
1473
  resourceType: z.string().optional()
1101
1474
  });
1102
1475
  //#endregion
1103
- //#region src/target.ts
1104
- /** Resolve the target org from a flag, the committed config, or a lone org. */
1105
- async function resolveOrg(ctx, orgSlug) {
1106
- const wanted = orgSlug ?? findProjectConfig()?.org;
1107
- const { orgs } = await ctx.client.listOrgs();
1108
- if (wanted) {
1109
- const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
1110
- if (!org) fail(`no accessible org "${wanted}"`);
1111
- return {
1112
- id: org.id,
1113
- slug: org.slug
1114
- };
1115
- }
1116
- const only = orgs[0];
1117
- if (orgs.length === 1 && only) return {
1118
- id: only.id,
1119
- slug: only.slug
1120
- };
1121
- fail("specify --org (or run `seekrit init`)");
1122
- }
1123
- /**
1124
- * Resolve an environment to operate on — an application env (`--app --env`,
1125
- * or the config's app + `--env`) or a group env (`--group --env`).
1126
- */
1127
- async function resolveEnvTarget(ctx, opts) {
1128
- const org = await resolveOrg(ctx, opts.org);
1129
- if (!opts.env) fail("specify --env");
1130
- if (opts.group) {
1131
- const { groups } = await ctx.client.listGroups(org.id);
1132
- const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
1133
- if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
1134
- const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
1135
- const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1136
- if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
1137
- return {
1138
- orgId: org.id,
1139
- envId: env.id,
1140
- label: `${group.slug}@${env.slug}`
1141
- };
1142
- }
1143
- const appSlug = opts.app ?? findProjectConfig()?.app;
1144
- if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
1145
- const { apps } = await ctx.client.listApps(org.id);
1146
- const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
1147
- if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
1148
- const { environments } = await ctx.client.listEnvs(org.id, app.id);
1149
- const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1150
- if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
1151
- return {
1152
- orgId: org.id,
1153
- envId: env.id,
1154
- label: `${app.slug}/${env.slug}`
1155
- };
1156
- }
1157
- /** Resolve an application environment, keeping ids + slugs (for token binding). */
1158
- async function resolveAppEnv(ctx, opts) {
1159
- const org = await resolveOrg(ctx, opts.org);
1160
- const appSlug = opts.app ?? findProjectConfig()?.app;
1161
- if (!appSlug) fail("specify --app (or run `seekrit init`)");
1162
- if (!opts.env) fail("specify --env");
1163
- const { apps } = await ctx.client.listApps(org.id);
1164
- const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
1165
- if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
1166
- const { environments } = await ctx.client.listEnvs(org.id, app.id);
1167
- const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1168
- if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
1169
- return {
1170
- orgId: org.id,
1171
- appId: app.id,
1172
- appSlug: app.slug,
1173
- envId: env.id,
1174
- envSlug: env.slug
1175
- };
1176
- }
1177
- /** Resolve a group by slug within the target org. */
1178
- async function resolveGroup(ctx, opts) {
1179
- const org = await resolveOrg(ctx, opts.org);
1180
- if (!opts.group) fail("specify --group");
1181
- const { groups } = await ctx.client.listGroups(org.id);
1182
- const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
1183
- if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
1184
- return {
1185
- orgId: org.id,
1186
- id: group.id,
1187
- slug: group.slug
1188
- };
1189
- }
1190
- //#endregion
1191
1476
  //#region src/pg.ts
1192
1477
  /**
1193
1478
  * `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
@@ -1933,9 +2218,10 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
1933
2218
  console.error(`${tokenId} revoked`);
1934
2219
  });
1935
2220
  registerPgCommands(program);
2221
+ registerMysqlCommands(program);
1936
2222
  registerSshCommands(program);
1937
2223
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
1938
- const { runMcpServer } = await import("./mcp-DNUBSbcd.js");
2224
+ const { runMcpServer } = await import("./mcp-BEcV_KpM.js");
1939
2225
  await runMcpServer();
1940
2226
  });
1941
2227
  program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
@@ -1949,4 +2235,4 @@ program.parseAsync(argv).catch((err) => {
1949
2235
  fail(err instanceof Error ? err.message : String(err));
1950
2236
  });
1951
2237
  //#endregion
1952
- export { parseServiceToken as _, resolveEnvTarget as a, getDek as c, setFailThrows as d, writeProjectConfig as f, isServiceToken as g, createServiceToken as h, resolveAppEnv as i, isTokenAuth as l, wrapDek as m, fetchDecryptedSecrets as n, resolveGroup as o, version as p, materializeEnv as r, resolveOrg as s, encryptAndSetSecret as t, tryBuildContext as u, generatePostgresCredential as v, generateDek as y };
2238
+ export { parseServiceToken as _, resolveEnvTarget as a, generateDek as b, getDek as c, setFailThrows as d, writeProjectConfig as f, isServiceToken as g, createServiceToken as h, resolveAppEnv as i, isTokenAuth as l, wrapDek as m, fetchDecryptedSecrets as n, resolveGroup as o, version as p, materializeEnv as r, resolveOrg as s, encryptAndSetSecret as t, tryBuildContext as u, generatePostgresCredential as v, generateMysqlCredential as y };
@@ -1,4 +1,4 @@
1
- import { _ as parseServiceToken, a as resolveEnvTarget, c as getDek, d as setFailThrows, f as writeProjectConfig, g as isServiceToken, h as createServiceToken, i as resolveAppEnv, l as isTokenAuth, m as wrapDek, n as fetchDecryptedSecrets, o as resolveGroup, p as version, r as materializeEnv, s as resolveOrg, t as encryptAndSetSecret, u as tryBuildContext, v as generatePostgresCredential, y as generateDek } from "./index.js";
1
+ import { _ as parseServiceToken, a as resolveEnvTarget, b as generateDek, c as getDek, d as setFailThrows, f as writeProjectConfig, g as isServiceToken, h as createServiceToken, i as resolveAppEnv, l as isTokenAuth, m as wrapDek, n as fetchDecryptedSecrets, o as resolveGroup, p as version, r as materializeEnv, s as resolveOrg, t as encryptAndSetSecret, u as tryBuildContext, v as generatePostgresCredential, y as generateMysqlCredential } from "./index.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -592,6 +592,58 @@ async function runMcpServer() {
592
592
  leaseId
593
593
  };
594
594
  });
595
+ tool("list_mysql_targets", "List registered MySQL/MariaDB provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
596
+ const ctx = getCtx();
597
+ const orgRef = await resolveOrg(ctx, org);
598
+ return (await ctx.client.listLeaseTargets(orgRef.id)).targets.filter((t) => t.provider === "mysql");
599
+ });
600
+ tool("create_mysql_lease", "Mint a short-lived MySQL/MariaDB credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its mysql_native_password hash is sent to the API — the plaintext never reaches seekrit or MySQL at rest. The user auto-expires; revoke early with revoke_mysql_lease.", {
601
+ org: z.string().optional(),
602
+ target: z.string().describe("target id or name"),
603
+ user: z.string().optional().describe("user name to create (default: random tmp_ name)"),
604
+ ttlSeconds: z.number().int().min(60).max(604800).optional().describe("lifetime (default 3600)")
605
+ }, async ({ org, target, user, ttlSeconds }) => {
606
+ const ctx = getCtx();
607
+ const orgRef = await resolveOrg(ctx, org);
608
+ const { targets } = await ctx.client.listLeaseTargets(orgRef.id);
609
+ const t = targets.find((x) => x.id === target || x.name === target);
610
+ if (!t) throw new Error(`no target "${target}" in ${orgRef.slug}`);
611
+ if (t.provider !== "mysql") throw new Error(`target "${target}" is not a MySQL target`);
612
+ const userName = user ?? `tmp_${randomLower(12)}`;
613
+ const { password, verifier } = await generateMysqlCredential();
614
+ const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
615
+ provider: "mysql",
616
+ targetId: t.id,
617
+ roleName: userName,
618
+ verifier,
619
+ ttlSeconds: ttlSeconds ?? 3600
620
+ });
621
+ const url = `mysql://${userName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
622
+ return {
623
+ leaseId: lease.id,
624
+ url,
625
+ username: userName,
626
+ expiresAt: connection.expiresAt,
627
+ note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
628
+ };
629
+ });
630
+ tool("list_mysql_leases", "List MySQL/MariaDB leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
631
+ const ctx = getCtx();
632
+ const orgRef = await resolveOrg(ctx, org);
633
+ return (await ctx.client.listLeases(orgRef.id)).leases.filter((l) => l.provider === "mysql");
634
+ });
635
+ tool("revoke_mysql_lease", "Revoke a MySQL/MariaDB lease now (drops the user immediately).", {
636
+ org: z.string().optional(),
637
+ leaseId: z.string()
638
+ }, async ({ org, leaseId }) => {
639
+ const ctx = getCtx();
640
+ const orgRef = await resolveOrg(ctx, org);
641
+ await ctx.client.revokeLease(orgRef.id, leaseId);
642
+ return {
643
+ ok: true,
644
+ leaseId
645
+ };
646
+ });
595
647
  tool("configure_project", "Link a directory to an org/app by writing seekrit.json (like `seekrit init`). The environment is chosen by the service token at runtime.", {
596
648
  org: z.string(),
597
649
  app: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {