@seekrit/cli 0.8.0 → 0.10.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 +453 -97
- package/dist/{mcp-DNUBSbcd.js → mcp-BEcV_KpM.js} +53 -1
- package/package.json +1 -1
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.
|
|
589
|
+
var version = "0.10.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
|
}
|
|
@@ -739,6 +814,19 @@ var SeekritClient = class {
|
|
|
739
814
|
const qs = params.size > 0 ? `?${params}` : "";
|
|
740
815
|
return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
|
|
741
816
|
}
|
|
817
|
+
getLogSink(orgId) {
|
|
818
|
+
return this.request("GET", `/v1/orgs/${orgId}/log-sink`);
|
|
819
|
+
}
|
|
820
|
+
setLogSink(orgId, input) {
|
|
821
|
+
return this.request("PUT", `/v1/orgs/${orgId}/log-sink`, input);
|
|
822
|
+
}
|
|
823
|
+
deleteLogSink(orgId) {
|
|
824
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/log-sink`);
|
|
825
|
+
}
|
|
826
|
+
/** Send a synthetic record to the configured endpoint to verify connectivity. */
|
|
827
|
+
testLogSink(orgId) {
|
|
828
|
+
return this.request("POST", `/v1/orgs/${orgId}/log-sink/test`);
|
|
829
|
+
}
|
|
742
830
|
};
|
|
743
831
|
//#endregion
|
|
744
832
|
//#region src/io.ts
|
|
@@ -864,7 +952,281 @@ function formatSecrets(values, format) {
|
|
|
864
952
|
case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
|
|
865
953
|
}
|
|
866
954
|
}
|
|
867
|
-
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/provisioner.ts
|
|
957
|
+
/**
|
|
958
|
+
* `seekrit provisioner` — helpers for the self-hosted **remote executor**
|
|
959
|
+
* (`seekrit-provisioner`), the daemon that runs a target's provisioning SQL
|
|
960
|
+
* inside the customer's own network so the control plane never sees the database
|
|
961
|
+
* admin credential.
|
|
962
|
+
*
|
|
963
|
+
* The only stateful command is `keygen`: the shared HMAC key authenticates the
|
|
964
|
+
* signed commands the broker sends the daemon. The same base64 value is given to
|
|
965
|
+
* BOTH `--hmac-key` when registering a remote target AND the daemon's
|
|
966
|
+
* `SEEKRIT_PROVISIONER_HMAC_KEY`.
|
|
967
|
+
*/
|
|
968
|
+
function registerProvisionerCommands(program) {
|
|
969
|
+
program.command("provisioner").description("self-hosted remote executor (seekrit-provisioner) helpers").command("keygen").description("generate a shared HMAC key for a remote provisioning target").action(() => {
|
|
970
|
+
const key = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
971
|
+
console.log(toBase64(key));
|
|
972
|
+
console.error("Give this key to BOTH `--hmac-key` when registering a remote target AND the\ndaemon's SEEKRIT_PROVISIONER_HMAC_KEY. Keep it secret; it authenticates the\nbroker's commands to your provisioner.");
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Resolve the "admin secret" a lease target registration wraps to the broker.
|
|
977
|
+
* Its meaning depends on the executor:
|
|
978
|
+
*
|
|
979
|
+
* - **remote** — the shared HMAC key (base64). The real database admin
|
|
980
|
+
* credential is NOT sent to seekrit; it is configured on the daemon instead.
|
|
981
|
+
* Preferred source is `--hmac-key` / `SEEKRIT_PROVISIONER_HMAC_KEY`, with the
|
|
982
|
+
* older `--admin-url` / provider admin-url env kept as a fallback.
|
|
983
|
+
* - **in_do** — the database admin connection string, which the broker decrypts
|
|
984
|
+
* transiently to run the SQL itself.
|
|
985
|
+
*
|
|
986
|
+
* `fail()` never returns, so the result is always a non-empty string.
|
|
987
|
+
*/
|
|
988
|
+
function resolveLeaseAdminSecret(opts) {
|
|
989
|
+
if (opts.executor === "remote") {
|
|
990
|
+
const key = opts.hmacKey ?? process.env.SEEKRIT_PROVISIONER_HMAC_KEY ?? opts.adminUrl ?? process.env[opts.adminUrlEnv];
|
|
991
|
+
if (!key) fail("the remote executor needs the shared HMAC key — pass --hmac-key or set SEEKRIT_PROVISIONER_HMAC_KEY (mint one with `seekrit provisioner keygen`)");
|
|
992
|
+
return key.trim();
|
|
993
|
+
}
|
|
994
|
+
const adminUrl = opts.adminUrl ?? process.env[opts.adminUrlEnv];
|
|
995
|
+
if (!adminUrl) fail(`provide the admin connection string via --admin-url or ${opts.adminUrlEnv}`);
|
|
996
|
+
return adminUrl;
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/target.ts
|
|
1000
|
+
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
1001
|
+
async function resolveOrg(ctx, orgSlug) {
|
|
1002
|
+
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
1003
|
+
const { orgs } = await ctx.client.listOrgs();
|
|
1004
|
+
if (wanted) {
|
|
1005
|
+
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
1006
|
+
if (!org) fail(`no accessible org "${wanted}"`);
|
|
1007
|
+
return {
|
|
1008
|
+
id: org.id,
|
|
1009
|
+
slug: org.slug
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const only = orgs[0];
|
|
1013
|
+
if (orgs.length === 1 && only) return {
|
|
1014
|
+
id: only.id,
|
|
1015
|
+
slug: only.slug
|
|
1016
|
+
};
|
|
1017
|
+
fail("specify --org (or run `seekrit init`)");
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
1021
|
+
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
1022
|
+
*/
|
|
1023
|
+
async function resolveEnvTarget(ctx, opts) {
|
|
1024
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1025
|
+
if (!opts.env) fail("specify --env");
|
|
1026
|
+
if (opts.group) {
|
|
1027
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
1028
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1029
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1030
|
+
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
1031
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1032
|
+
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
1033
|
+
return {
|
|
1034
|
+
orgId: org.id,
|
|
1035
|
+
envId: env.id,
|
|
1036
|
+
label: `${group.slug}@${env.slug}`
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1040
|
+
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
1041
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
1042
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1043
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1044
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1045
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1046
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1047
|
+
return {
|
|
1048
|
+
orgId: org.id,
|
|
1049
|
+
envId: env.id,
|
|
1050
|
+
label: `${app.slug}/${env.slug}`
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
1054
|
+
async function resolveAppEnv(ctx, opts) {
|
|
1055
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1056
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1057
|
+
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
1058
|
+
if (!opts.env) fail("specify --env");
|
|
1059
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
1060
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1061
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1062
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1063
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1064
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1065
|
+
return {
|
|
1066
|
+
orgId: org.id,
|
|
1067
|
+
appId: app.id,
|
|
1068
|
+
appSlug: app.slug,
|
|
1069
|
+
envId: env.id,
|
|
1070
|
+
envSlug: env.slug
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/** Resolve a group by slug within the target org. */
|
|
1074
|
+
async function resolveGroup(ctx, opts) {
|
|
1075
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1076
|
+
if (!opts.group) fail("specify --group");
|
|
1077
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
1078
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1079
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1080
|
+
return {
|
|
1081
|
+
orgId: org.id,
|
|
1082
|
+
id: group.id,
|
|
1083
|
+
slug: group.slug
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/mysql.ts
|
|
1088
|
+
/**
|
|
1089
|
+
* `seekrit mysql` — temporary MySQL/MariaDB credentials (Vault-style dynamic
|
|
1090
|
+
* secrets).
|
|
1091
|
+
*
|
|
1092
|
+
* Zero-knowledge: minting generates the password and its
|
|
1093
|
+
* `mysql_native_password` hash on THIS machine and sends only the hash; the
|
|
1094
|
+
* plaintext password never reaches the API or gets stored. Registering a target
|
|
1095
|
+
* wraps the admin connection string to the broker's public key locally, so the
|
|
1096
|
+
* control plane only ever stores ciphertext.
|
|
1097
|
+
*/
|
|
1098
|
+
/** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
|
|
1099
|
+
function parseTtlSeconds$2(input) {
|
|
1100
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1101
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
|
|
1102
|
+
return Number(m[1]) * ({
|
|
1103
|
+
s: 1,
|
|
1104
|
+
m: 60,
|
|
1105
|
+
h: 3600,
|
|
1106
|
+
d: 86400
|
|
1107
|
+
}[m[2] || "s"] ?? 1);
|
|
1108
|
+
}
|
|
1109
|
+
/** A fresh, valid MySQL user name: `tmp_` + lowercase alphanumerics. */
|
|
1110
|
+
function generateUserName(prefix = "tmp") {
|
|
1111
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1112
|
+
let out = "";
|
|
1113
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
|
|
1114
|
+
for (const b of bytes) out += alphabet[b % 36];
|
|
1115
|
+
return `${prefix}_${out}`;
|
|
1116
|
+
}
|
|
1117
|
+
function registerMysqlCommands(program) {
|
|
1118
|
+
const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
|
|
1119
|
+
const target = mysql.command("target").description("manage provisioning targets");
|
|
1120
|
+
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("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: 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) => {
|
|
1121
|
+
const ctx = buildContext();
|
|
1122
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1123
|
+
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
1124
|
+
if (executor === "remote" && !options.provisionerUrl) fail("--provisioner-url is required for the remote executor");
|
|
1125
|
+
if (![
|
|
1126
|
+
"readonly",
|
|
1127
|
+
"readwrite",
|
|
1128
|
+
"custom"
|
|
1129
|
+
].includes(options.access)) fail("--access must be readonly, readwrite, or custom");
|
|
1130
|
+
const accessLevel = options.access;
|
|
1131
|
+
const adminSecret = resolveLeaseAdminSecret({
|
|
1132
|
+
executor,
|
|
1133
|
+
hmacKey: options.hmacKey,
|
|
1134
|
+
adminUrl: options.adminUrl,
|
|
1135
|
+
adminUrlEnv: "SEEKRIT_MYSQL_ADMIN_URL"
|
|
1136
|
+
});
|
|
1137
|
+
const config = {
|
|
1138
|
+
provider: "mysql",
|
|
1139
|
+
executor,
|
|
1140
|
+
accessLevel,
|
|
1141
|
+
connection: {
|
|
1142
|
+
host: options.host,
|
|
1143
|
+
port: Number.parseInt(options.port, 10),
|
|
1144
|
+
database: options.database
|
|
1145
|
+
},
|
|
1146
|
+
userHost: options.userHost,
|
|
1147
|
+
...accessLevel === "custom" ? {
|
|
1148
|
+
...options.createStatement.length ? { createStatements: options.createStatement } : {},
|
|
1149
|
+
...options.revokeStatement.length ? { revokeStatements: options.revokeStatement } : {}
|
|
1150
|
+
} : {},
|
|
1151
|
+
...options.provisionerUrl ? { provisionerUrl: options.provisionerUrl } : {}
|
|
1152
|
+
};
|
|
1153
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
1154
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminSecret), publicKeyJwk);
|
|
1155
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
1156
|
+
name: options.name,
|
|
1157
|
+
config,
|
|
1158
|
+
wrappedAdminSecret
|
|
1159
|
+
});
|
|
1160
|
+
console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
|
|
1161
|
+
});
|
|
1162
|
+
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
1163
|
+
const ctx = buildContext();
|
|
1164
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1165
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1166
|
+
for (const t of targets) {
|
|
1167
|
+
if (t.provider !== "mysql") continue;
|
|
1168
|
+
const cfg = t.config;
|
|
1169
|
+
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
1173
|
+
const ctx = buildContext();
|
|
1174
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1175
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
1176
|
+
console.error(`removed ${targetId}`);
|
|
1177
|
+
});
|
|
1178
|
+
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) => {
|
|
1179
|
+
const ctx = buildContext();
|
|
1180
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1181
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1182
|
+
const target = targets.find((t) => t.id === targetRef || t.name === targetRef);
|
|
1183
|
+
if (!target) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1184
|
+
if (target.provider !== "mysql") fail(`target "${targetRef}" is not a MySQL target`);
|
|
1185
|
+
const userName = options.user ?? generateUserName();
|
|
1186
|
+
const ttlSeconds = parseTtlSeconds$2(options.ttl);
|
|
1187
|
+
const { password, verifier } = await generateMysqlCredential();
|
|
1188
|
+
const { connection } = await ctx.client.mintLease(org.id, {
|
|
1189
|
+
provider: "mysql",
|
|
1190
|
+
targetId: target.id,
|
|
1191
|
+
roleName: userName,
|
|
1192
|
+
verifier,
|
|
1193
|
+
ttlSeconds
|
|
1194
|
+
});
|
|
1195
|
+
const url = `mysql://${userName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
|
|
1196
|
+
console.error(`leased ${userName} on ${connection.host}/${connection.database} — expires ${connection.expiresAt}`);
|
|
1197
|
+
if (options.json) console.log(JSON.stringify({
|
|
1198
|
+
...connection,
|
|
1199
|
+
password,
|
|
1200
|
+
url
|
|
1201
|
+
}, null, 2));
|
|
1202
|
+
else console.log(url);
|
|
1203
|
+
});
|
|
1204
|
+
mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
1205
|
+
const ctx = buildContext();
|
|
1206
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1207
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
1208
|
+
for (const l of leases) {
|
|
1209
|
+
if (l.provider !== "mysql") continue;
|
|
1210
|
+
console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
1214
|
+
const ctx = buildContext();
|
|
1215
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1216
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
1217
|
+
console.error(`revoked ${leaseId}`);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
/** Collect a repeatable option into an array. */
|
|
1221
|
+
function collect$2(value, acc) {
|
|
1222
|
+
acc.push(value);
|
|
1223
|
+
return acc;
|
|
1224
|
+
}
|
|
1225
|
+
z.enum([
|
|
1226
|
+
"postgres",
|
|
1227
|
+
"mysql",
|
|
1228
|
+
"ssh"
|
|
1229
|
+
]);
|
|
868
1230
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
869
1231
|
/**
|
|
870
1232
|
* A Postgres role name we are willing to create. Deliberately strict — this
|
|
@@ -888,6 +1250,20 @@ const sshPrincipalSchema = z.string().regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1
|
|
|
888
1250
|
const sshPublicKeySchema = z.string().max(2048).regex(/^ssh-ed25519 [A-Za-z0-9+/=]+( .*)?$/, "must be an ssh-ed25519 public key");
|
|
889
1251
|
/** An SSH certificate extension name, e.g. `permit-pty`. */
|
|
890
1252
|
const sshExtensionSchema = z.string().regex(/^[a-z0-9-]{1,64}$/);
|
|
1253
|
+
/**
|
|
1254
|
+
* A MySQL/MariaDB user name we are willing to create. Interpolated into a
|
|
1255
|
+
* quoted SQL literal (`'{{name}}'@'%'`), so it is kept strict — plain
|
|
1256
|
+
* alphanumerics/underscore, no quotes/whitespace/semicolons to break out.
|
|
1257
|
+
*/
|
|
1258
|
+
const mysqlUserNameSchema = z.string().regex(/^[A-Za-z0-9_]{3,32}$/, "must be 3–32 chars, letters/digits/underscore");
|
|
1259
|
+
/**
|
|
1260
|
+
* A `mysql_native_password` authentication string — `*` followed by 40 upper
|
|
1261
|
+
* hex chars (`UPPER(HEX(SHA1(SHA1(password))))`), as produced by
|
|
1262
|
+
* @seekrit/crypto `mysqlNativePasswordVerifier`. Stored verbatim by
|
|
1263
|
+
* `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'`, and its
|
|
1264
|
+
* alphabet contains no single quote, so it is safe in a quoted SQL literal.
|
|
1265
|
+
*/
|
|
1266
|
+
const mysqlNativeVerifierSchema = z.string().regex(/^\*[0-9A-F]{40}$/, "must be a mysql_native_password hash (*<40 hex>)");
|
|
891
1267
|
const postgresAccessLevelSchema = z.enum([
|
|
892
1268
|
"readonly",
|
|
893
1269
|
"readwrite",
|
|
@@ -898,6 +1274,11 @@ const POSTGRES_GROUP_ROLES = {
|
|
|
898
1274
|
readonly: "seekrit_readonly",
|
|
899
1275
|
readwrite: "seekrit_readwrite"
|
|
900
1276
|
};
|
|
1277
|
+
const mysqlAccessLevelSchema = z.enum([
|
|
1278
|
+
"readonly",
|
|
1279
|
+
"readwrite",
|
|
1280
|
+
"custom"
|
|
1281
|
+
]);
|
|
901
1282
|
const connectionSchema = z.object({
|
|
902
1283
|
host: z.string().min(1),
|
|
903
1284
|
port: z.number().int().min(1).max(65535),
|
|
@@ -917,6 +1298,18 @@ const postgresTargetConfigSchema = z.object({
|
|
|
917
1298
|
createStatements: z.array(statementSchema).max(16).optional(),
|
|
918
1299
|
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
919
1300
|
});
|
|
1301
|
+
/** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
|
|
1302
|
+
const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
|
|
1303
|
+
const mysqlTargetConfigSchema = z.object({
|
|
1304
|
+
provider: z.literal("mysql"),
|
|
1305
|
+
executor: executorModeSchema,
|
|
1306
|
+
accessLevel: mysqlAccessLevelSchema.optional(),
|
|
1307
|
+
connection: connectionSchema,
|
|
1308
|
+
userHost: mysqlHostSchema.optional(),
|
|
1309
|
+
provisionerUrl: z.url().optional(),
|
|
1310
|
+
createStatements: z.array(statementSchema).max(16).optional(),
|
|
1311
|
+
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
1312
|
+
});
|
|
920
1313
|
const sshTargetConfigSchema = z.object({
|
|
921
1314
|
provider: z.literal("ssh"),
|
|
922
1315
|
executor: z.literal("in_do"),
|
|
@@ -929,7 +1322,11 @@ const sshTargetConfigSchema = z.object({
|
|
|
929
1322
|
user: sshPrincipalSchema.optional()
|
|
930
1323
|
}).optional()
|
|
931
1324
|
});
|
|
932
|
-
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
1325
|
+
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
1326
|
+
postgresTargetConfigSchema,
|
|
1327
|
+
mysqlTargetConfigSchema,
|
|
1328
|
+
sshTargetConfigSchema
|
|
1329
|
+
]);
|
|
933
1330
|
z.object({
|
|
934
1331
|
name: z.string().trim().min(1).max(128),
|
|
935
1332
|
config: leaseTargetConfigSchema,
|
|
@@ -955,6 +1352,18 @@ const mintPostgresLeaseSchema = z.object({
|
|
|
955
1352
|
ttlSeconds: ttlSecondsSchema
|
|
956
1353
|
});
|
|
957
1354
|
/**
|
|
1355
|
+
* Client → API: mint a MySQL/MariaDB lease. The client generates the password
|
|
1356
|
+
* and its `mysql_native_password` hash locally and sends only the hash — the
|
|
1357
|
+
* plaintext password never leaves the requesting machine.
|
|
1358
|
+
*/
|
|
1359
|
+
const mintMysqlLeaseSchema = z.object({
|
|
1360
|
+
provider: z.literal("mysql"),
|
|
1361
|
+
targetId: z.string().min(1),
|
|
1362
|
+
roleName: mysqlUserNameSchema,
|
|
1363
|
+
verifier: mysqlNativeVerifierSchema,
|
|
1364
|
+
ttlSeconds: ttlSecondsSchema
|
|
1365
|
+
});
|
|
1366
|
+
/**
|
|
958
1367
|
* Client → API: mint an SSH lease. The client generates an ephemeral keypair
|
|
959
1368
|
* locally and sends only the public key; the signed certificate comes back in
|
|
960
1369
|
* the response. The private key never leaves the requesting machine.
|
|
@@ -966,7 +1375,11 @@ const mintSshLeaseSchema = z.object({
|
|
|
966
1375
|
principals: z.array(sshPrincipalSchema).min(1).max(32),
|
|
967
1376
|
ttlSeconds: ttlSecondsSchema
|
|
968
1377
|
});
|
|
969
|
-
z.discriminatedUnion("provider", [
|
|
1378
|
+
z.discriminatedUnion("provider", [
|
|
1379
|
+
mintPostgresLeaseSchema,
|
|
1380
|
+
mintMysqlLeaseSchema,
|
|
1381
|
+
mintSshLeaseSchema
|
|
1382
|
+
]);
|
|
970
1383
|
//#endregion
|
|
971
1384
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
972
1385
|
/**
|
|
@@ -1021,6 +1434,25 @@ function sshHostSetupInstructions(config) {
|
|
|
1021
1434
|
].join("\n");
|
|
1022
1435
|
}
|
|
1023
1436
|
//#endregion
|
|
1437
|
+
//#region ../../packages/core/src/types.ts
|
|
1438
|
+
/**
|
|
1439
|
+
* Transactional notification emails seekrit can send. Each id is one
|
|
1440
|
+
* user-facing on/off toggle (see `NOTIFICATION_TYPE_META`). These carry only
|
|
1441
|
+
* audit-grade metadata — never secret material — and every one is opt-out
|
|
1442
|
+
* (defaults on). Kept as a const array so the API, api-client, and dashboard
|
|
1443
|
+
* share a single source of truth (mirrors `AUDIT_ACTIONS`).
|
|
1444
|
+
*/
|
|
1445
|
+
const NOTIFICATION_TYPES = [
|
|
1446
|
+
"token_created",
|
|
1447
|
+
"token_revoked",
|
|
1448
|
+
"env_access_granted",
|
|
1449
|
+
"env_access_revoked",
|
|
1450
|
+
"resolve_denied",
|
|
1451
|
+
"org_welcome",
|
|
1452
|
+
"token_expiring",
|
|
1453
|
+
"lease_expired"
|
|
1454
|
+
];
|
|
1455
|
+
//#endregion
|
|
1024
1456
|
//#region ../../packages/core/src/schemas.ts
|
|
1025
1457
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
1026
1458
|
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
@@ -1093,6 +1525,12 @@ z.object({
|
|
|
1093
1525
|
environmentId: z.string().min(1).nullish(),
|
|
1094
1526
|
expiresAt: z.iso.datetime().nullish()
|
|
1095
1527
|
});
|
|
1528
|
+
z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
|
|
1529
|
+
z.object({
|
|
1530
|
+
endpoint: z.url().max(2048),
|
|
1531
|
+
headers: z.record(z.string().min(1).max(256), z.string().max(4096)).optional(),
|
|
1532
|
+
enabled: z.boolean().default(true)
|
|
1533
|
+
});
|
|
1096
1534
|
z.object({
|
|
1097
1535
|
cursor: z.string().optional(),
|
|
1098
1536
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
@@ -1100,94 +1538,6 @@ z.object({
|
|
|
1100
1538
|
resourceType: z.string().optional()
|
|
1101
1539
|
});
|
|
1102
1540
|
//#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
1541
|
//#region src/pg.ts
|
|
1192
1542
|
/**
|
|
1193
1543
|
* `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
|
|
@@ -1220,7 +1570,7 @@ function generateRoleName(prefix = "tmp") {
|
|
|
1220
1570
|
function registerPgCommands(program) {
|
|
1221
1571
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
1222
1572
|
const target = pg.command("target").description("manage provisioning targets");
|
|
1223
|
-
target.command("add").description("register a Postgres cluster 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", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--admin-url <url>", "admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$1, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$1, []).action(async (options) => {
|
|
1573
|
+
target.command("add").description("register a Postgres cluster 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", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$1, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$1, []).action(async (options) => {
|
|
1224
1574
|
const ctx = buildContext();
|
|
1225
1575
|
const org = await resolveOrg(ctx, options.org);
|
|
1226
1576
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -1231,8 +1581,12 @@ function registerPgCommands(program) {
|
|
|
1231
1581
|
"custom"
|
|
1232
1582
|
].includes(options.access)) fail("--access must be readonly, readwrite, or custom");
|
|
1233
1583
|
const accessLevel = options.access;
|
|
1234
|
-
const adminSecret =
|
|
1235
|
-
|
|
1584
|
+
const adminSecret = resolveLeaseAdminSecret({
|
|
1585
|
+
executor,
|
|
1586
|
+
hmacKey: options.hmacKey,
|
|
1587
|
+
adminUrl: options.adminUrl,
|
|
1588
|
+
adminUrlEnv: "SEEKRIT_PG_ADMIN_URL"
|
|
1589
|
+
});
|
|
1236
1590
|
const config = {
|
|
1237
1591
|
provider: "postgres",
|
|
1238
1592
|
executor,
|
|
@@ -1933,9 +2287,11 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
|
|
|
1933
2287
|
console.error(`${tokenId} revoked`);
|
|
1934
2288
|
});
|
|
1935
2289
|
registerPgCommands(program);
|
|
2290
|
+
registerMysqlCommands(program);
|
|
2291
|
+
registerProvisionerCommands(program);
|
|
1936
2292
|
registerSshCommands(program);
|
|
1937
2293
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
1938
|
-
const { runMcpServer } = await import("./mcp-
|
|
2294
|
+
const { runMcpServer } = await import("./mcp-BEcV_KpM.js");
|
|
1939
2295
|
await runMcpServer();
|
|
1940
2296
|
});
|
|
1941
2297
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -1949,4 +2305,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
1949
2305
|
fail(err instanceof Error ? err.message : String(err));
|
|
1950
2306
|
});
|
|
1951
2307
|
//#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,
|
|
2308
|
+
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
|
|
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(),
|