@seekrit/cli 0.7.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 +732 -103
- package/dist/{mcp-Dw3hVhbC.js → mcp-BEcV_KpM.js} +54 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join, parse } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { Writable } from "node:stream";
|
|
@@ -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
|
|
@@ -259,6 +328,138 @@ async function generatePostgresCredential(options = {}) {
|
|
|
259
328
|
};
|
|
260
329
|
}
|
|
261
330
|
//#endregion
|
|
331
|
+
//#region ../../packages/crypto/src/ssh.ts
|
|
332
|
+
/**
|
|
333
|
+
* Client-side SSH certificate authority for minting *temporary SSH access*
|
|
334
|
+
* without any private key ever reaching seekrit's control plane.
|
|
335
|
+
*
|
|
336
|
+
* The model (Vault-style, tier-1 verifier injection — see the `ZkTier` doc in
|
|
337
|
+
* @seekrit/core leases.ts):
|
|
338
|
+
*
|
|
339
|
+
* 1. the machine that will connect generates an ephemeral Ed25519 keypair
|
|
340
|
+
* locally ({@link generateSshKeyPair}),
|
|
341
|
+
* 2. it sends only the *public* key to the broker,
|
|
342
|
+
* 3. the broker signs a short-lived OpenSSH user *certificate* over that
|
|
343
|
+
* public key ({@link signSshUserCertificate}) using the CA private key,
|
|
344
|
+
* 4. the consumer connects with `ssh -i <key> -o CertificateFile=<cert>`; the
|
|
345
|
+
* host trusts the CA (`TrustedUserCAKeys`) and never needs the key on disk.
|
|
346
|
+
*
|
|
347
|
+
* Zero-knowledge end to end: the user private key exists only on the consumer,
|
|
348
|
+
* and a certificate is a public artifact (it authorizes but cannot authenticate
|
|
349
|
+
* without the matching private key). The only secret seekrit stores is the CA
|
|
350
|
+
* private key — wrapped to the broker DO's public key, decrypted transiently in
|
|
351
|
+
* the DO to sign (the same in-DO trade the Postgres admin credential makes).
|
|
352
|
+
*
|
|
353
|
+
* Everything here is WebCrypto Ed25519 + hand-rolled SSH wire encoding, so it
|
|
354
|
+
* runs unchanged in the browser, the CLI, and Workers. No Node-specific crypto.
|
|
355
|
+
*/
|
|
356
|
+
const ED25519 = { name: "Ed25519" };
|
|
357
|
+
/**
|
|
358
|
+
* Serializer for the SSH binary wire types. `string` is a uint32 length prefix
|
|
359
|
+
* followed by that many bytes (a length-delimited byte blob, not text); ints
|
|
360
|
+
* are big-endian. This is the encoding used by public keys, certificates, and
|
|
361
|
+
* the OpenSSH private key container alike.
|
|
362
|
+
*/
|
|
363
|
+
var SshWriter = class {
|
|
364
|
+
chunks = [];
|
|
365
|
+
len = 0;
|
|
366
|
+
bytes(b) {
|
|
367
|
+
this.chunks.push(b);
|
|
368
|
+
this.len += b.length;
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
byte(n) {
|
|
372
|
+
return this.bytes(new Uint8Array([n & 255]));
|
|
373
|
+
}
|
|
374
|
+
uint32(n) {
|
|
375
|
+
const b = /* @__PURE__ */ new Uint8Array(4);
|
|
376
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, false);
|
|
377
|
+
return this.bytes(b);
|
|
378
|
+
}
|
|
379
|
+
uint64(n) {
|
|
380
|
+
const b = /* @__PURE__ */ new Uint8Array(8);
|
|
381
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
|
|
382
|
+
return this.bytes(b);
|
|
383
|
+
}
|
|
384
|
+
string(s) {
|
|
385
|
+
const b = typeof s === "string" ? utf8Encode(s) : s;
|
|
386
|
+
return this.uint32(b.length).bytes(b);
|
|
387
|
+
}
|
|
388
|
+
build() {
|
|
389
|
+
const out = new Uint8Array(this.len);
|
|
390
|
+
let o = 0;
|
|
391
|
+
for (const c of this.chunks) {
|
|
392
|
+
out.set(c, o);
|
|
393
|
+
o += c.length;
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
function concat(...arrays) {
|
|
399
|
+
const total = arrays.reduce((n, a) => n + a.length, 0);
|
|
400
|
+
const out = new Uint8Array(total);
|
|
401
|
+
let o = 0;
|
|
402
|
+
for (const a of arrays) {
|
|
403
|
+
out.set(a, o);
|
|
404
|
+
o += a.length;
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
/** The `ssh-ed25519` public-key blob: string "ssh-ed25519" ‖ string <32 bytes>. */
|
|
409
|
+
function ed25519PublicKeyBlob(pub) {
|
|
410
|
+
return new SshWriter().string("ssh-ed25519").string(pub).build();
|
|
411
|
+
}
|
|
412
|
+
function encodeSshPublicKey(pub, comment) {
|
|
413
|
+
const b64 = toBase64(ed25519PublicKeyBlob(pub));
|
|
414
|
+
return comment ? `ssh-ed25519 ${b64} ${comment}` : `ssh-ed25519 ${b64}`;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Generate an ephemeral client keypair. The private key is serialized in the
|
|
418
|
+
* `openssh-key-v1` format so `ssh -i` accepts it directly; the public key is
|
|
419
|
+
* what gets certified. Nothing here is ever sent to the control plane except
|
|
420
|
+
* the public key.
|
|
421
|
+
*/
|
|
422
|
+
async function generateSshKeyPair(comment = "seekrit") {
|
|
423
|
+
const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
|
|
424
|
+
const pub = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
425
|
+
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey));
|
|
426
|
+
const seed = pkcs8.subarray(pkcs8.length - 32);
|
|
427
|
+
return {
|
|
428
|
+
publicKeyOpenssh: encodeSshPublicKey(pub, comment),
|
|
429
|
+
privateKeyOpenssh: encodeOpensshPrivateKey(seed, pub, comment)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Generate a certificate-authority keypair. The private half is exported as a
|
|
434
|
+
* JWK (so the broker can re-import it to sign); the public half is printed for
|
|
435
|
+
* admins to install on their hosts.
|
|
436
|
+
*/
|
|
437
|
+
async function generateSshCaKeyPair(comment = "seekrit-ca") {
|
|
438
|
+
const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
|
|
439
|
+
const pub = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
440
|
+
const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
441
|
+
return {
|
|
442
|
+
privateKeyJwk: JSON.stringify(jwk),
|
|
443
|
+
publicKeyOpenssh: encodeSshPublicKey(pub, comment)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Serialize an Ed25519 keypair as an unencrypted `openssh-key-v1` private key.
|
|
448
|
+
* Format (PROTOCOL.key): magic ‖ ciphername "none" ‖ kdfname "none" ‖ empty
|
|
449
|
+
* kdfoptions ‖ nkeys=1 ‖ public-key blob ‖ private section (wrapped as a
|
|
450
|
+
* string). The private section is two equal check-ints, then the key, then the
|
|
451
|
+
* comment, padded with 1,2,3,… to the "none" block size (8).
|
|
452
|
+
*/
|
|
453
|
+
function encodeOpensshPrivateKey(seed, pub, comment) {
|
|
454
|
+
const check = new DataView(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(4)).buffer).getUint32(0, false);
|
|
455
|
+
const privSection = new SshWriter().uint32(check).uint32(check).string("ssh-ed25519").string(pub).string(concat(seed, pub)).string(comment).build();
|
|
456
|
+
const padLen = (8 - privSection.length % 8) % 8;
|
|
457
|
+
const pad = new Uint8Array(padLen);
|
|
458
|
+
for (let i = 0; i < padLen; i++) pad[i] = i + 1;
|
|
459
|
+
const b64 = toBase64(new SshWriter().bytes(utf8Encode("openssh-key-v1")).byte(0).string("none").string("none").string(/* @__PURE__ */ new Uint8Array(0)).uint32(1).string(ed25519PublicKeyBlob(pub)).string(concat(privSection, pad)).build());
|
|
460
|
+
return `-----BEGIN OPENSSH PRIVATE KEY-----\n${b64.match(/.{1,70}/g)?.join("\n") ?? b64}\n-----END OPENSSH PRIVATE KEY-----\n`;
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
262
463
|
//#region ../../packages/crypto/src/token.ts
|
|
263
464
|
/**
|
|
264
465
|
* Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
|
|
@@ -385,7 +586,7 @@ async function unwrapDek(wrapped, privateKey) {
|
|
|
385
586
|
}
|
|
386
587
|
//#endregion
|
|
387
588
|
//#region package.json
|
|
388
|
-
var version = "0.
|
|
589
|
+
var version = "0.9.0";
|
|
389
590
|
const PROJECT_FILE = "seekrit.json";
|
|
390
591
|
function globalConfigPath() {
|
|
391
592
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
@@ -474,6 +675,12 @@ var SeekritClient = class {
|
|
|
474
675
|
setMyKeys(input) {
|
|
475
676
|
return this.request("PUT", "/v1/me/keys", input);
|
|
476
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
|
+
}
|
|
477
684
|
listOrgs() {
|
|
478
685
|
return this.request("GET", "/v1/orgs");
|
|
479
686
|
}
|
|
@@ -592,7 +799,6 @@ var SeekritClient = class {
|
|
|
592
799
|
listLeases(orgId) {
|
|
593
800
|
return this.request("GET", `/v1/orgs/${orgId}/leases`);
|
|
594
801
|
}
|
|
595
|
-
/** Mint a temporary credential. Send only the client-computed verifier. */
|
|
596
802
|
mintLease(orgId, input) {
|
|
597
803
|
return this.request("POST", `/v1/orgs/${orgId}/leases`, input);
|
|
598
804
|
}
|
|
@@ -733,7 +939,234 @@ function formatSecrets(values, format) {
|
|
|
733
939
|
case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
|
|
734
940
|
}
|
|
735
941
|
}
|
|
736
|
-
|
|
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
|
+
]);
|
|
737
1170
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
738
1171
|
/**
|
|
739
1172
|
* A Postgres role name we are willing to create. Deliberately strict — this
|
|
@@ -747,6 +1180,30 @@ const postgresRoleNameSchema = z.string().regex(/^[a-z_][a-z0-9_]{2,62}$/, "must
|
|
|
747
1180
|
* base64 + the fixed structural characters, none of which is a single quote).
|
|
748
1181
|
*/
|
|
749
1182
|
const scramVerifierSchema = z.string().regex(/^SCRAM-SHA-256\$\d{3,}:[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+$/, "must be a SCRAM-SHA-256 verifier");
|
|
1183
|
+
/**
|
|
1184
|
+
* An SSH login principal (a Unix-style username the certificate authorizes).
|
|
1185
|
+
* Bounded and restricted to a safe charset — principals are SSH-wire-encoded,
|
|
1186
|
+
* not shell-interpolated, so this is sanity/DoS hardening, not an injection gate.
|
|
1187
|
+
*/
|
|
1188
|
+
const sshPrincipalSchema = z.string().regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1–64 chars of letters, digits, dot, dash, underscore");
|
|
1189
|
+
/** An `ssh-ed25519 <base64> [comment]` public key line (deep-validated on sign). */
|
|
1190
|
+
const sshPublicKeySchema = z.string().max(2048).regex(/^ssh-ed25519 [A-Za-z0-9+/=]+( .*)?$/, "must be an ssh-ed25519 public key");
|
|
1191
|
+
/** An SSH certificate extension name, e.g. `permit-pty`. */
|
|
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>)");
|
|
750
1207
|
const postgresAccessLevelSchema = z.enum([
|
|
751
1208
|
"readonly",
|
|
752
1209
|
"readwrite",
|
|
@@ -757,6 +1214,11 @@ const POSTGRES_GROUP_ROLES = {
|
|
|
757
1214
|
readonly: "seekrit_readonly",
|
|
758
1215
|
readwrite: "seekrit_readwrite"
|
|
759
1216
|
};
|
|
1217
|
+
const mysqlAccessLevelSchema = z.enum([
|
|
1218
|
+
"readonly",
|
|
1219
|
+
"readwrite",
|
|
1220
|
+
"custom"
|
|
1221
|
+
]);
|
|
760
1222
|
const connectionSchema = z.object({
|
|
761
1223
|
host: z.string().min(1),
|
|
762
1224
|
port: z.number().int().min(1).max(65535),
|
|
@@ -766,7 +1228,7 @@ const connectionSchema = z.object({
|
|
|
766
1228
|
const statementSchema = z.string().min(1).max(4e3);
|
|
767
1229
|
/** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
|
|
768
1230
|
const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
|
|
769
|
-
const
|
|
1231
|
+
const postgresTargetConfigSchema = z.object({
|
|
770
1232
|
provider: z.literal("postgres"),
|
|
771
1233
|
executor: executorModeSchema,
|
|
772
1234
|
accessLevel: postgresAccessLevelSchema.optional(),
|
|
@@ -776,6 +1238,35 @@ const leaseTargetConfigSchema = z.object({
|
|
|
776
1238
|
createStatements: z.array(statementSchema).max(16).optional(),
|
|
777
1239
|
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
778
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
|
+
});
|
|
1253
|
+
const sshTargetConfigSchema = z.object({
|
|
1254
|
+
provider: z.literal("ssh"),
|
|
1255
|
+
executor: z.literal("in_do"),
|
|
1256
|
+
caPublicKey: sshPublicKeySchema,
|
|
1257
|
+
allowedPrincipals: z.array(sshPrincipalSchema).max(64).optional(),
|
|
1258
|
+
extensions: z.array(sshExtensionSchema).max(16).optional(),
|
|
1259
|
+
maxTtlSeconds: z.number().int().min(60).max(3600 * 24 * 7).optional(),
|
|
1260
|
+
connection: z.object({
|
|
1261
|
+
host: z.string().min(1).optional(),
|
|
1262
|
+
user: sshPrincipalSchema.optional()
|
|
1263
|
+
}).optional()
|
|
1264
|
+
});
|
|
1265
|
+
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
1266
|
+
postgresTargetConfigSchema,
|
|
1267
|
+
mysqlTargetConfigSchema,
|
|
1268
|
+
sshTargetConfigSchema
|
|
1269
|
+
]);
|
|
779
1270
|
z.object({
|
|
780
1271
|
name: z.string().trim().min(1).max(128),
|
|
781
1272
|
config: leaseTargetConfigSchema,
|
|
@@ -786,12 +1277,49 @@ z.object({
|
|
|
786
1277
|
*/
|
|
787
1278
|
wrappedAdminSecret: z.string().min(1)
|
|
788
1279
|
});
|
|
789
|
-
|
|
1280
|
+
/** Requested lease lifetime, shared by all providers. */
|
|
1281
|
+
const ttlSecondsSchema = z.number().int().min(60).max(3600 * 24 * 7);
|
|
1282
|
+
/**
|
|
1283
|
+
* Client → API: mint a Postgres lease. The client generates the password and
|
|
1284
|
+
* its SCRAM verifier locally and sends only the verifier — the plaintext
|
|
1285
|
+
* password never leaves the requesting machine.
|
|
1286
|
+
*/
|
|
1287
|
+
const mintPostgresLeaseSchema = z.object({
|
|
1288
|
+
provider: z.literal("postgres"),
|
|
790
1289
|
targetId: z.string().min(1),
|
|
791
1290
|
roleName: postgresRoleNameSchema,
|
|
792
1291
|
verifier: scramVerifierSchema,
|
|
793
|
-
ttlSeconds:
|
|
1292
|
+
ttlSeconds: ttlSecondsSchema
|
|
1293
|
+
});
|
|
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
|
|
794
1305
|
});
|
|
1306
|
+
/**
|
|
1307
|
+
* Client → API: mint an SSH lease. The client generates an ephemeral keypair
|
|
1308
|
+
* locally and sends only the public key; the signed certificate comes back in
|
|
1309
|
+
* the response. The private key never leaves the requesting machine.
|
|
1310
|
+
*/
|
|
1311
|
+
const mintSshLeaseSchema = z.object({
|
|
1312
|
+
provider: z.literal("ssh"),
|
|
1313
|
+
targetId: z.string().min(1),
|
|
1314
|
+
publicKey: sshPublicKeySchema,
|
|
1315
|
+
principals: z.array(sshPrincipalSchema).min(1).max(32),
|
|
1316
|
+
ttlSeconds: ttlSecondsSchema
|
|
1317
|
+
});
|
|
1318
|
+
z.discriminatedUnion("provider", [
|
|
1319
|
+
mintPostgresLeaseSchema,
|
|
1320
|
+
mintMysqlLeaseSchema,
|
|
1321
|
+
mintSshLeaseSchema
|
|
1322
|
+
]);
|
|
795
1323
|
//#endregion
|
|
796
1324
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
797
1325
|
/**
|
|
@@ -825,6 +1353,46 @@ function postgresGroupBootstrapSql(config) {
|
|
|
825
1353
|
return lines.join("\n");
|
|
826
1354
|
}
|
|
827
1355
|
//#endregion
|
|
1356
|
+
//#region ../../packages/core/src/providers/ssh.ts
|
|
1357
|
+
/**
|
|
1358
|
+
* The one-time host setup an admin runs so a target's certificates are accepted.
|
|
1359
|
+
* Analogue of `postgresGroupBootstrapSql` — displayed for the admin to run, not
|
|
1360
|
+
* executed by seekrit. Embeds the CA public key so it's copy-paste runnable.
|
|
1361
|
+
*/
|
|
1362
|
+
function sshHostSetupInstructions(config) {
|
|
1363
|
+
const principals = config.allowedPrincipals?.length ? config.allowedPrincipals : ["<login-user>"];
|
|
1364
|
+
return [
|
|
1365
|
+
"# Run once on each target host so it trusts seekrit-issued certificates.",
|
|
1366
|
+
"# 1. Install the CA public key and trust it for user authentication:",
|
|
1367
|
+
`echo '${config.caPublicKey}' | sudo tee /etc/ssh/seekrit_ca.pub`,
|
|
1368
|
+
"sudo sh -c 'echo \"TrustedUserCAKeys /etc/ssh/seekrit_ca.pub\" >> /etc/ssh/sshd_config'",
|
|
1369
|
+
"# 2. (optional) Restrict which cert principals may log in as which users via",
|
|
1370
|
+
"# AuthorizedPrincipalsFile, e.g. /etc/ssh/auth_principals/<user> listing:",
|
|
1371
|
+
...principals.map((p) => `# ${p}`),
|
|
1372
|
+
"# 3. Reload sshd:",
|
|
1373
|
+
"sudo systemctl reload sshd"
|
|
1374
|
+
].join("\n");
|
|
1375
|
+
}
|
|
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
|
|
828
1396
|
//#region ../../packages/core/src/schemas.ts
|
|
829
1397
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
830
1398
|
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
@@ -897,6 +1465,7 @@ z.object({
|
|
|
897
1465
|
environmentId: z.string().min(1).nullish(),
|
|
898
1466
|
expiresAt: z.iso.datetime().nullish()
|
|
899
1467
|
});
|
|
1468
|
+
z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
|
|
900
1469
|
z.object({
|
|
901
1470
|
cursor: z.string().optional(),
|
|
902
1471
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
@@ -904,94 +1473,6 @@ z.object({
|
|
|
904
1473
|
resourceType: z.string().optional()
|
|
905
1474
|
});
|
|
906
1475
|
//#endregion
|
|
907
|
-
//#region src/target.ts
|
|
908
|
-
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
909
|
-
async function resolveOrg(ctx, orgSlug) {
|
|
910
|
-
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
911
|
-
const { orgs } = await ctx.client.listOrgs();
|
|
912
|
-
if (wanted) {
|
|
913
|
-
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
914
|
-
if (!org) fail(`no accessible org "${wanted}"`);
|
|
915
|
-
return {
|
|
916
|
-
id: org.id,
|
|
917
|
-
slug: org.slug
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
const only = orgs[0];
|
|
921
|
-
if (orgs.length === 1 && only) return {
|
|
922
|
-
id: only.id,
|
|
923
|
-
slug: only.slug
|
|
924
|
-
};
|
|
925
|
-
fail("specify --org (or run `seekrit init`)");
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
929
|
-
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
930
|
-
*/
|
|
931
|
-
async function resolveEnvTarget(ctx, opts) {
|
|
932
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
933
|
-
if (!opts.env) fail("specify --env");
|
|
934
|
-
if (opts.group) {
|
|
935
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
936
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
937
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
938
|
-
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
939
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
940
|
-
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
941
|
-
return {
|
|
942
|
-
orgId: org.id,
|
|
943
|
-
envId: env.id,
|
|
944
|
-
label: `${group.slug}@${env.slug}`
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
948
|
-
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
949
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
950
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
951
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
952
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
953
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
954
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
955
|
-
return {
|
|
956
|
-
orgId: org.id,
|
|
957
|
-
envId: env.id,
|
|
958
|
-
label: `${app.slug}/${env.slug}`
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
962
|
-
async function resolveAppEnv(ctx, opts) {
|
|
963
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
964
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
965
|
-
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
966
|
-
if (!opts.env) fail("specify --env");
|
|
967
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
968
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
969
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
970
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
971
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
972
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
973
|
-
return {
|
|
974
|
-
orgId: org.id,
|
|
975
|
-
appId: app.id,
|
|
976
|
-
appSlug: app.slug,
|
|
977
|
-
envId: env.id,
|
|
978
|
-
envSlug: env.slug
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
/** Resolve a group by slug within the target org. */
|
|
982
|
-
async function resolveGroup(ctx, opts) {
|
|
983
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
984
|
-
if (!opts.group) fail("specify --group");
|
|
985
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
986
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
987
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
988
|
-
return {
|
|
989
|
-
orgId: org.id,
|
|
990
|
-
id: group.id,
|
|
991
|
-
slug: group.slug
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
//#endregion
|
|
995
1476
|
//#region src/pg.ts
|
|
996
1477
|
/**
|
|
997
1478
|
* `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
|
|
@@ -1003,7 +1484,7 @@ async function resolveGroup(ctx, opts) {
|
|
|
1003
1484
|
* ciphertext.
|
|
1004
1485
|
*/
|
|
1005
1486
|
/** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
|
|
1006
|
-
function parseTtlSeconds(input) {
|
|
1487
|
+
function parseTtlSeconds$1(input) {
|
|
1007
1488
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1008
1489
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
|
|
1009
1490
|
return Number(m[1]) * ({
|
|
@@ -1024,7 +1505,7 @@ function generateRoleName(prefix = "tmp") {
|
|
|
1024
1505
|
function registerPgCommands(program) {
|
|
1025
1506
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
1026
1507
|
const target = pg.command("target").description("manage provisioning targets");
|
|
1027
|
-
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, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect, []).action(async (options) => {
|
|
1508
|
+
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) => {
|
|
1028
1509
|
const ctx = buildContext();
|
|
1029
1510
|
const org = await resolveOrg(ctx, options.org);
|
|
1030
1511
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -1072,6 +1553,7 @@ function registerPgCommands(program) {
|
|
|
1072
1553
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1073
1554
|
for (const t of targets) {
|
|
1074
1555
|
const cfg = t.config;
|
|
1556
|
+
if (cfg.provider !== "postgres") continue;
|
|
1075
1557
|
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
1076
1558
|
}
|
|
1077
1559
|
});
|
|
@@ -1081,7 +1563,9 @@ function registerPgCommands(program) {
|
|
|
1081
1563
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1082
1564
|
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
1083
1565
|
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
1084
|
-
const
|
|
1566
|
+
const cfg = t.config;
|
|
1567
|
+
if (cfg.provider !== "postgres") fail("not a postgres target (see `seekrit ssh`)");
|
|
1568
|
+
const bootstrap = postgresGroupBootstrapSql(cfg);
|
|
1085
1569
|
if (!bootstrap) fail("this is a custom target — it has no generated setup SQL");
|
|
1086
1570
|
console.log(bootstrap);
|
|
1087
1571
|
});
|
|
@@ -1097,10 +1581,12 @@ function registerPgCommands(program) {
|
|
|
1097
1581
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1098
1582
|
const target = targets.find((t) => t.id === targetRef || t.name === targetRef);
|
|
1099
1583
|
if (!target) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1584
|
+
if (target.config.provider !== "postgres") fail(`"${target.name}" is not a postgres target (see \`seekrit ssh\`)`);
|
|
1100
1585
|
const roleName = options.role ?? generateRoleName();
|
|
1101
|
-
const ttlSeconds = parseTtlSeconds(options.ttl);
|
|
1586
|
+
const ttlSeconds = parseTtlSeconds$1(options.ttl);
|
|
1102
1587
|
const { password, verifier } = await generatePostgresCredential();
|
|
1103
1588
|
const { connection } = await ctx.client.mintLease(org.id, {
|
|
1589
|
+
provider: "postgres",
|
|
1104
1590
|
targetId: target.id,
|
|
1105
1591
|
roleName,
|
|
1106
1592
|
verifier,
|
|
@@ -1129,7 +1615,7 @@ function registerPgCommands(program) {
|
|
|
1129
1615
|
});
|
|
1130
1616
|
}
|
|
1131
1617
|
/** Collect a repeatable option into an array. */
|
|
1132
|
-
function collect(value, acc) {
|
|
1618
|
+
function collect$1(value, acc) {
|
|
1133
1619
|
acc.push(value);
|
|
1134
1620
|
return acc;
|
|
1135
1621
|
}
|
|
@@ -1234,6 +1720,147 @@ function printExplain(provenance) {
|
|
|
1234
1720
|
for (const name of names) process.stderr.write(`${name.padEnd(width)} ${provenance[name]}\n`);
|
|
1235
1721
|
}
|
|
1236
1722
|
//#endregion
|
|
1723
|
+
//#region src/ssh.ts
|
|
1724
|
+
/**
|
|
1725
|
+
* `seekrit ssh` — temporary SSH access via short-lived certificates (Vault-style
|
|
1726
|
+
* dynamic secrets, the SSH sibling of `seekrit pg`).
|
|
1727
|
+
*
|
|
1728
|
+
* Zero-knowledge: minting generates an ephemeral keypair on THIS machine and
|
|
1729
|
+
* sends only the public key; the broker signs a certificate and returns it (a
|
|
1730
|
+
* public artifact). The private key never leaves this machine. Registering a
|
|
1731
|
+
* target generates a CA keypair locally and wraps the CA *private* key to the
|
|
1732
|
+
* broker's public key, so the control plane only ever stores ciphertext; the CA
|
|
1733
|
+
* *public* key is printed for you to install on your hosts.
|
|
1734
|
+
*/
|
|
1735
|
+
/** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
|
|
1736
|
+
function parseTtlSeconds(input) {
|
|
1737
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1738
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
|
|
1739
|
+
return Number(m[1]) * ({
|
|
1740
|
+
s: 1,
|
|
1741
|
+
m: 60,
|
|
1742
|
+
h: 3600,
|
|
1743
|
+
d: 86400
|
|
1744
|
+
}[m[2] || "s"] ?? 1);
|
|
1745
|
+
}
|
|
1746
|
+
function registerSshCommands(program) {
|
|
1747
|
+
const ssh = program.command("ssh").description("temporary SSH access (short-lived certificates, zero-knowledge)");
|
|
1748
|
+
const target = ssh.command("target").description("manage SSH CA targets");
|
|
1749
|
+
target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
|
|
1750
|
+
const ctx = buildContext();
|
|
1751
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1752
|
+
const ca = await generateSshCaKeyPair(`seekrit-ca:${options.name}`);
|
|
1753
|
+
const config = {
|
|
1754
|
+
provider: "ssh",
|
|
1755
|
+
executor: "in_do",
|
|
1756
|
+
caPublicKey: ca.publicKeyOpenssh,
|
|
1757
|
+
...options.principal.length ? { allowedPrincipals: options.principal } : {},
|
|
1758
|
+
...options.extension.length ? { extensions: options.extension } : {},
|
|
1759
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds(options.maxTtl) } : {},
|
|
1760
|
+
...options.host || options.user ? { connection: {
|
|
1761
|
+
...options.host ? { host: options.host } : {},
|
|
1762
|
+
...options.user ? { user: options.user } : {}
|
|
1763
|
+
} } : {}
|
|
1764
|
+
};
|
|
1765
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
1766
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(ca.privateKeyJwk), publicKeyJwk);
|
|
1767
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
1768
|
+
name: options.name,
|
|
1769
|
+
config,
|
|
1770
|
+
wrappedAdminSecret
|
|
1771
|
+
});
|
|
1772
|
+
console.error(`registered SSH target ${created.name} (${created.id})`);
|
|
1773
|
+
console.error("\nInstall the CA on your hosts, then issue certs with `seekrit ssh lease`:\n");
|
|
1774
|
+
console.log(sshHostSetupInstructions(config));
|
|
1775
|
+
});
|
|
1776
|
+
target.command("list").description("list SSH CA targets").option("--org <slug>").action(async (options) => {
|
|
1777
|
+
const ctx = buildContext();
|
|
1778
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1779
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1780
|
+
for (const t of targets) {
|
|
1781
|
+
const cfg = t.config;
|
|
1782
|
+
if (cfg.provider !== "ssh") continue;
|
|
1783
|
+
const where = cfg.connection?.host ?? "-";
|
|
1784
|
+
const principals = cfg.allowedPrincipals?.join(",") ?? "any";
|
|
1785
|
+
console.log(`${t.id}\t${t.name}\t${where}\tprincipals=${principals}`);
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>").action(async (targetId, options) => {
|
|
1789
|
+
const ctx = buildContext();
|
|
1790
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1791
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1792
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
1793
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
1794
|
+
const cfg = t.config;
|
|
1795
|
+
if (cfg.provider !== "ssh") fail("not an ssh target (see `seekrit pg`)");
|
|
1796
|
+
console.log(sshHostSetupInstructions(cfg));
|
|
1797
|
+
});
|
|
1798
|
+
target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>").action(async (targetId, options) => {
|
|
1799
|
+
const ctx = buildContext();
|
|
1800
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1801
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
1802
|
+
console.error(`deleted ${targetId}`);
|
|
1803
|
+
});
|
|
1804
|
+
ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
|
|
1805
|
+
const ctx = buildContext();
|
|
1806
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1807
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1808
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
1809
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1810
|
+
const cfg = t.config;
|
|
1811
|
+
if (cfg.provider !== "ssh") fail(`"${t.name}" is not an ssh target (see \`seekrit pg\`)`);
|
|
1812
|
+
const principals = options.principal.length ? options.principal : cfg.connection?.user ? [cfg.connection.user] : cfg.allowedPrincipals ?? [];
|
|
1813
|
+
if (principals.length === 0) fail("specify at least one --principal");
|
|
1814
|
+
const ttlSeconds = parseTtlSeconds(options.ttl);
|
|
1815
|
+
const keypair = await generateSshKeyPair(`seekrit:${t.name}`);
|
|
1816
|
+
const { ssh: cert } = await ctx.client.mintLease(org.id, {
|
|
1817
|
+
provider: "ssh",
|
|
1818
|
+
targetId: t.id,
|
|
1819
|
+
publicKey: keypair.publicKeyOpenssh,
|
|
1820
|
+
principals,
|
|
1821
|
+
ttlSeconds
|
|
1822
|
+
});
|
|
1823
|
+
const dir = options.out ?? mkdtempSync(join(tmpdir(), "seekrit-ssh-"));
|
|
1824
|
+
if (options.out) mkdirSync(dir, { recursive: true });
|
|
1825
|
+
const keyPath = join(dir, "id_ed25519");
|
|
1826
|
+
const certPath = join(dir, "id_ed25519-cert.pub");
|
|
1827
|
+
writeFileSync(keyPath, keypair.privateKeyOpenssh, { mode: 384 });
|
|
1828
|
+
writeFileSync(certPath, `${cert.certificate}\n`, { mode: 420 });
|
|
1829
|
+
const loginUser = principals[0];
|
|
1830
|
+
const command = `ssh -i ${keyPath} -o CertificateFile=${certPath}${cert.host ? ` ${loginUser}@${cert.host}` : ""}`;
|
|
1831
|
+
console.error(`issued cert for ${principals.join(",")} — expires ${cert.expiresAt}`);
|
|
1832
|
+
if (options.json) console.log(JSON.stringify({
|
|
1833
|
+
keyPath,
|
|
1834
|
+
certPath,
|
|
1835
|
+
certificate: cert.certificate,
|
|
1836
|
+
principals,
|
|
1837
|
+
command,
|
|
1838
|
+
expiresAt: cert.expiresAt
|
|
1839
|
+
}, null, 2));
|
|
1840
|
+
else console.log(command);
|
|
1841
|
+
});
|
|
1842
|
+
ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
1843
|
+
const ctx = buildContext();
|
|
1844
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1845
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
1846
|
+
for (const l of leases) {
|
|
1847
|
+
if (l.provider !== "ssh") continue;
|
|
1848
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>").action(async (leaseId, options) => {
|
|
1852
|
+
const ctx = buildContext();
|
|
1853
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1854
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
1855
|
+
console.error(`revoked ${leaseId} (issued certs remain valid until they expire)`);
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
/** Collect a repeatable option into an array. */
|
|
1859
|
+
function collect(value, acc) {
|
|
1860
|
+
acc.push(value);
|
|
1861
|
+
return acc;
|
|
1862
|
+
}
|
|
1863
|
+
//#endregion
|
|
1237
1864
|
//#region src/index.ts
|
|
1238
1865
|
/** Collect repeated `--with group=env` flags into a map. */
|
|
1239
1866
|
function collectKv(value, acc = {}) {
|
|
@@ -1591,8 +2218,10 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
|
|
|
1591
2218
|
console.error(`${tokenId} revoked`);
|
|
1592
2219
|
});
|
|
1593
2220
|
registerPgCommands(program);
|
|
2221
|
+
registerMysqlCommands(program);
|
|
2222
|
+
registerSshCommands(program);
|
|
1594
2223
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
1595
|
-
const { runMcpServer } = await import("./mcp-
|
|
2224
|
+
const { runMcpServer } = await import("./mcp-BEcV_KpM.js");
|
|
1596
2225
|
await runMcpServer();
|
|
1597
2226
|
});
|
|
1598
2227
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -1606,4 +2235,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
1606
2235
|
fail(err instanceof Error ? err.message : String(err));
|
|
1607
2236
|
});
|
|
1608
2237
|
//#endregion
|
|
1609
|
-
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,
|
|
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
|
|
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";
|
|
@@ -560,6 +560,7 @@ async function runMcpServer() {
|
|
|
560
560
|
const roleName = role ?? `tmp_${randomLower(12)}`;
|
|
561
561
|
const { password, verifier } = await generatePostgresCredential();
|
|
562
562
|
const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
|
|
563
|
+
provider: "postgres",
|
|
563
564
|
targetId: t.id,
|
|
564
565
|
roleName,
|
|
565
566
|
verifier,
|
|
@@ -591,6 +592,58 @@ async function runMcpServer() {
|
|
|
591
592
|
leaseId
|
|
592
593
|
};
|
|
593
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
|
+
});
|
|
594
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.", {
|
|
595
648
|
org: z.string(),
|
|
596
649
|
app: z.string(),
|