@seekrit/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/dist/index.js +2230 -0
  3. package/package.json +32 -0
package/dist/index.js ADDED
@@ -0,0 +1,2230 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, parse } from "node:path";
9
+ import { createInterface } from "node:readline";
10
+ import { Writable } from "node:stream";
11
+ //#region ../../packages/crypto/src/encoding.ts
12
+ const CHUNK = 32768;
13
+ /** Base64url (no padding) — portable across browsers, Workers, and Node. */
14
+ function toBase64Url(bytes) {
15
+ let binary = "";
16
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
17
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
18
+ }
19
+ function fromBase64Url(text) {
20
+ const base64 = text.replaceAll("-", "+").replaceAll("_", "/");
21
+ const binary = atob(base64);
22
+ const bytes = new Uint8Array(binary.length);
23
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
24
+ return bytes;
25
+ }
26
+ /**
27
+ * Standard base64 (with `+`, `/`, and `=` padding). Most seekrit blobs use
28
+ * base64url, but some external wire formats mandate standard base64 — notably
29
+ * PostgreSQL SCRAM-SHA-256 verifier strings (see scram.ts).
30
+ */
31
+ function toBase64(bytes) {
32
+ let binary = "";
33
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
34
+ return btoa(binary);
35
+ }
36
+ function utf8Encode(text) {
37
+ return new TextEncoder().encode(text);
38
+ }
39
+ function utf8Decode(bytes) {
40
+ return new TextDecoder().decode(bytes);
41
+ }
42
+ //#endregion
43
+ //#region ../../packages/crypto/src/errors.ts
44
+ var SeekritCryptoError = class extends Error {
45
+ code;
46
+ constructor(code, message) {
47
+ super(message);
48
+ this.name = "SeekritCryptoError";
49
+ this.code = code;
50
+ }
51
+ };
52
+ /**
53
+ * Split a versioned blob like `sc1.<b64>.<b64>` and verify the prefix.
54
+ * AES-GCM auth failure downstream surfaces as DECRYPT_FAILED — that is also
55
+ * the "wrong passphrase" signal for passphrase-encrypted blobs.
56
+ */
57
+ function splitBlob(blob, prefix, segments) {
58
+ const parts = blob.split(".");
59
+ if (parts[0] !== prefix) throw new SeekritCryptoError("UNSUPPORTED_VERSION", `expected a "${prefix}" blob, got "${parts[0] ?? ""}"`);
60
+ if (parts.length !== segments + 1 || parts.some((p) => p.length === 0)) throw new SeekritCryptoError("MALFORMED_BLOB", `malformed "${prefix}" blob`);
61
+ return parts.slice(1);
62
+ }
63
+ //#endregion
64
+ //#region ../../packages/crypto/src/aes.ts
65
+ const SECRET_PREFIX = "sc1";
66
+ const IV_LENGTH$1 = 12;
67
+ /** Generate a fresh 256-bit data encryption key for an environment. */
68
+ function generateDek() {
69
+ return crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
70
+ }
71
+ async function importDek(dek, usage) {
72
+ return crypto.subtle.importKey("raw", dek, { name: "AES-GCM" }, false, [usage]);
73
+ }
74
+ /**
75
+ * Encrypt a secret value with the environment DEK.
76
+ *
77
+ * @param aad Authenticated context binding the ciphertext to its location
78
+ * (e.g. `environmentId/SECRET_NAME`) so blobs cannot be swapped between
79
+ * secrets or environments without detection.
80
+ */
81
+ async function encryptSecret(dek, plaintext, aad) {
82
+ const key = await importDek(dek, "encrypt");
83
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH$1));
84
+ const ciphertext = await crypto.subtle.encrypt({
85
+ name: "AES-GCM",
86
+ iv,
87
+ additionalData: utf8Encode(aad)
88
+ }, key, utf8Encode(plaintext));
89
+ return `${SECRET_PREFIX}.${toBase64Url(iv)}.${toBase64Url(new Uint8Array(ciphertext))}`;
90
+ }
91
+ async function decryptSecret(dek, blob, aad) {
92
+ const [ivB64, ctB64] = splitBlob(blob, SECRET_PREFIX, 2);
93
+ const key = await importDek(dek, "decrypt");
94
+ try {
95
+ const plaintext = await crypto.subtle.decrypt({
96
+ name: "AES-GCM",
97
+ iv: fromBase64Url(ivB64),
98
+ additionalData: utf8Encode(aad)
99
+ }, key, fromBase64Url(ctB64));
100
+ return utf8Decode(new Uint8Array(plaintext));
101
+ } catch {
102
+ throw new SeekritCryptoError("DECRYPT_FAILED", "secret decryption failed: wrong key, tampered data, or mismatched context");
103
+ }
104
+ }
105
+ /** AAD binding a secret ciphertext to its environment + name. */
106
+ function secretAad(environmentId, secretName) {
107
+ return `${environmentId}/${secretName}`;
108
+ }
109
+ //#endregion
110
+ //#region ../../packages/crypto/src/keys.ts
111
+ async function generateKeyPair() {
112
+ const pair = await crypto.subtle.generateKey({
113
+ name: "ECDH",
114
+ namedCurve: "P-256"
115
+ }, true, ["deriveBits"]);
116
+ const [publicJwk, privateJwk] = await Promise.all([crypto.subtle.exportKey("jwk", pair.publicKey), crypto.subtle.exportKey("jwk", pair.privateKey)]);
117
+ return {
118
+ publicKeyJwk: JSON.stringify(publicJwk),
119
+ privateKeyJwk: JSON.stringify(privateJwk)
120
+ };
121
+ }
122
+ async function importPublicKey(publicKeyJwk) {
123
+ return crypto.subtle.importKey("jwk", JSON.parse(publicKeyJwk), {
124
+ name: "ECDH",
125
+ namedCurve: "P-256"
126
+ }, true, []);
127
+ }
128
+ async function importPrivateKey(privateKeyJwk) {
129
+ return crypto.subtle.importKey("jwk", JSON.parse(privateKeyJwk), {
130
+ name: "ECDH",
131
+ namedCurve: "P-256"
132
+ }, true, ["deriveBits"]);
133
+ }
134
+ async function exportPrivateKeyPkcs8(key) {
135
+ return new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
136
+ }
137
+ async function importPrivateKeyPkcs8(pkcs8) {
138
+ return crypto.subtle.importKey("pkcs8", pkcs8, {
139
+ name: "ECDH",
140
+ namedCurve: "P-256"
141
+ }, true, ["deriveBits"]);
142
+ }
143
+ //#endregion
144
+ //#region ../../packages/crypto/src/wrap.ts
145
+ /**
146
+ * ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
147
+ * the recipient's public key; the shared secret is run through HKDF-SHA256 to
148
+ * derive a one-time AES-256-GCM wrapping key. Only the holder of the
149
+ * recipient private key can unwrap.
150
+ *
151
+ * Blob format: `wd1.<ephemeral pub (raw)>.<hkdf salt>.<iv>.<ciphertext>`
152
+ */
153
+ const WRAP_PREFIX = "wd1";
154
+ const HKDF_INFO = "seekrit/wrap-dek/v1";
155
+ async function deriveWrappingKey(ownPrivateKey, peerPublicKey, salt, usage) {
156
+ const ecdh = {
157
+ name: "ECDH",
158
+ public: peerPublicKey
159
+ };
160
+ const sharedBits = await crypto.subtle.deriveBits(ecdh, ownPrivateKey, 256);
161
+ const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
162
+ return crypto.subtle.deriveKey({
163
+ name: "HKDF",
164
+ hash: "SHA-256",
165
+ salt,
166
+ info: utf8Encode(HKDF_INFO)
167
+ }, hkdfKey, {
168
+ name: "AES-GCM",
169
+ length: 256
170
+ }, false, [usage]);
171
+ }
172
+ /** Wrap an environment DEK to a principal's public key. */
173
+ async function wrapDek(dek, recipientPublicKeyJwk) {
174
+ const recipientKey = await importPublicKey(recipientPublicKeyJwk);
175
+ const ephemeral = await crypto.subtle.generateKey({
176
+ name: "ECDH",
177
+ namedCurve: "P-256"
178
+ }, true, ["deriveBits"]);
179
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
180
+ const wrappingKey = await deriveWrappingKey(ephemeral.privateKey, recipientKey, salt, "encrypt");
181
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
182
+ const ciphertext = await crypto.subtle.encrypt({
183
+ name: "AES-GCM",
184
+ iv
185
+ }, wrappingKey, dek);
186
+ const ephemeralRaw = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
187
+ return [
188
+ WRAP_PREFIX,
189
+ toBase64Url(ephemeralRaw),
190
+ toBase64Url(salt),
191
+ toBase64Url(iv),
192
+ toBase64Url(new Uint8Array(ciphertext))
193
+ ].join(".");
194
+ }
195
+ /** Unwrap an environment DEK with the principal's private ECDH key. */
196
+ async function unwrapDek(wrapped, privateKey) {
197
+ const [ephB64, saltB64, ivB64, ctB64] = splitBlob(wrapped, WRAP_PREFIX, 4);
198
+ const wrappingKey = await deriveWrappingKey(privateKey, await crypto.subtle.importKey("raw", fromBase64Url(ephB64), {
199
+ name: "ECDH",
200
+ namedCurve: "P-256"
201
+ }, false, []), fromBase64Url(saltB64), "decrypt");
202
+ try {
203
+ const dek = await crypto.subtle.decrypt({
204
+ name: "AES-GCM",
205
+ iv: fromBase64Url(ivB64)
206
+ }, wrappingKey, fromBase64Url(ctB64));
207
+ return new Uint8Array(dek);
208
+ } catch {
209
+ throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
210
+ }
211
+ }
212
+ //#endregion
213
+ //#region ../../packages/crypto/src/kms.ts
214
+ /**
215
+ * Client-side KMS envelope encryption. A managed `encrypt` key is a 256-bit
216
+ * AES-GCM key whose material reaches the client only as a `wd1.` grant (wrapped
217
+ * to the principal's public key). These helpers operate on that material
218
+ * directly — the server never sees plaintext or key material, exactly as for
219
+ * environment DEKs.
220
+ *
221
+ * Blob formats:
222
+ * `ce1.<keyId>.<version>.<iv>.<ciphertext>` — Encrypt output
223
+ * `dk1.<keyId>.<version>.<iv>.<ciphertext>` — GenerateDataKey wrapped key
224
+ *
225
+ * The keyId + version travel in the blob (so Decrypt can select the right key
226
+ * version) and are folded into the AAD (so a blob can't be replayed under a
227
+ * different key/version). For `ce1` the caller's optional encryption *context*
228
+ * is also bound — Decrypt must supply the same context, mirroring AWS KMS.
229
+ */
230
+ const ENCRYPT_PREFIX = "ce1";
231
+ const DATAKEY_PREFIX = "dk1";
232
+ const IV_LENGTH = 12;
233
+ const KEY_LENGTH = 32;
234
+ /** Generate fresh 256-bit material for an `encrypt` KMS key. */
235
+ function generateEncryptKeyMaterial() {
236
+ return crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
237
+ }
238
+ async function importAesKey(material, usage) {
239
+ return crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, [usage]);
240
+ }
241
+ function encryptAad(ref, context) {
242
+ return `${ref.keyId}/${ref.version}/${context}`;
243
+ }
244
+ function dataKeyAad(keyId, version) {
245
+ return `${keyId}/${version}`;
246
+ }
247
+ function parseVersion(versionStr) {
248
+ const version = Number(versionStr);
249
+ if (!Number.isInteger(version) || version < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid key version in KMS blob");
250
+ return version;
251
+ }
252
+ /** Read the key id + version a `ce1`/`dk1` blob was produced under. */
253
+ function kmsBlobKeyRef(blob) {
254
+ const prefix = blob.split(".")[0];
255
+ if (prefix !== ENCRYPT_PREFIX && prefix !== DATAKEY_PREFIX) throw new SeekritCryptoError("UNSUPPORTED_VERSION", `not a KMS blob: "${prefix ?? ""}"`);
256
+ const [keyId, versionStr] = splitBlob(blob, prefix, 4);
257
+ return {
258
+ keyId,
259
+ version: parseVersion(versionStr)
260
+ };
261
+ }
262
+ /** Encrypt a value under a managed key. `context` (bound as AAD) defaults to empty. */
263
+ async function kmsEncrypt(material, ref, plaintext, context = "") {
264
+ const key = await importAesKey(material, "encrypt");
265
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
266
+ const ciphertext = await crypto.subtle.encrypt({
267
+ name: "AES-GCM",
268
+ iv,
269
+ additionalData: utf8Encode(encryptAad(ref, context))
270
+ }, key, utf8Encode(plaintext));
271
+ return [
272
+ ENCRYPT_PREFIX,
273
+ ref.keyId,
274
+ String(ref.version),
275
+ toBase64Url(iv),
276
+ toBase64Url(new Uint8Array(ciphertext))
277
+ ].join(".");
278
+ }
279
+ /**
280
+ * Decrypt a `ce1` blob. The caller supplies the material for the key version
281
+ * named in the blob (see `kmsBlobKeyRef`) and the same `context` used to
282
+ * encrypt.
283
+ */
284
+ async function kmsDecrypt(material, blob, context = "") {
285
+ const [keyId, versionStr, ivB64, ctB64] = splitBlob(blob, ENCRYPT_PREFIX, 4);
286
+ const ref = {
287
+ keyId,
288
+ version: parseVersion(versionStr)
289
+ };
290
+ const key = await importAesKey(material, "decrypt");
291
+ try {
292
+ const plaintext = await crypto.subtle.decrypt({
293
+ name: "AES-GCM",
294
+ iv: fromBase64Url(ivB64),
295
+ additionalData: utf8Encode(encryptAad(ref, context))
296
+ }, key, fromBase64Url(ctB64));
297
+ return utf8Decode(new Uint8Array(plaintext));
298
+ } catch {
299
+ throw new SeekritCryptoError("DECRYPT_FAILED", "KMS decryption failed: wrong key/version, tampered data, or mismatched context");
300
+ }
301
+ }
302
+ /**
303
+ * Generate a fresh data key wrapped under a managed key — the envelope pattern
304
+ * for large payloads (AWS KMS GenerateDataKey). Encrypt bulk data with
305
+ * `plaintext`, store `wrapped` alongside it, and recover the key later with
306
+ * `decryptDataKey`.
307
+ */
308
+ async function generateDataKey(material, ref) {
309
+ const plaintext = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
310
+ const key = await importAesKey(material, "encrypt");
311
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
312
+ const ciphertext = await crypto.subtle.encrypt({
313
+ name: "AES-GCM",
314
+ iv,
315
+ additionalData: utf8Encode(dataKeyAad(ref.keyId, String(ref.version)))
316
+ }, key, plaintext);
317
+ return {
318
+ plaintext,
319
+ wrapped: [
320
+ DATAKEY_PREFIX,
321
+ ref.keyId,
322
+ String(ref.version),
323
+ toBase64Url(iv),
324
+ toBase64Url(new Uint8Array(ciphertext))
325
+ ].join(".")
326
+ };
327
+ }
328
+ //#endregion
329
+ //#region ../../packages/crypto/src/mysql.ts
330
+ /**
331
+ * Client-side construction of a MySQL/MariaDB `mysql_native_password`
332
+ * authentication string, for minting *temporary MySQL login credentials*
333
+ * without the password plaintext ever reaching seekrit's control plane OR
334
+ * MySQL itself.
335
+ *
336
+ * The trick mirrors the Postgres SCRAM one (scram.ts): the stored auth string
337
+ * is `*<UPPER(HEX(SHA1(SHA1(password))))>`, and
338
+ * `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'` stores that
339
+ * string verbatim — MySQL does NOT re-hash it. So the flow is:
340
+ *
341
+ * 1. the machine that will connect generates a random password locally,
342
+ * 2. computes this hash locally,
343
+ * 3. sends only the hash to the broker → `CREATE USER … AS '<hash>'`,
344
+ * 4. connects directly to MySQL with the plaintext it never shared.
345
+ *
346
+ * Zero-knowledge at both layers: the control plane relays only the hash, and
347
+ * the hash is NOT sufficient to authenticate. `mysql_native_password` login is
348
+ * a challenge-response — the server proves knowledge of `SHA1(SHA1(password))`
349
+ * against a fresh scramble, and verifying a client requires `SHA1(password)`
350
+ * (the preimage of the first inner hash), which the stored double-SHA1 does not
351
+ * reveal. A dump of `mysql.user` / the query log therefore cannot log in.
352
+ *
353
+ * SHA-1 is available via WebCrypto (`crypto.subtle.digest("SHA-1", …)`) in the
354
+ * browser, the CLI, the MCP server, and Workers — so this runs everywhere the
355
+ * SCRAM helper does, with no hand-rolled hash primitive.
356
+ */
357
+ const DEFAULT_PASSWORD_LENGTH$1 = 32;
358
+ const PASSWORD_ALPHABET$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
359
+ async function sha1(data) {
360
+ return new Uint8Array(await crypto.subtle.digest("SHA-1", data));
361
+ }
362
+ function toUpperHex(bytes) {
363
+ let hex = "";
364
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
365
+ return hex.toUpperCase();
366
+ }
367
+ function randomPassword$1(length) {
368
+ let out = "";
369
+ while (out.length < length) {
370
+ const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
371
+ for (const byte of bytes) {
372
+ if (byte < 248) out += PASSWORD_ALPHABET$1[byte % 62];
373
+ if (out.length === length) break;
374
+ }
375
+ }
376
+ return out;
377
+ }
378
+ /**
379
+ * Compute the `mysql_native_password` auth string `*<UPPER(HEX(SHA1(SHA1(pw))))>`
380
+ * for a known password. Pass the result straight to
381
+ * `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<str>'`.
382
+ */
383
+ async function mysqlNativePasswordVerifier(password) {
384
+ return `*${toUpperHex(await sha1(await sha1(utf8Encode(password))))}`;
385
+ }
386
+ /**
387
+ * Mint a fresh random password and its `mysql_native_password` hash in one step
388
+ * — the client-side half of a Vault-style dynamic MySQL credential.
389
+ */
390
+ async function generateMysqlCredential(options = {}) {
391
+ const password = randomPassword$1(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
392
+ return {
393
+ password,
394
+ verifier: await mysqlNativePasswordVerifier(password)
395
+ };
396
+ }
397
+ //#endregion
398
+ //#region ../../packages/crypto/src/passphrase.ts
399
+ /**
400
+ * User private keys are stored server-side encrypted under a key derived from
401
+ * the user's passphrase, so any browser or CLI session can fetch and unlock
402
+ * them without the server ever seeing the passphrase or plaintext key.
403
+ *
404
+ * KDF is PBKDF2-HMAC-SHA256 (WebCrypto-native everywhere). The blob embeds
405
+ * its own salt + iteration count for future agility; bumping ITERATIONS only
406
+ * affects newly written blobs. TODO: revisit Argon2id via WASM later.
407
+ *
408
+ * Blob format: `pk1.<iterations>.<salt>.<iv>.<ciphertext>`
409
+ */
410
+ const PK_PREFIX = "pk1";
411
+ async function deriveKek(passphrase, salt, iterations, usage) {
412
+ const material = await crypto.subtle.importKey("raw", utf8Encode(passphrase), "PBKDF2", false, ["deriveKey"]);
413
+ return crypto.subtle.deriveKey({
414
+ name: "PBKDF2",
415
+ hash: "SHA-256",
416
+ salt,
417
+ iterations
418
+ }, material, {
419
+ name: "AES-GCM",
420
+ length: 256
421
+ }, false, [usage]);
422
+ }
423
+ /** Wrong passphrase surfaces as SeekritCryptoError with code DECRYPT_FAILED. */
424
+ async function decryptPrivateKey(passphrase, blob) {
425
+ const [iterStr, saltB64, ivB64, ctB64] = splitBlob(blob, PK_PREFIX, 4);
426
+ const iterations = Number.parseInt(iterStr, 10);
427
+ if (!Number.isFinite(iterations) || iterations < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid PBKDF2 iteration count");
428
+ const kek = await deriveKek(passphrase, fromBase64Url(saltB64), iterations, "decrypt");
429
+ try {
430
+ const plaintext = await crypto.subtle.decrypt({
431
+ name: "AES-GCM",
432
+ iv: fromBase64Url(ivB64)
433
+ }, kek, fromBase64Url(ctB64));
434
+ return utf8Decode(new Uint8Array(plaintext));
435
+ } catch {
436
+ throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
437
+ }
438
+ }
439
+ //#endregion
440
+ //#region ../../packages/crypto/src/shamir.ts
441
+ /**
442
+ * Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
443
+ * reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b). Splits a byte string
444
+ * into `shares` shares such that any `threshold` of them reconstruct the secret
445
+ * exactly and any fewer reveal nothing about it.
446
+ *
447
+ * This is the one hand-rolled primitive behind org recovery (P0-1): the org
448
+ * recovery private key is split into shares, each wrapped to a designated
449
+ * custodian's public key, so a quorum — never seekrit, never any single
450
+ * custodian below the threshold — can reconstruct it. See recovery.ts for the
451
+ * composition with key wrapping.
452
+ *
453
+ * Each secret byte gets its own degree-(threshold-1) polynomial whose constant
454
+ * term is that byte; a share is that polynomial family evaluated at one distinct
455
+ * nonzero x-coordinate. Reconstruction is Lagrange interpolation back to x = 0.
456
+ *
457
+ * Share wire format: a Uint8Array whose first byte is the share's distinct
458
+ * nonzero x-coordinate and whose remaining bytes are the evaluations p_j(x) for
459
+ * each secret byte j. Self-describing, so combineSecret() needs no external
460
+ * index — the x-coordinate survives being wrapped/unwrapped/re-wrapped intact.
461
+ */
462
+ /** Russian-peasant multiply in GF(2^8) (mod 0x11b) — used only to seed tables. */
463
+ function peasantMul(a, b) {
464
+ let product = 0;
465
+ let x = a;
466
+ let y = b;
467
+ for (let i = 0; i < 8; i++) {
468
+ if (y & 1) product ^= x;
469
+ const carry = x & 128;
470
+ x = x << 1 & 255;
471
+ if (carry) x ^= 27;
472
+ y >>= 1;
473
+ }
474
+ return product;
475
+ }
476
+ const EXP = /* @__PURE__ */ new Uint8Array(512);
477
+ const LOG = /* @__PURE__ */ new Uint8Array(256);
478
+ {
479
+ let a = 1;
480
+ for (let i = 0; i < 255; i++) {
481
+ EXP[i] = a;
482
+ LOG[a] = i;
483
+ a = peasantMul(a, 3);
484
+ }
485
+ for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
486
+ }
487
+ const SALT_LENGTH = 16;
488
+ const DEFAULT_PASSWORD_LENGTH = 32;
489
+ const PASSWORD_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
490
+ async function hmacSha256(key, message) {
491
+ const k = await crypto.subtle.importKey("raw", key, {
492
+ name: "HMAC",
493
+ hash: "SHA-256"
494
+ }, false, ["sign"]);
495
+ return new Uint8Array(await crypto.subtle.sign("HMAC", k, message));
496
+ }
497
+ async function sha256(data) {
498
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
499
+ }
500
+ async function saltPassword(password, salt, iterations) {
501
+ const material = await crypto.subtle.importKey("raw", utf8Encode(password), "PBKDF2", false, ["deriveBits"]);
502
+ const bits = await crypto.subtle.deriveBits({
503
+ name: "PBKDF2",
504
+ hash: "SHA-256",
505
+ salt,
506
+ iterations
507
+ }, material, 256);
508
+ return new Uint8Array(bits);
509
+ }
510
+ function randomPassword(length) {
511
+ let out = "";
512
+ while (out.length < length) {
513
+ const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
514
+ for (const byte of bytes) {
515
+ if (byte < 248) out += PASSWORD_ALPHABET[byte % 62];
516
+ if (out.length === length) break;
517
+ }
518
+ }
519
+ return out;
520
+ }
521
+ /**
522
+ * Compute the `SCRAM-SHA-256$<i>:<salt>$<StoredKey>:<ServerKey>` verifier for a
523
+ * known password. Pass the result straight to `CREATE ROLE … PASSWORD`.
524
+ */
525
+ async function scramSha256Verifier(password, options = {}) {
526
+ const salt = options.salt ?? crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
527
+ const iterations = options.iterations ?? 4096;
528
+ const saltedPassword = await saltPassword(password, salt, iterations);
529
+ const storedKey = await sha256(await hmacSha256(saltedPassword, utf8Encode("Client Key")));
530
+ const serverKey = await hmacSha256(saltedPassword, utf8Encode("Server Key"));
531
+ return `SCRAM-SHA-256$${iterations}:${toBase64(salt)}$${toBase64(storedKey)}:${toBase64(serverKey)}`;
532
+ }
533
+ /**
534
+ * Mint a fresh random password and its SCRAM verifier in one step — the
535
+ * client-side half of a Vault-style dynamic Postgres credential.
536
+ */
537
+ async function generatePostgresCredential(options = {}) {
538
+ const password = randomPassword(options.length ?? DEFAULT_PASSWORD_LENGTH);
539
+ const iterations = options.iterations ?? 4096;
540
+ return {
541
+ password,
542
+ verifier: await scramSha256Verifier(password, { iterations }),
543
+ iterations
544
+ };
545
+ }
546
+ //#endregion
547
+ //#region ../../packages/crypto/src/sign.ts
548
+ /**
549
+ * Managed signing keys for the client-side KMS — ECDSA over P-256 (the curve
550
+ * already used for principal keypairs; universal in WebCrypto). A `sign` key's
551
+ * private half reaches the client only as a `wd1.` grant wrapping its PKCS8
552
+ * bytes; the public half is published per version so verification needs no
553
+ * grant. Signatures are `sg1.<keyId>.<version>.<signature>` — the keyId +
554
+ * version let a verifier fetch the matching version's public key.
555
+ */
556
+ const ECDSA_PARAMS = {
557
+ name: "ECDSA",
558
+ namedCurve: "P-256"
559
+ };
560
+ const ECDSA_SIGN = {
561
+ name: "ECDSA",
562
+ hash: "SHA-256"
563
+ };
564
+ const SIGN_PREFIX = "sg1";
565
+ /** Generate a fresh signing keypair for a `sign` KMS key (or a new version). */
566
+ async function generateSigningKeyMaterial() {
567
+ const pair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
568
+ const [publicJwk, pkcs8] = await Promise.all([crypto.subtle.exportKey("jwk", pair.publicKey), crypto.subtle.exportKey("pkcs8", pair.privateKey)]);
569
+ return {
570
+ publicKeyJwk: JSON.stringify(publicJwk),
571
+ privateKeyPkcs8: new Uint8Array(pkcs8)
572
+ };
573
+ }
574
+ /** Import the wrapped-then-unwrapped PKCS8 private key for signing. */
575
+ async function importSigningKey(pkcs8) {
576
+ return crypto.subtle.importKey("pkcs8", pkcs8, ECDSA_PARAMS, false, ["sign"]);
577
+ }
578
+ /** Import a published public key (JWK) for verification. */
579
+ async function importVerifyingKey(publicKeyJwk) {
580
+ return crypto.subtle.importKey("jwk", JSON.parse(publicKeyJwk), ECDSA_PARAMS, false, ["verify"]);
581
+ }
582
+ /** Sign a message with a managed signing key; returns an `sg1.` blob. */
583
+ async function signMessage(privateKey, ref, message) {
584
+ const data = typeof message === "string" ? utf8Encode(message) : message;
585
+ const sig = new Uint8Array(await crypto.subtle.sign(ECDSA_SIGN, privateKey, data));
586
+ return [
587
+ SIGN_PREFIX,
588
+ ref.keyId,
589
+ String(ref.version),
590
+ toBase64Url(sig)
591
+ ].join(".");
592
+ }
593
+ /** Verify an `sg1.` signature over a message with the version's public key. */
594
+ async function verifyMessage(publicKey, signature, message) {
595
+ const [, , sigB64] = splitBlob(signature, SIGN_PREFIX, 3);
596
+ const data = typeof message === "string" ? utf8Encode(message) : message;
597
+ return crypto.subtle.verify(ECDSA_SIGN, publicKey, fromBase64Url(sigB64), data);
598
+ }
599
+ /** Read the key id + version an `sg1.` signature was produced under. */
600
+ function signatureKeyRef(signature) {
601
+ const [keyId, versionStr] = splitBlob(signature, SIGN_PREFIX, 3);
602
+ const version = Number(versionStr);
603
+ if (!Number.isInteger(version) || version < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid key version in signature");
604
+ return {
605
+ keyId,
606
+ version
607
+ };
608
+ }
609
+ //#endregion
610
+ //#region ../../packages/crypto/src/token.ts
611
+ /**
612
+ * Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
613
+ * principals: the token string itself carries the private key, so the server
614
+ * never holds it. The server stores only the SHA-256 hash of the full token
615
+ * (for authentication) and the public key (for wrapping DEK grants).
616
+ *
617
+ * Format: `skt_<token id>_<private key pkcs8, base64url>`
618
+ */
619
+ const TOKEN_PREFIX = "skt";
620
+ const TOKEN_ID_LENGTH = 22;
621
+ const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
622
+ function randomTokenId() {
623
+ let out = "";
624
+ while (out.length < TOKEN_ID_LENGTH) {
625
+ const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_ID_LENGTH - out.length));
626
+ for (const byte of bytes) {
627
+ if (byte < 248) out += ID_ALPHABET[byte % 62];
628
+ if (out.length === TOKEN_ID_LENGTH) break;
629
+ }
630
+ }
631
+ return `${TOKEN_PREFIX}_${out}`;
632
+ }
633
+ async function hashToken(token) {
634
+ const digest = await crypto.subtle.digest("SHA-256", utf8Encode(token));
635
+ return toBase64Url(new Uint8Array(digest));
636
+ }
637
+ async function createServiceToken() {
638
+ const { publicKeyJwk, privateKeyJwk } = await generateKeyPair();
639
+ const pkcs8 = await exportPrivateKeyPkcs8(await importPrivateKey(privateKeyJwk));
640
+ const tokenId = randomTokenId();
641
+ const token = `${tokenId}_${toBase64Url(pkcs8)}`;
642
+ return {
643
+ token,
644
+ tokenId,
645
+ tokenHash: await hashToken(token),
646
+ publicKeyJwk
647
+ };
648
+ }
649
+ async function parseServiceToken(token) {
650
+ const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
651
+ if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
652
+ const [, tokenId, keyB64] = match;
653
+ try {
654
+ return {
655
+ tokenId,
656
+ privateKey: await importPrivateKeyPkcs8(fromBase64Url(keyB64))
657
+ };
658
+ } catch {
659
+ throw new SeekritCryptoError("MALFORMED_TOKEN", "service token private key is corrupted");
660
+ }
661
+ }
662
+ function isServiceToken(value) {
663
+ return value.startsWith(`${TOKEN_PREFIX}_`);
664
+ }
665
+ //#endregion
666
+ //#region ../cli/package.json
667
+ var version$1 = "0.18.0";
668
+ const PROJECT_FILE = "seekrit.json";
669
+ function globalConfigPath() {
670
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
671
+ }
672
+ function readGlobalConfig() {
673
+ const path = globalConfigPath();
674
+ if (!existsSync(path)) return {};
675
+ return JSON.parse(readFileSync(path, "utf8"));
676
+ }
677
+ /** Walk up from cwd looking for seekrit.json. */
678
+ function findProjectConfig(startDir = process.cwd()) {
679
+ let dir = startDir;
680
+ const { root } = parse(dir);
681
+ while (true) {
682
+ const candidate = join(dir, PROJECT_FILE);
683
+ if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8"));
684
+ if (dir === root) return null;
685
+ dir = dirname(dir);
686
+ }
687
+ }
688
+ function writeProjectConfig(config, dir = process.cwd()) {
689
+ const path = join(dir, PROJECT_FILE);
690
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
691
+ return path;
692
+ }
693
+ //#endregion
694
+ //#region ../../packages/api-client/src/index.ts
695
+ var SeekritApiError = class extends Error {
696
+ status;
697
+ code;
698
+ constructor(status, code, message) {
699
+ super(message);
700
+ this.name = "SeekritApiError";
701
+ this.status = status;
702
+ this.code = code;
703
+ }
704
+ };
705
+ var SeekritClient = class {
706
+ baseUrl;
707
+ auth;
708
+ fetchImpl;
709
+ constructor(options) {
710
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
711
+ this.auth = options.auth;
712
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
713
+ }
714
+ async request(method, path, body) {
715
+ const headers = { accept: "application/json" };
716
+ if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
717
+ else if (this.auth.type === "dynamic") {
718
+ const token = await this.auth.getToken();
719
+ if (!token) throw new SeekritApiError(401, "unauthorized", "session expired");
720
+ headers.authorization = `Bearer ${token}`;
721
+ } else headers["x-seekrit-dev-user"] = this.auth.email;
722
+ if (body !== void 0) headers["content-type"] = "application/json";
723
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
724
+ method,
725
+ headers,
726
+ body: body === void 0 ? void 0 : JSON.stringify(body)
727
+ });
728
+ if (!res.ok) {
729
+ const fallback = { error: {
730
+ code: "internal",
731
+ message: `HTTP ${res.status}`
732
+ } };
733
+ const payload = await res.json().catch(() => fallback);
734
+ throw new SeekritApiError(res.status, payload.error?.code ?? "internal", payload.error?.message ?? `HTTP ${res.status}`);
735
+ }
736
+ return await res.json();
737
+ }
738
+ me() {
739
+ return this.request("GET", "/v1/me");
740
+ }
741
+ getMyKeys() {
742
+ return this.request("GET", "/v1/me/keys");
743
+ }
744
+ setMyKeys(input) {
745
+ return this.request("PUT", "/v1/me/keys", input);
746
+ }
747
+ getMyNotificationPrefs() {
748
+ return this.request("GET", "/v1/me/notifications");
749
+ }
750
+ setMyNotificationPrefs(input) {
751
+ return this.request("PUT", "/v1/me/notifications", input);
752
+ }
753
+ listOrgs() {
754
+ return this.request("GET", "/v1/orgs");
755
+ }
756
+ createOrg(input) {
757
+ return this.request("POST", "/v1/orgs", input);
758
+ }
759
+ getOrg(orgId) {
760
+ return this.request("GET", `/v1/orgs/${orgId}`);
761
+ }
762
+ /** Rename an organization (display name only — the slug is immutable). */
763
+ updateOrg(orgId, input) {
764
+ return this.request("PATCH", `/v1/orgs/${orgId}`, input);
765
+ }
766
+ listMembers(orgId) {
767
+ return this.request("GET", `/v1/orgs/${orgId}/members`);
768
+ }
769
+ /**
770
+ * The org-wide "require a second factor for all members" policy. `configured`
771
+ * is false when the identity provider isn't wired up (local dev), in which
772
+ * case the toggle is inert.
773
+ */
774
+ getMfaPolicy(orgId) {
775
+ return this.request("GET", `/v1/orgs/${orgId}/mfa-policy`);
776
+ }
777
+ setMfaPolicy(orgId, input) {
778
+ return this.request("PATCH", `/v1/orgs/${orgId}/mfa-policy`, input);
779
+ }
780
+ listInvites(orgId) {
781
+ return this.request("GET", `/v1/orgs/${orgId}/invites`);
782
+ }
783
+ createInvite(orgId, input) {
784
+ return this.request("POST", `/v1/orgs/${orgId}/invites`, input);
785
+ }
786
+ revokeInvite(orgId, inviteId) {
787
+ return this.request("DELETE", `/v1/orgs/${orgId}/invites/${inviteId}`);
788
+ }
789
+ listApps(orgId) {
790
+ return this.request("GET", `/v1/orgs/${orgId}/apps`);
791
+ }
792
+ createApp(orgId, input) {
793
+ return this.request("POST", `/v1/orgs/${orgId}/apps`, input);
794
+ }
795
+ getApp(orgId, appId) {
796
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}`);
797
+ }
798
+ /** Rename an application (display name only — the slug is immutable). */
799
+ updateApp(orgId, appId, input) {
800
+ return this.request("PATCH", `/v1/orgs/${orgId}/apps/${appId}`, input);
801
+ }
802
+ deleteApp(orgId, appId) {
803
+ return this.request("DELETE", `/v1/orgs/${orgId}/apps/${appId}`);
804
+ }
805
+ listEnvs(orgId, appId) {
806
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}/envs`);
807
+ }
808
+ createEnv(orgId, appId, input) {
809
+ return this.request("POST", `/v1/orgs/${orgId}/apps/${appId}/envs`, input);
810
+ }
811
+ getEnv(orgId, envId) {
812
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}`);
813
+ }
814
+ deleteEnv(orgId, envId) {
815
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}`);
816
+ }
817
+ listGroups(orgId) {
818
+ return this.request("GET", `/v1/orgs/${orgId}/groups`);
819
+ }
820
+ createGroup(orgId, input) {
821
+ return this.request("POST", `/v1/orgs/${orgId}/groups`, input);
822
+ }
823
+ getGroup(orgId, groupId) {
824
+ return this.request("GET", `/v1/orgs/${orgId}/groups/${groupId}`);
825
+ }
826
+ /** Rename a group (display name only — the slug is immutable). */
827
+ updateGroup(orgId, groupId, input) {
828
+ return this.request("PATCH", `/v1/orgs/${orgId}/groups/${groupId}`, input);
829
+ }
830
+ deleteGroup(orgId, groupId) {
831
+ return this.request("DELETE", `/v1/orgs/${orgId}/groups/${groupId}`);
832
+ }
833
+ listGroupEnvs(orgId, groupId) {
834
+ return this.request("GET", `/v1/orgs/${orgId}/groups/${groupId}/envs`);
835
+ }
836
+ createGroupEnv(orgId, groupId, input) {
837
+ return this.request("POST", `/v1/orgs/${orgId}/groups/${groupId}/envs`, input);
838
+ }
839
+ listEnvGroups(orgId, envId) {
840
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/groups`);
841
+ }
842
+ linkEnvGroup(orgId, envId, input) {
843
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/groups`, input);
844
+ }
845
+ unlinkEnvGroup(orgId, envId, groupId) {
846
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/groups/${groupId}`);
847
+ }
848
+ /** Ordered ciphertext layers + wrapped DEKs for the calling principal. */
849
+ resolve(query = {}) {
850
+ const params = new URLSearchParams();
851
+ if (query.env) params.set("env", query.env);
852
+ for (const [group, slug] of Object.entries(query.with ?? {})) params.append("with", `${group}:${slug}`);
853
+ const qs = params.size > 0 ? `?${params}` : "";
854
+ return this.request("GET", `/v1/resolve${qs}`);
855
+ }
856
+ listSecrets(orgId, envId) {
857
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets`);
858
+ }
859
+ setSecret(orgId, envId, name, ciphertext) {
860
+ return this.request("PUT", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`, { ciphertext });
861
+ }
862
+ deleteSecret(orgId, envId, name) {
863
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
864
+ }
865
+ /** The calling principal's wrapped DEK for this environment. */
866
+ getMyEnvKey(orgId, envId) {
867
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
868
+ }
869
+ listEnvKeys(orgId, envId) {
870
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/keys`);
871
+ }
872
+ grantEnvKey(orgId, envId, input) {
873
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/keys`, input);
874
+ }
875
+ revokeEnvKey(orgId, envId, grantId) {
876
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/keys/${grantId}`);
877
+ }
878
+ /** Org recovery status: threshold, custodians, and environment coverage. */
879
+ getRecovery(orgId) {
880
+ return this.request("GET", `/v1/orgs/${orgId}/recovery`);
881
+ }
882
+ /** Enable recovery with the recovery public key, custodian shares, and grants. */
883
+ configureRecovery(orgId, input) {
884
+ return this.request("POST", `/v1/orgs/${orgId}/recovery`, input);
885
+ }
886
+ /** Rotate the recovery key: new keypair, custodian set, and env re-wraps. */
887
+ rotateRecovery(orgId, input) {
888
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/rotate`, input);
889
+ }
890
+ disableRecovery(orgId) {
891
+ return this.request("DELETE", `/v1/orgs/${orgId}/recovery`);
892
+ }
893
+ /** The calling principal's own wrapped recovery share (custodian only). */
894
+ getMyRecoveryShare(orgId) {
895
+ return this.request("GET", `/v1/orgs/${orgId}/recovery/share`);
896
+ }
897
+ /** Every environment DEK wrapped to the org recovery key (admin only). */
898
+ getRecoveryEnvKeys(orgId) {
899
+ return this.request("GET", `/v1/orgs/${orgId}/recovery/env-keys`);
900
+ }
901
+ /** Backfill recovery grants for environments the caller can decrypt. */
902
+ uploadRecoveryGrants(orgId, input) {
903
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/grants`, input);
904
+ }
905
+ listRecoveryRequests(orgId) {
906
+ return this.request("GET", `/v1/orgs/${orgId}/recovery/requests`);
907
+ }
908
+ createRecoveryRequest(orgId, input) {
909
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/requests`, input);
910
+ }
911
+ getRecoveryRequest(orgId, requestId) {
912
+ return this.request("GET", `/v1/orgs/${orgId}/recovery/requests/${requestId}`);
913
+ }
914
+ /** A custodian contributes their share, re-wrapped to the request target. */
915
+ contributeRecoveryShare(orgId, requestId, input) {
916
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/shares`, input);
917
+ }
918
+ /** The target finalizes recovery, re-granting itself the recovered DEKs. */
919
+ completeRecoveryRequest(orgId, requestId, input) {
920
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/complete`, input);
921
+ }
922
+ cancelRecoveryRequest(orgId, requestId) {
923
+ return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/cancel`);
924
+ }
925
+ listTokens(orgId) {
926
+ return this.request("GET", `/v1/orgs/${orgId}/tokens`);
927
+ }
928
+ createToken(orgId, input) {
929
+ return this.request("POST", `/v1/orgs/${orgId}/tokens`, input);
930
+ }
931
+ revokeToken(orgId, tokenId) {
932
+ return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
933
+ }
934
+ /** Permanently delete a token. Only allowed once it has been revoked. */
935
+ deleteToken(orgId, tokenId) {
936
+ return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}/permanent`);
937
+ }
938
+ /** Keys the caller can see: all org keys for admins, granted keys otherwise. */
939
+ listKmsKeys(orgId) {
940
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
941
+ }
942
+ createKmsKey(orgId, input) {
943
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys`, input);
944
+ }
945
+ /** Key metadata + every version (admin). */
946
+ getKmsKey(orgId, keyId) {
947
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}`);
948
+ }
949
+ /** The caller's wrapped key material for a key, across granted versions. */
950
+ getMyKmsKey(orgId, keyId) {
951
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/key`);
952
+ }
953
+ /** Published public keys of a `sign` key (grant-free within the org). */
954
+ getKmsPublicKeys(orgId, keyId) {
955
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/public`);
956
+ }
957
+ listKmsGrants(orgId, keyId) {
958
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants`);
959
+ }
960
+ grantKmsKey(orgId, keyId, input) {
961
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants`, input);
962
+ }
963
+ /** Revoke a principal entirely (all versions). */
964
+ revokeKmsKey(orgId, keyId, principal) {
965
+ const qs = new URLSearchParams(principal).toString();
966
+ return this.request("DELETE", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants?${qs}`);
967
+ }
968
+ rotateKmsKey(orgId, keyId, input) {
969
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/rotate`, input);
970
+ }
971
+ disableKmsKey(orgId, keyId) {
972
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/disable`);
973
+ }
974
+ /** The broker's public key — wrap the admin credential to it before registering a target. */
975
+ getLeaseBrokerKey(orgId) {
976
+ return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
977
+ }
978
+ listLeaseTargets(orgId) {
979
+ return this.request("GET", `/v1/orgs/${orgId}/leases/targets`);
980
+ }
981
+ registerLeaseTarget(orgId, input) {
982
+ return this.request("POST", `/v1/orgs/${orgId}/leases/targets`, input);
983
+ }
984
+ deleteLeaseTarget(orgId, targetId) {
985
+ return this.request("DELETE", `/v1/orgs/${orgId}/leases/targets/${targetId}`);
986
+ }
987
+ listLeases(orgId) {
988
+ return this.request("GET", `/v1/orgs/${orgId}/leases`);
989
+ }
990
+ mintLease(orgId, input) {
991
+ return this.request("POST", `/v1/orgs/${orgId}/leases`, input);
992
+ }
993
+ revokeLease(orgId, leaseId) {
994
+ return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
995
+ }
996
+ listAudit(orgId, query = {}) {
997
+ const params = new URLSearchParams();
998
+ if (query.cursor) params.set("cursor", query.cursor);
999
+ if (query.limit) params.set("limit", String(query.limit));
1000
+ if (query.action) params.set("action", query.action);
1001
+ if (query.resourceType) params.set("resourceType", query.resourceType);
1002
+ const qs = params.size > 0 ? `?${params}` : "";
1003
+ return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
1004
+ }
1005
+ getLogSink(orgId) {
1006
+ return this.request("GET", `/v1/orgs/${orgId}/log-sink`);
1007
+ }
1008
+ setLogSink(orgId, input) {
1009
+ return this.request("PUT", `/v1/orgs/${orgId}/log-sink`, input);
1010
+ }
1011
+ deleteLogSink(orgId) {
1012
+ return this.request("DELETE", `/v1/orgs/${orgId}/log-sink`);
1013
+ }
1014
+ /** Send a synthetic record to the configured endpoint to verify connectivity. */
1015
+ testLogSink(orgId) {
1016
+ return this.request("POST", `/v1/orgs/${orgId}/log-sink/test`);
1017
+ }
1018
+ /**
1019
+ * The org's plan, effective entitlements, current metered usage ("X of Y"),
1020
+ * overrides, and which self-serve actions are available (`manage`). Readable
1021
+ * by any member; `enforced` reports whether limits are currently active
1022
+ * (false until plans are turned on, when every org has full access).
1023
+ */
1024
+ getBilling(orgId) {
1025
+ return this.request("GET", `/v1/orgs/${orgId}/billing`);
1026
+ }
1027
+ /**
1028
+ * Start self-serve checkout to upgrade the org to a plan family. Returns a
1029
+ * biller-hosted URL to redirect the browser to. Admin-only; the biller must
1030
+ * be configured (see `getBilling().manage`). The resulting subscription links
1031
+ * back to the org via the checkout webhook.
1032
+ */
1033
+ startCheckout(orgId, input) {
1034
+ return this.request("POST", `/v1/orgs/${orgId}/billing/checkout`, input);
1035
+ }
1036
+ /**
1037
+ * Open the biller's Billing Portal to manage the existing subscription
1038
+ * (update card, change plan, cancel). Returns a URL to redirect to. Admin-only
1039
+ * and only once a biller customer is linked (see `getBilling().manage.portal`).
1040
+ */
1041
+ openBillingPortal(orgId) {
1042
+ return this.request("POST", `/v1/orgs/${orgId}/billing/portal`);
1043
+ }
1044
+ /**
1045
+ * Downgrade the org to the Free (default) plan: cancels any active paid
1046
+ * subscription in the biller and reverts the org to Free immediately.
1047
+ * Admin-only. Refetch `getBilling` for the new state.
1048
+ */
1049
+ cancelSubscription(orgId) {
1050
+ return this.request("POST", `/v1/orgs/${orgId}/billing/cancel`);
1051
+ }
1052
+ };
1053
+ //#endregion
1054
+ //#region ../cli/src/io.ts
1055
+ let failThrows = false;
1056
+ /**
1057
+ * In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
1058
+ * must surface as a catchable error (→ a tool error result) rather than
1059
+ * exiting and killing every other tool. Toggled on once at MCP startup.
1060
+ */
1061
+ function setFailThrows(value) {
1062
+ failThrows = value;
1063
+ }
1064
+ function fail(message) {
1065
+ if (failThrows) throw new Error(message);
1066
+ console.error(`error: ${message}`);
1067
+ process.exit(1);
1068
+ }
1069
+ /** Prompt without echoing input (for passphrases). */
1070
+ function promptHidden(question) {
1071
+ const muted = new Writable({ write(_chunk, _encoding, callback) {
1072
+ callback();
1073
+ } });
1074
+ process.stderr.write(question);
1075
+ const rl = createInterface({
1076
+ input: process.stdin,
1077
+ output: muted,
1078
+ terminal: true
1079
+ });
1080
+ return new Promise((resolve) => {
1081
+ rl.question("", (answer) => {
1082
+ rl.close();
1083
+ process.stderr.write("\n");
1084
+ resolve(answer);
1085
+ });
1086
+ });
1087
+ }
1088
+ //#endregion
1089
+ //#region ../cli/src/context.ts
1090
+ /**
1091
+ * Build the client context from configured credentials, or return null when
1092
+ * none are set. `seekrit run` uses this to degrade to a plain launcher instead
1093
+ * of exiting; every other command goes through `buildContext`, which fails.
1094
+ *
1095
+ * `dotenvVars` supplies `SEEKRIT_*` values read from a `.env` file. They sit
1096
+ * below the live `process.env` but above the saved config, so a project-local
1097
+ * `.env` can carry the token / API URL — matching `seekrit-run`'s
1098
+ * `flag > env > .env` credential resolution. Empty for every command but
1099
+ * `seekrit run`, which loads `.env` before authenticating.
1100
+ */
1101
+ function tryBuildContext(dotenvVars = {}) {
1102
+ const config = readGlobalConfig();
1103
+ const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
1104
+ const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
1105
+ const token = fromEnv("SEEKRIT_TOKEN") ?? config.token;
1106
+ const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
1107
+ let auth;
1108
+ if (token) auth = {
1109
+ type: "bearer",
1110
+ token
1111
+ };
1112
+ else if (devUser) auth = {
1113
+ type: "dev",
1114
+ email: devUser
1115
+ };
1116
+ else return null;
1117
+ return {
1118
+ client: new SeekritClient({
1119
+ baseUrl: apiUrl,
1120
+ auth
1121
+ }),
1122
+ auth
1123
+ };
1124
+ }
1125
+ function isTokenAuth(ctx) {
1126
+ return ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token);
1127
+ }
1128
+ /**
1129
+ * Recover the calling principal's private key:
1130
+ * - service tokens carry their private key in the token string;
1131
+ * - users fetch their passphrase-encrypted key from the API and unlock it.
1132
+ */
1133
+ async function getPrivateKey(ctx) {
1134
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
1135
+ const { privateKey } = await parseServiceToken(ctx.auth.token);
1136
+ return privateKey;
1137
+ }
1138
+ const { encryptedPrivateKey } = await ctx.client.getMyKeys();
1139
+ return importPrivateKey(await decryptPrivateKey(process.env.SEEKRIT_PASSPHRASE ?? await promptHidden("Passphrase: "), encryptedPrivateKey));
1140
+ }
1141
+ /** Recover one environment's DEK for the current principal. */
1142
+ async function getDek(ctx, orgId, envId) {
1143
+ const [{ wrappedDek }, privateKey] = await Promise.all([ctx.client.getMyEnvKey(orgId, envId), getPrivateKey(ctx)]);
1144
+ return unwrapDek(wrappedDek, privateKey);
1145
+ }
1146
+ //#endregion
1147
+ //#region ../cli/src/target.ts
1148
+ /** Resolve the target org from a flag, the committed config, or a lone org. */
1149
+ async function resolveOrg(ctx, orgSlug) {
1150
+ const wanted = orgSlug ?? findProjectConfig()?.org;
1151
+ const { orgs } = await ctx.client.listOrgs();
1152
+ if (wanted) {
1153
+ const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
1154
+ if (!org) fail(`no accessible org "${wanted}"`);
1155
+ return {
1156
+ id: org.id,
1157
+ slug: org.slug
1158
+ };
1159
+ }
1160
+ const only = orgs[0];
1161
+ if (orgs.length === 1 && only) return {
1162
+ id: only.id,
1163
+ slug: only.slug
1164
+ };
1165
+ fail("specify --org (or run `seekrit init`)");
1166
+ }
1167
+ /**
1168
+ * Resolve an environment to operate on — an application env (`--app --env`,
1169
+ * or the config's app + `--env`) or a group env (`--group --env`).
1170
+ */
1171
+ async function resolveEnvTarget(ctx, opts) {
1172
+ const org = await resolveOrg(ctx, opts.org);
1173
+ if (!opts.env) fail("specify --env");
1174
+ if (opts.group) {
1175
+ const { groups } = await ctx.client.listGroups(org.id);
1176
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
1177
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
1178
+ const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
1179
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1180
+ if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
1181
+ return {
1182
+ orgId: org.id,
1183
+ envId: env.id,
1184
+ label: `${group.slug}@${env.slug}`
1185
+ };
1186
+ }
1187
+ const appSlug = opts.app ?? findProjectConfig()?.app;
1188
+ if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
1189
+ const { apps } = await ctx.client.listApps(org.id);
1190
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
1191
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
1192
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
1193
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1194
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
1195
+ return {
1196
+ orgId: org.id,
1197
+ envId: env.id,
1198
+ label: `${app.slug}/${env.slug}`
1199
+ };
1200
+ }
1201
+ /** Resolve an application environment, keeping ids + slugs (for token binding). */
1202
+ async function resolveAppEnv(ctx, opts) {
1203
+ const org = await resolveOrg(ctx, opts.org);
1204
+ const appSlug = opts.app ?? findProjectConfig()?.app;
1205
+ if (!appSlug) fail("specify --app (or run `seekrit init`)");
1206
+ if (!opts.env) fail("specify --env");
1207
+ const { apps } = await ctx.client.listApps(org.id);
1208
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
1209
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
1210
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
1211
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
1212
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
1213
+ return {
1214
+ orgId: org.id,
1215
+ appId: app.id,
1216
+ appSlug: app.slug,
1217
+ envId: env.id,
1218
+ envSlug: env.slug
1219
+ };
1220
+ }
1221
+ /** Resolve a group by slug within the target org. */
1222
+ async function resolveGroup(ctx, opts) {
1223
+ const org = await resolveOrg(ctx, opts.org);
1224
+ if (!opts.group) fail("specify --group");
1225
+ const { groups } = await ctx.client.listGroups(org.id);
1226
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
1227
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
1228
+ return {
1229
+ orgId: org.id,
1230
+ id: group.id,
1231
+ slug: group.slug
1232
+ };
1233
+ }
1234
+ //#endregion
1235
+ //#region ../cli/src/kms.ts
1236
+ /** The calling principal's identity + public key (for a self-grant). */
1237
+ async function kmsCallerIdentity(ctx) {
1238
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
1239
+ const { tokenId, privateKey } = await parseServiceToken(ctx.auth.token);
1240
+ const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
1241
+ return {
1242
+ principalType: "service_token",
1243
+ principalId: tokenId,
1244
+ publicKeyJwk: JSON.stringify(pub)
1245
+ };
1246
+ }
1247
+ const { user } = await ctx.client.me();
1248
+ if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
1249
+ return {
1250
+ principalType: "user",
1251
+ principalId: user.id,
1252
+ publicKeyJwk: user.publicKeyJwk
1253
+ };
1254
+ }
1255
+ /** Look up an org member (by email) or service token (by id) as a grant recipient. */
1256
+ async function kmsResolveRecipient(ctx, orgId, who) {
1257
+ if (who.user) {
1258
+ const { members } = await ctx.client.listMembers(orgId);
1259
+ const m = members.find((x) => x.email === who.user);
1260
+ if (!m) fail(`no member ${who.user}`);
1261
+ if (!m.publicKeyJwk) fail(`${who.user} has not completed key setup`);
1262
+ return {
1263
+ principalType: "user",
1264
+ principalId: m.userId,
1265
+ publicKeyJwk: m.publicKeyJwk
1266
+ };
1267
+ }
1268
+ if (who.token) {
1269
+ const { tokens } = await ctx.client.listTokens(orgId);
1270
+ const t = tokens.find((x) => x.id === who.token);
1271
+ if (!t) fail(`no service token ${who.token}`);
1272
+ return {
1273
+ principalType: "service_token",
1274
+ principalId: t.id,
1275
+ publicKeyJwk: t.publicKeyJwk
1276
+ };
1277
+ }
1278
+ fail("specify --user <email> or --token <id>");
1279
+ }
1280
+ async function kmsResolveKey(ctx, orgId, ref) {
1281
+ const { keys } = await ctx.client.listKmsKeys(orgId);
1282
+ const key = keys.find((k) => k.id === ref || k.name === ref);
1283
+ if (!key) fail(`no KMS key "${ref}"`);
1284
+ return key;
1285
+ }
1286
+ /** Recover a key's material for one version (default: current), for the caller. */
1287
+ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
1288
+ const mat = await ctx.client.getMyKmsKey(orgId, keyId);
1289
+ const v = version ?? mat.currentVersion;
1290
+ const grant = mat.grants.find((g) => g.version === v);
1291
+ if (!grant) fail(`no grant for version ${v} of this key`);
1292
+ return {
1293
+ material: await unwrapDek(grant.wrappedKey, await getPrivateKey(ctx)),
1294
+ version: v,
1295
+ currentVersion: mat.currentVersion
1296
+ };
1297
+ }
1298
+ //#endregion
1299
+ //#region ../cli/src/dotenv.ts
1300
+ /**
1301
+ * Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
1302
+ * prefix, and single/double-quoted values (double quotes honor `\n \t \r \" \\`
1303
+ * escapes; unquoted values drop trailing ` # comments`). Multiline values are
1304
+ * not supported — keep those in seekrit itself.
1305
+ */
1306
+ function parseDotenv(content) {
1307
+ const out = {};
1308
+ for (const raw of content.split(/\r?\n/)) {
1309
+ let line = raw.trim();
1310
+ if (!line || line.startsWith("#")) continue;
1311
+ if (line.startsWith("export ")) line = line.slice(7).trimStart();
1312
+ const eq = line.indexOf("=");
1313
+ if (eq === -1) continue;
1314
+ const key = line.slice(0, eq).trim();
1315
+ if (!key) continue;
1316
+ let value = line.slice(eq + 1).trim();
1317
+ const quote = value[0];
1318
+ if (value.length >= 2 && (quote === "\"" || quote === "'") && value.at(-1) === quote) {
1319
+ value = value.slice(1, -1);
1320
+ if (quote === "\"") value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
1321
+ } else {
1322
+ const comment = value.indexOf(" #");
1323
+ if (comment !== -1) value = value.slice(0, comment).trim();
1324
+ }
1325
+ out[key] = value;
1326
+ }
1327
+ return out;
1328
+ }
1329
+ //#endregion
1330
+ //#region ../cli/src/secrets.ts
1331
+ /** Fetch + decrypt every secret in a single environment. */
1332
+ async function fetchDecryptedSecrets(ctx, orgId, envId) {
1333
+ const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
1334
+ const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
1335
+ return Object.fromEntries(entries);
1336
+ }
1337
+ async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
1338
+ const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
1339
+ await ctx.client.setSecret(orgId, envId, name, ciphertext);
1340
+ }
1341
+ /**
1342
+ * Resolve the full, layered environment for a running app: composed group
1343
+ * secrets (lowest precedence) → the app env's own secrets → `.env` files.
1344
+ * Each layer's DEK is unwrapped once with the principal's private key and its
1345
+ * ciphertext decrypted locally. `process.env` is NOT applied here — callers
1346
+ * that spawn a process layer it on top so the live shell always wins.
1347
+ */
1348
+ async function materializeEnv(ctx, opts) {
1349
+ const query = {};
1350
+ if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
1351
+ if (!isTokenAuth(ctx)) {
1352
+ if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
1353
+ query.env = opts.envId;
1354
+ }
1355
+ const { scope, layers } = await ctx.client.resolve(query);
1356
+ const privateKey = await getPrivateKey(ctx);
1357
+ const values = {};
1358
+ const provenance = {};
1359
+ for (const layer of layers) {
1360
+ const dek = await unwrapDek(layer.wrappedDek, privateKey);
1361
+ const label = layer.source === "group" ? `group:${layer.groupSlug}@${layer.slug}` : `app:${scope.appSlug}/${layer.slug}`;
1362
+ for (const secret of layer.secrets) {
1363
+ values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
1364
+ provenance[secret.name] = label;
1365
+ }
1366
+ }
1367
+ return {
1368
+ values,
1369
+ provenance,
1370
+ scope,
1371
+ loadedEnvFiles: overlayEnvFiles(values, provenance, opts.envFiles)
1372
+ };
1373
+ }
1374
+ /**
1375
+ * Overlay `.env` files onto an existing value/provenance set (later files win).
1376
+ * Missing files are skipped. Returns the files that were actually loaded. Used
1377
+ * both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
1378
+ * managed secrets are unavailable but `.env` should still apply.
1379
+ */
1380
+ function overlayEnvFiles(values, provenance, envFiles) {
1381
+ const loaded = [];
1382
+ for (const file of envFiles) {
1383
+ if (!existsSync(file)) continue;
1384
+ loaded.push(file);
1385
+ for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
1386
+ values[name] = value;
1387
+ provenance[name] = `dotenv:${file}`;
1388
+ }
1389
+ }
1390
+ return loaded;
1391
+ }
1392
+ //#endregion
1393
+ //#region ../cli/src/mcp.ts
1394
+ /** Lowercase-alphanumeric string for a fresh Postgres role name. */
1395
+ function randomLower(length) {
1396
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
1397
+ let out = "";
1398
+ for (const b of crypto.getRandomValues(new Uint8Array(length))) out += alphabet[b % 36];
1399
+ return out;
1400
+ }
1401
+ function jsonText(data) {
1402
+ return { content: [{
1403
+ type: "text",
1404
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
1405
+ }] };
1406
+ }
1407
+ function errText(err) {
1408
+ return {
1409
+ content: [{
1410
+ type: "text",
1411
+ text: `error: ${err instanceof Error ? err.message : String(err)}`
1412
+ }],
1413
+ isError: true
1414
+ };
1415
+ }
1416
+ /** Build the client context or throw a friendly, agent-readable error. */
1417
+ function getCtx() {
1418
+ const ctx = tryBuildContext();
1419
+ if (!ctx) throw new Error("no credentials — set SEEKRIT_TOKEN (a skt_… token) or SEEKRIT_DEV_USER in the MCP server env, or run `seekrit login` first");
1420
+ return ctx;
1421
+ }
1422
+ /**
1423
+ * Guard tools that decrypt: under user auth we cannot prompt for a passphrase
1424
+ * (stdin is the transport), so it must be supplied out-of-band.
1425
+ */
1426
+ function ensureDecryptable(ctx) {
1427
+ if (!isTokenAuth(ctx) && !process.env.SEEKRIT_PASSPHRASE) throw new Error("this operation decrypts data; set SEEKRIT_PASSPHRASE in the MCP server env, or use a service token (SEEKRIT_TOKEN)");
1428
+ }
1429
+ /**
1430
+ * The calling principal's public key JWK — needed to wrap a freshly generated
1431
+ * DEK when creating an environment. Service tokens carry their private key, so
1432
+ * we derive the public half from it; users publish theirs at key setup.
1433
+ */
1434
+ async function principalPublicKeyJwk(ctx) {
1435
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
1436
+ const { privateKey } = await parseServiceToken(ctx.auth.token);
1437
+ const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
1438
+ return JSON.stringify(pub);
1439
+ }
1440
+ const { user } = await ctx.client.me();
1441
+ if (!user.publicKeyJwk) throw new Error("this user has no keypair yet — run `seekrit keys setup` first");
1442
+ return user.publicKeyJwk;
1443
+ }
1444
+ /** Run a child command with `env` and capture its output (values never returned). */
1445
+ function runChild(cmd, args, env, cwd) {
1446
+ return new Promise((resolve, reject) => {
1447
+ const child = spawn(cmd, args, {
1448
+ env,
1449
+ cwd,
1450
+ stdio: [
1451
+ "ignore",
1452
+ "pipe",
1453
+ "pipe"
1454
+ ]
1455
+ });
1456
+ let stdout = "";
1457
+ let stderr = "";
1458
+ const cap = 64e3;
1459
+ child.stdout.on("data", (d) => {
1460
+ if (stdout.length < cap) stdout += d.toString("utf8");
1461
+ });
1462
+ child.stderr.on("data", (d) => {
1463
+ if (stderr.length < cap) stderr += d.toString("utf8");
1464
+ });
1465
+ child.on("error", reject);
1466
+ child.on("close", (code) => resolve({
1467
+ exitCode: code ?? 1,
1468
+ stdout,
1469
+ stderr
1470
+ }));
1471
+ });
1472
+ }
1473
+ /** Resolve the layered environment for run/export (token path infers the env). */
1474
+ async function materializeFor(ctx, o) {
1475
+ let envId;
1476
+ if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, o)).envId;
1477
+ return materializeEnv(ctx, {
1478
+ envId,
1479
+ with: o.with,
1480
+ envFiles: o.envFile ?? [".env"]
1481
+ });
1482
+ }
1483
+ const targetShape = {
1484
+ org: z.string().optional().describe("organization slug or id (defaults to a lone org)"),
1485
+ app: z.string().optional().describe("application slug or id (or set via configure_project)"),
1486
+ group: z.string().optional().describe("target a group environment instead of an app"),
1487
+ env: z.string().optional().describe("environment slug or id (a service token infers its own)")
1488
+ };
1489
+ /**
1490
+ * Resolve which environment a secret tool addresses. A service token with no
1491
+ * explicit app/group targets its own bound environment (no flags needed);
1492
+ * everyone else names app|group + env.
1493
+ */
1494
+ async function resolveTargetEnv(ctx, o) {
1495
+ if (isTokenAuth(ctx) && !o.app && !o.group) {
1496
+ const { scope } = await ctx.client.resolve();
1497
+ return {
1498
+ orgId: scope.orgId,
1499
+ envId: scope.envId,
1500
+ label: `${scope.appSlug}/${scope.envSlug}`
1501
+ };
1502
+ }
1503
+ return resolveEnvTarget(ctx, o);
1504
+ }
1505
+ async function runMcpServer(options = {}) {
1506
+ setFailThrows(true);
1507
+ const server = new McpServer({
1508
+ name: "seekrit",
1509
+ version: options.version ?? version$1
1510
+ });
1511
+ /** Register a tool whose handler returns data (serialized) or throws (→ isError). */
1512
+ const tool = (name, description, shape, handler) => {
1513
+ server.registerTool(name, {
1514
+ description,
1515
+ inputSchema: shape
1516
+ }, (async (args) => {
1517
+ try {
1518
+ return jsonText(await handler(args));
1519
+ } catch (err) {
1520
+ return errText(err);
1521
+ }
1522
+ }));
1523
+ };
1524
+ tool("whoami", "Show the authenticated identity, its role, and accessible orgs.", {}, async () => {
1525
+ const ctx = getCtx();
1526
+ if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
1527
+ const { tokenId } = await parseServiceToken(ctx.auth.token);
1528
+ const { orgs } = await ctx.client.listOrgs();
1529
+ let scope = null;
1530
+ try {
1531
+ scope = (await ctx.client.resolve()).scope;
1532
+ } catch {}
1533
+ return {
1534
+ kind: "service_token",
1535
+ tokenId,
1536
+ org: orgs[0]?.slug ?? null,
1537
+ boundScope: scope
1538
+ };
1539
+ }
1540
+ return {
1541
+ kind: "user",
1542
+ ...await ctx.client.me()
1543
+ };
1544
+ });
1545
+ tool("list_orgs", "List organizations the caller can access.", {}, async () => (await getCtx().client.listOrgs()).orgs);
1546
+ tool("list_apps", "List applications in an organization.", { org: z.string().optional() }, async ({ org }) => {
1547
+ const ctx = getCtx();
1548
+ const orgRef = await resolveOrg(ctx, org);
1549
+ return (await ctx.client.listApps(orgRef.id)).apps;
1550
+ });
1551
+ tool("list_envs", "List environments of an application.", {
1552
+ org: z.string().optional(),
1553
+ app: z.string()
1554
+ }, async ({ org, app }) => {
1555
+ const ctx = getCtx();
1556
+ const orgRef = await resolveOrg(ctx, org);
1557
+ const { apps } = await ctx.client.listApps(orgRef.id);
1558
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
1559
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
1560
+ return (await ctx.client.listEnvs(orgRef.id, appRow.id)).environments;
1561
+ });
1562
+ tool("list_groups", "List shared groups (reusable secret bags) in an organization.", { org: z.string().optional() }, async ({ org }) => {
1563
+ const ctx = getCtx();
1564
+ const orgRef = await resolveOrg(ctx, org);
1565
+ return (await ctx.client.listGroups(orgRef.id)).groups;
1566
+ });
1567
+ tool("list_group_envs", "List a group's environments (per-slug value sets).", {
1568
+ org: z.string().optional(),
1569
+ group: z.string()
1570
+ }, async ({ org, group }) => {
1571
+ const ctx = getCtx();
1572
+ const g = await resolveGroup(ctx, {
1573
+ org,
1574
+ group
1575
+ });
1576
+ return (await ctx.client.listGroupEnvs(g.orgId, g.id)).environments;
1577
+ });
1578
+ tool("list_env_groups", "List the groups composed into an application environment (precedence order).", {
1579
+ org: z.string().optional(),
1580
+ app: z.string(),
1581
+ env: z.string()
1582
+ }, async ({ org, app, env }) => {
1583
+ const ctx = getCtx();
1584
+ const target = await resolveAppEnv(ctx, {
1585
+ org,
1586
+ app,
1587
+ env
1588
+ });
1589
+ return (await ctx.client.listEnvGroups(target.orgId, target.envId)).groups;
1590
+ });
1591
+ tool("list_members", "List organization members and their public keys (for granting access).", { org: z.string().optional() }, async ({ org }) => {
1592
+ const ctx = getCtx();
1593
+ const orgRef = await resolveOrg(ctx, org);
1594
+ return (await ctx.client.listMembers(orgRef.id)).members;
1595
+ });
1596
+ tool("kms_list_keys", "List managed KMS keys the caller can see (metadata only).", { org: z.string().optional() }, async ({ org }) => {
1597
+ const ctx = getCtx();
1598
+ const orgRef = await resolveOrg(ctx, org);
1599
+ return (await ctx.client.listKmsKeys(orgRef.id)).keys;
1600
+ });
1601
+ tool("kms_create_key", "Create an org-scoped managed key. Material is generated locally and wrapped to each grantee (self plus any listed users/tokens); the server never sees it. Use the CLI for app/group-scoped keys.", {
1602
+ org: z.string().optional(),
1603
+ name: z.string(),
1604
+ purpose: z.enum(["encrypt", "sign"]),
1605
+ grantUsers: z.array(z.string()).optional(),
1606
+ grantTokens: z.array(z.string()).optional()
1607
+ }, async ({ org, name, purpose, grantUsers, grantTokens }) => {
1608
+ const ctx = getCtx();
1609
+ const orgRef = await resolveOrg(ctx, org);
1610
+ let toWrap;
1611
+ let publicKeyJwk;
1612
+ if (purpose === "encrypt") toWrap = generateEncryptKeyMaterial();
1613
+ else {
1614
+ const km = await generateSigningKeyMaterial();
1615
+ toWrap = km.privateKeyPkcs8;
1616
+ publicKeyJwk = km.publicKeyJwk;
1617
+ }
1618
+ const recipients = [await kmsCallerIdentity(ctx)];
1619
+ for (const u of grantUsers ?? []) recipients.push(await kmsResolveRecipient(ctx, orgRef.id, { user: u }));
1620
+ for (const t of grantTokens ?? []) recipients.push(await kmsResolveRecipient(ctx, orgRef.id, { token: t }));
1621
+ const seen = /* @__PURE__ */ new Set();
1622
+ const grants = [];
1623
+ for (const r of recipients) {
1624
+ const dedupeKey = `${r.principalType}:${r.principalId}`;
1625
+ if (seen.has(dedupeKey)) continue;
1626
+ seen.add(dedupeKey);
1627
+ grants.push({
1628
+ principalType: r.principalType,
1629
+ principalId: r.principalId,
1630
+ wrappedKey: await wrapDek(toWrap, r.publicKeyJwk)
1631
+ });
1632
+ }
1633
+ const { key } = await ctx.client.createKmsKey(orgRef.id, {
1634
+ name,
1635
+ purpose,
1636
+ spec: purpose === "sign" ? "ecdsa-p256" : "aes-256-gcm",
1637
+ ...publicKeyJwk ? { publicKeyJwk } : {},
1638
+ grants
1639
+ });
1640
+ return key;
1641
+ });
1642
+ tool("kms_grant", "Grant a principal (user email or token id) use of a key's current version.", {
1643
+ org: z.string().optional(),
1644
+ key: z.string(),
1645
+ user: z.string().optional(),
1646
+ token: z.string().optional()
1647
+ }, async ({ org, key, user, token }) => {
1648
+ const ctx = getCtx();
1649
+ ensureDecryptable(ctx);
1650
+ const orgRef = await resolveOrg(ctx, org);
1651
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1652
+ const recipient = await kmsResolveRecipient(ctx, orgRef.id, {
1653
+ user,
1654
+ token
1655
+ });
1656
+ const { material } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
1657
+ await ctx.client.grantKmsKey(orgRef.id, k.id, {
1658
+ principalType: recipient.principalType,
1659
+ principalId: recipient.principalId,
1660
+ wrappedKey: await wrapDek(material, recipient.publicKeyJwk)
1661
+ });
1662
+ return {
1663
+ granted: recipient.principalId,
1664
+ key: k.name
1665
+ };
1666
+ });
1667
+ tool("kms_encrypt", "Encrypt a value under a managed encrypt key; returns a ce1 ciphertext blob. `context` (if given) is bound as AAD and must be supplied identically to decrypt.", {
1668
+ org: z.string().optional(),
1669
+ key: z.string(),
1670
+ plaintext: z.string(),
1671
+ context: z.string().optional()
1672
+ }, async ({ org, key, plaintext, context }) => {
1673
+ const ctx = getCtx();
1674
+ ensureDecryptable(ctx);
1675
+ const orgRef = await resolveOrg(ctx, org);
1676
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1677
+ if (k.purpose !== "encrypt") throw new Error(`${k.name} is a ${k.purpose} key`);
1678
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
1679
+ return { ciphertext: await kmsEncrypt(material, {
1680
+ keyId: k.id,
1681
+ version: currentVersion
1682
+ }, plaintext, context ?? "") };
1683
+ });
1684
+ tool("kms_decrypt", "Decrypt a ce1 blob. Supply the same `context` used to encrypt.", {
1685
+ org: z.string().optional(),
1686
+ key: z.string(),
1687
+ ciphertext: z.string(),
1688
+ context: z.string().optional()
1689
+ }, async ({ org, key, ciphertext, context }) => {
1690
+ const ctx = getCtx();
1691
+ ensureDecryptable(ctx);
1692
+ const orgRef = await resolveOrg(ctx, org);
1693
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1694
+ const ref = kmsBlobKeyRef(ciphertext);
1695
+ const { material } = await kmsRecoverMaterial(ctx, orgRef.id, k.id, ref.version);
1696
+ return { plaintext: await kmsDecrypt(material, ciphertext, context ?? "") };
1697
+ });
1698
+ tool("kms_generate_data_key", "Generate a data key under a managed encrypt key (envelope encryption). Returns the plaintext key (base64) and its wrapped form to store.", {
1699
+ org: z.string().optional(),
1700
+ key: z.string()
1701
+ }, async ({ org, key }) => {
1702
+ const ctx = getCtx();
1703
+ ensureDecryptable(ctx);
1704
+ const orgRef = await resolveOrg(ctx, org);
1705
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1706
+ if (k.purpose !== "encrypt") throw new Error(`${k.name} is a ${k.purpose} key`);
1707
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
1708
+ const dk = await generateDataKey(material, {
1709
+ keyId: k.id,
1710
+ version: currentVersion
1711
+ });
1712
+ return {
1713
+ plaintextBase64: toBase64(dk.plaintext),
1714
+ wrapped: dk.wrapped
1715
+ };
1716
+ });
1717
+ tool("kms_sign", "Sign a message with a managed signing key; returns an sg1 signature blob.", {
1718
+ org: z.string().optional(),
1719
+ key: z.string(),
1720
+ message: z.string()
1721
+ }, async ({ org, key, message }) => {
1722
+ const ctx = getCtx();
1723
+ ensureDecryptable(ctx);
1724
+ const orgRef = await resolveOrg(ctx, org);
1725
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1726
+ if (k.purpose !== "sign") throw new Error(`${k.name} is a ${k.purpose} key`);
1727
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
1728
+ return { signature: await signMessage(await importSigningKey(material), {
1729
+ keyId: k.id,
1730
+ version: currentVersion
1731
+ }, message) };
1732
+ });
1733
+ tool("kms_verify", "Verify an sg1 signature over a message using a signing key's published public key (no grant needed).", {
1734
+ org: z.string().optional(),
1735
+ key: z.string(),
1736
+ signature: z.string(),
1737
+ message: z.string()
1738
+ }, async ({ org, key, signature, message }) => {
1739
+ const ctx = getCtx();
1740
+ const orgRef = await resolveOrg(ctx, org);
1741
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
1742
+ const ref = signatureKeyRef(signature);
1743
+ const { versions } = await ctx.client.getKmsPublicKeys(orgRef.id, k.id);
1744
+ const pub = versions.find((v) => v.version === ref.version)?.publicKeyJwk;
1745
+ if (!pub) throw new Error(`no published public key for version ${ref.version}`);
1746
+ return { valid: await verifyMessage(await importVerifyingKey(pub), signature, message) };
1747
+ });
1748
+ tool("list_secrets", "List secret names + versions in an environment (never values).", targetShape, async (o) => {
1749
+ const ctx = getCtx();
1750
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
1751
+ const { secrets } = await ctx.client.listSecrets(orgId, envId);
1752
+ return secrets.map((s) => ({
1753
+ name: s.name,
1754
+ version: s.version,
1755
+ updatedAt: s.updatedAt
1756
+ }));
1757
+ });
1758
+ tool("list_tokens", "List an organization's service tokens (never the secret token strings).", { org: z.string().optional() }, async ({ org }) => {
1759
+ const ctx = getCtx();
1760
+ const orgRef = await resolveOrg(ctx, org);
1761
+ return (await ctx.client.listTokens(orgRef.id)).tokens;
1762
+ });
1763
+ tool("audit", "Read the organization's audit trail (most recent first).", {
1764
+ org: z.string().optional(),
1765
+ limit: z.number().int().min(1).max(200).optional(),
1766
+ action: z.string().optional().describe("filter by action, e.g. secret.updated")
1767
+ }, async ({ org, limit, action }) => {
1768
+ const ctx = getCtx();
1769
+ const orgRef = await resolveOrg(ctx, org);
1770
+ return (await ctx.client.listAudit(orgRef.id, {
1771
+ limit: limit ?? 50,
1772
+ action
1773
+ })).entries;
1774
+ });
1775
+ tool("create_org", "Create an organization. Requires a user session — service tokens cannot own a Stytch org.", {
1776
+ name: z.string(),
1777
+ slug: z.string()
1778
+ }, async ({ name, slug }) => (await getCtx().client.createOrg({
1779
+ name,
1780
+ slug
1781
+ })).org);
1782
+ tool("create_app", "Create an application in an organization.", {
1783
+ org: z.string().optional(),
1784
+ name: z.string(),
1785
+ slug: z.string()
1786
+ }, async ({ org, name, slug }) => {
1787
+ const ctx = getCtx();
1788
+ const orgRef = await resolveOrg(ctx, org);
1789
+ return (await ctx.client.createApp(orgRef.id, {
1790
+ name,
1791
+ slug
1792
+ })).app;
1793
+ });
1794
+ tool("create_group", "Create a shared group (reusable secret bag) in an organization.", {
1795
+ org: z.string().optional(),
1796
+ name: z.string(),
1797
+ slug: z.string()
1798
+ }, async ({ org, name, slug }) => {
1799
+ const ctx = getCtx();
1800
+ const orgRef = await resolveOrg(ctx, org);
1801
+ return (await ctx.client.createGroup(orgRef.id, {
1802
+ name,
1803
+ slug
1804
+ })).group;
1805
+ });
1806
+ tool("create_env", "Create an application environment. Generates the data key locally and grants it to the caller.", {
1807
+ org: z.string().optional(),
1808
+ app: z.string(),
1809
+ name: z.string(),
1810
+ slug: z.string()
1811
+ }, async ({ org, app, name, slug }) => {
1812
+ const ctx = getCtx();
1813
+ const orgRef = await resolveOrg(ctx, org);
1814
+ const { apps } = await ctx.client.listApps(orgRef.id);
1815
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
1816
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
1817
+ const wrappedDek = await wrapDek(generateDek(), await principalPublicKeyJwk(ctx));
1818
+ return (await ctx.client.createEnv(orgRef.id, appRow.id, {
1819
+ name,
1820
+ slug,
1821
+ wrappedDek
1822
+ })).environment;
1823
+ });
1824
+ tool("create_group_env", "Create a group environment. Generates the data key locally and grants it to the caller.", {
1825
+ org: z.string().optional(),
1826
+ group: z.string(),
1827
+ name: z.string(),
1828
+ slug: z.string()
1829
+ }, async ({ org, group, name, slug }) => {
1830
+ const ctx = getCtx();
1831
+ const g = await resolveGroup(ctx, {
1832
+ org,
1833
+ group
1834
+ });
1835
+ const wrappedDek = await wrapDek(generateDek(), await principalPublicKeyJwk(ctx));
1836
+ return (await ctx.client.createGroupEnv(g.orgId, g.id, {
1837
+ name,
1838
+ slug,
1839
+ wrappedDek
1840
+ })).environment;
1841
+ });
1842
+ tool("compose_group", "Compose a group into an application environment (higher position wins on name clashes).", {
1843
+ org: z.string().optional(),
1844
+ app: z.string(),
1845
+ env: z.string(),
1846
+ group: z.string(),
1847
+ position: z.number().int().min(0).optional()
1848
+ }, async ({ org, app, env, group, position }) => {
1849
+ const ctx = getCtx();
1850
+ const target = await resolveAppEnv(ctx, {
1851
+ org,
1852
+ app,
1853
+ env
1854
+ });
1855
+ const g = await resolveGroup(ctx, {
1856
+ org,
1857
+ group
1858
+ });
1859
+ return (await ctx.client.linkEnvGroup(target.orgId, target.envId, {
1860
+ groupId: g.id,
1861
+ position
1862
+ })).group;
1863
+ });
1864
+ tool("uncompose_group", "Remove a composed group from an application environment.", {
1865
+ org: z.string().optional(),
1866
+ app: z.string(),
1867
+ env: z.string(),
1868
+ group: z.string()
1869
+ }, async ({ org, app, env, group }) => {
1870
+ const ctx = getCtx();
1871
+ const target = await resolveAppEnv(ctx, {
1872
+ org,
1873
+ app,
1874
+ env
1875
+ });
1876
+ const g = await resolveGroup(ctx, {
1877
+ org,
1878
+ group
1879
+ });
1880
+ await ctx.client.unlinkEnvGroup(target.orgId, target.envId, g.id);
1881
+ return { ok: true };
1882
+ });
1883
+ tool("set_secret", "Encrypt a value locally and store it in an environment.", {
1884
+ ...targetShape,
1885
+ name: z.string(),
1886
+ value: z.string()
1887
+ }, async (o) => {
1888
+ const ctx = getCtx();
1889
+ ensureDecryptable(ctx);
1890
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
1891
+ await encryptAndSetSecret(ctx, orgId, envId, o.name, o.value);
1892
+ return {
1893
+ ok: true,
1894
+ name: o.name
1895
+ };
1896
+ });
1897
+ tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command).", {
1898
+ ...targetShape,
1899
+ name: z.string(),
1900
+ reveal: z.boolean().optional()
1901
+ }, async (o) => {
1902
+ const ctx = getCtx();
1903
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
1904
+ if (!o.reveal) {
1905
+ const { secrets } = await ctx.client.listSecrets(orgId, envId);
1906
+ const row = secrets.find((s) => s.name === o.name);
1907
+ if (!row) throw new Error(`no secret named ${o.name}`);
1908
+ return {
1909
+ name: row.name,
1910
+ version: row.version,
1911
+ revealed: false
1912
+ };
1913
+ }
1914
+ ensureDecryptable(ctx);
1915
+ const values = await fetchDecryptedSecrets(ctx, orgId, envId);
1916
+ if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
1917
+ return {
1918
+ name: o.name,
1919
+ value: values[o.name],
1920
+ revealed: true
1921
+ };
1922
+ });
1923
+ tool("delete_secret", "Delete a secret from an environment.", {
1924
+ ...targetShape,
1925
+ name: z.string()
1926
+ }, async (o) => {
1927
+ const ctx = getCtx();
1928
+ const { orgId, envId } = await resolveTargetEnv(ctx, o);
1929
+ await ctx.client.deleteSecret(orgId, envId, o.name);
1930
+ return {
1931
+ ok: true,
1932
+ name: o.name
1933
+ };
1934
+ });
1935
+ tool("run_command", "Run a command with the resolved secrets injected as environment variables, and return its exit code + captured output. Secret VALUES are never returned — this is the preferred way to use secrets. process env > .env > app env > groups.", {
1936
+ command: z.string().describe("executable to run"),
1937
+ args: z.array(z.string()).optional(),
1938
+ org: z.string().optional(),
1939
+ app: z.string().optional(),
1940
+ env: z.string().optional().describe("environment slug (token auth infers this)"),
1941
+ with: z.record(z.string(), z.string()).optional().describe("group=env slice overrides"),
1942
+ envFile: z.array(z.string()).optional().describe(".env files to overlay (default [.env])"),
1943
+ cwd: z.string().optional()
1944
+ }, async (o) => {
1945
+ const ctx = getCtx();
1946
+ ensureDecryptable(ctx);
1947
+ const { values } = await materializeFor(ctx, o);
1948
+ const childEnv = {
1949
+ ...values,
1950
+ ...process.env
1951
+ };
1952
+ return {
1953
+ ...await runChild(o.command, o.args ?? [], childEnv, o.cwd),
1954
+ injectedVarCount: Object.keys(values).length
1955
+ };
1956
+ });
1957
+ tool("export_env", "Write the resolved secrets to a dotenv file on disk and return the variable names written (never the values). Use to materialize a .env for local tooling.", {
1958
+ file: z.string().describe("path to write, e.g. .env"),
1959
+ org: z.string().optional(),
1960
+ app: z.string().optional(),
1961
+ env: z.string().optional(),
1962
+ with: z.record(z.string(), z.string()).optional()
1963
+ }, async (o) => {
1964
+ const ctx = getCtx();
1965
+ ensureDecryptable(ctx);
1966
+ const { values } = await materializeFor(ctx, o);
1967
+ const { writeFileSync } = await import("node:fs");
1968
+ const body = Object.entries(values).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("\n");
1969
+ writeFileSync(o.file, `${body}\n`, { mode: 384 });
1970
+ return {
1971
+ file: o.file,
1972
+ names: Object.keys(values).sort()
1973
+ };
1974
+ });
1975
+ tool("create_token", "Mint a service token, printed once. Runtime tokens bind to one app environment (auto-granted its keys, so a command/agent can decrypt it). Pass admin:true for an org-scoped provisioning token (create apps/groups/envs, grant, mint tokens) — admin tokens need no env binding.", {
1976
+ name: z.string().describe("display name, e.g. ci-deploy or agent-session"),
1977
+ org: z.string().optional(),
1978
+ app: z.string().optional().describe("bind to this app (runtime tokens)"),
1979
+ env: z.string().optional().describe("bind to this env (runtime tokens)"),
1980
+ admin: z.boolean().optional().describe("mint an org-scoped admin token")
1981
+ }, async ({ name, org, app, env, admin }) => {
1982
+ const ctx = getCtx();
1983
+ const role = admin ? "admin" : "member";
1984
+ const boundToEnv = Boolean(app || env);
1985
+ if (!admin && !boundToEnv) throw new Error("runtime tokens need app + env, or pass admin:true for an org-scoped token");
1986
+ if (boundToEnv && !(app && env)) throw new Error("pass both app and env to bind a token");
1987
+ const target = boundToEnv ? await resolveAppEnv(ctx, {
1988
+ org,
1989
+ app,
1990
+ env
1991
+ }) : { orgId: (await resolveOrg(ctx, org)).id };
1992
+ const created = await createServiceToken();
1993
+ await ctx.client.createToken(target.orgId, {
1994
+ name,
1995
+ tokenId: created.tokenId,
1996
+ tokenHash: created.tokenHash,
1997
+ publicKeyJwk: created.publicKeyJwk,
1998
+ role,
1999
+ environmentId: "envId" in target ? target.envId : null
2000
+ });
2001
+ if (boundToEnv && "envId" in target) {
2002
+ ensureDecryptable(ctx);
2003
+ const grantEnv = async (envId) => {
2004
+ const dek = await getDek(ctx, target.orgId, envId);
2005
+ await ctx.client.grantEnvKey(target.orgId, envId, {
2006
+ principalType: "service_token",
2007
+ principalId: created.tokenId,
2008
+ wrappedDek: await wrapDek(dek, created.publicKeyJwk)
2009
+ });
2010
+ };
2011
+ await grantEnv(target.envId);
2012
+ const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
2013
+ for (const g of groups) {
2014
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
2015
+ const slice = environments.find((e) => e.slug === target.envSlug);
2016
+ if (slice) await grantEnv(slice.id);
2017
+ }
2018
+ }
2019
+ return {
2020
+ token: created.token,
2021
+ tokenId: created.tokenId,
2022
+ role,
2023
+ note: "save this now — the secret token string is not stored and cannot be retrieved"
2024
+ };
2025
+ });
2026
+ tool("revoke_token", "Revoke a service token by id.", {
2027
+ org: z.string().optional(),
2028
+ tokenId: z.string()
2029
+ }, async ({ org, tokenId }) => {
2030
+ const ctx = getCtx();
2031
+ const orgRef = await resolveOrg(ctx, org);
2032
+ await ctx.client.revokeToken(orgRef.id, tokenId);
2033
+ return {
2034
+ ok: true,
2035
+ tokenId
2036
+ };
2037
+ });
2038
+ tool("grant_env", "Grant a member (by email) or service token (by id) access to an environment's data key. Re-wraps the DEK to the grantee — the caller must already hold the key.", {
2039
+ ...targetShape,
2040
+ user: z.string().optional().describe("org member email"),
2041
+ token: z.string().optional().describe("service token id (skt_…)")
2042
+ }, async (o) => {
2043
+ if (Boolean(o.user) === Boolean(o.token)) throw new Error("pass exactly one of user or token");
2044
+ const ctx = getCtx();
2045
+ ensureDecryptable(ctx);
2046
+ const { orgId, envId, label } = await resolveTargetEnv(ctx, o);
2047
+ const dek = await getDek(ctx, orgId, envId);
2048
+ let principalType;
2049
+ let principalId;
2050
+ let publicKeyJwk;
2051
+ if (o.user) {
2052
+ const { members } = await ctx.client.listMembers(orgId);
2053
+ const member = members.find((m) => m.email === o.user);
2054
+ if (!member) throw new Error(`no member ${o.user}`);
2055
+ if (!member.publicKeyJwk) throw new Error(`${o.user} has not completed key setup`);
2056
+ [principalType, principalId, publicKeyJwk] = [
2057
+ "user",
2058
+ member.userId,
2059
+ member.publicKeyJwk
2060
+ ];
2061
+ } else {
2062
+ const { tokens } = await ctx.client.listTokens(orgId);
2063
+ const t = tokens.find((x) => x.id === o.token);
2064
+ if (!t) throw new Error(`no service token ${o.token}`);
2065
+ [principalType, principalId, publicKeyJwk] = [
2066
+ "service_token",
2067
+ t.id,
2068
+ t.publicKeyJwk
2069
+ ];
2070
+ }
2071
+ await ctx.client.grantEnvKey(orgId, envId, {
2072
+ principalType,
2073
+ principalId,
2074
+ wrappedDek: await wrapDek(dek, publicKeyJwk)
2075
+ });
2076
+ return {
2077
+ ok: true,
2078
+ env: label,
2079
+ principalType,
2080
+ principalId
2081
+ };
2082
+ });
2083
+ tool("list_pg_targets", "List registered Postgres provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
2084
+ const ctx = getCtx();
2085
+ const orgRef = await resolveOrg(ctx, org);
2086
+ return (await ctx.client.listLeaseTargets(orgRef.id)).targets;
2087
+ });
2088
+ tool("create_pg_lease", "Mint a short-lived Postgres credential against a target and return a ready-to-use connection URL. The password is generated on THIS machine and only its SCRAM verifier is sent to the API — the plaintext never reaches seekrit or Postgres at rest. The role auto-expires; revoke early with revoke_pg_lease.", {
2089
+ org: z.string().optional(),
2090
+ target: z.string().describe("target id or name"),
2091
+ role: z.string().optional().describe("role name to create (default: random tmp_ name)"),
2092
+ ttlSeconds: z.number().int().min(60).max(604800).optional().describe("lifetime (default 3600)")
2093
+ }, async ({ org, target, role, ttlSeconds }) => {
2094
+ const ctx = getCtx();
2095
+ const orgRef = await resolveOrg(ctx, org);
2096
+ const { targets } = await ctx.client.listLeaseTargets(orgRef.id);
2097
+ const t = targets.find((x) => x.id === target || x.name === target);
2098
+ if (!t) throw new Error(`no target "${target}" in ${orgRef.slug}`);
2099
+ const roleName = role ?? `tmp_${randomLower(12)}`;
2100
+ const { password, verifier } = await generatePostgresCredential();
2101
+ const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
2102
+ provider: "postgres",
2103
+ targetId: t.id,
2104
+ roleName,
2105
+ verifier,
2106
+ ttlSeconds: ttlSeconds ?? 3600
2107
+ });
2108
+ const url = `postgres://${roleName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
2109
+ return {
2110
+ leaseId: lease.id,
2111
+ url,
2112
+ username: roleName,
2113
+ expiresAt: connection.expiresAt,
2114
+ note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
2115
+ };
2116
+ });
2117
+ tool("list_pg_leases", "List Postgres leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
2118
+ const ctx = getCtx();
2119
+ const orgRef = await resolveOrg(ctx, org);
2120
+ return (await ctx.client.listLeases(orgRef.id)).leases;
2121
+ });
2122
+ tool("revoke_pg_lease", "Revoke a Postgres lease now (drops the role immediately).", {
2123
+ org: z.string().optional(),
2124
+ leaseId: z.string()
2125
+ }, async ({ org, leaseId }) => {
2126
+ const ctx = getCtx();
2127
+ const orgRef = await resolveOrg(ctx, org);
2128
+ await ctx.client.revokeLease(orgRef.id, leaseId);
2129
+ return {
2130
+ ok: true,
2131
+ leaseId
2132
+ };
2133
+ });
2134
+ tool("list_mysql_targets", "List registered MySQL/MariaDB provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
2135
+ const ctx = getCtx();
2136
+ const orgRef = await resolveOrg(ctx, org);
2137
+ return (await ctx.client.listLeaseTargets(orgRef.id)).targets.filter((t) => t.provider === "mysql");
2138
+ });
2139
+ 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.", {
2140
+ org: z.string().optional(),
2141
+ target: z.string().describe("target id or name"),
2142
+ user: z.string().optional().describe("user name to create (default: random tmp_ name)"),
2143
+ ttlSeconds: z.number().int().min(60).max(604800).optional().describe("lifetime (default 3600)")
2144
+ }, async ({ org, target, user, ttlSeconds }) => {
2145
+ const ctx = getCtx();
2146
+ const orgRef = await resolveOrg(ctx, org);
2147
+ const { targets } = await ctx.client.listLeaseTargets(orgRef.id);
2148
+ const t = targets.find((x) => x.id === target || x.name === target);
2149
+ if (!t) throw new Error(`no target "${target}" in ${orgRef.slug}`);
2150
+ if (t.provider !== "mysql") throw new Error(`target "${target}" is not a MySQL target`);
2151
+ const userName = user ?? `tmp_${randomLower(12)}`;
2152
+ const { password, verifier } = await generateMysqlCredential();
2153
+ const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
2154
+ provider: "mysql",
2155
+ targetId: t.id,
2156
+ roleName: userName,
2157
+ verifier,
2158
+ ttlSeconds: ttlSeconds ?? 3600
2159
+ });
2160
+ const url = `mysql://${userName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
2161
+ return {
2162
+ leaseId: lease.id,
2163
+ url,
2164
+ username: userName,
2165
+ expiresAt: connection.expiresAt,
2166
+ note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
2167
+ };
2168
+ });
2169
+ tool("list_mysql_leases", "List MySQL/MariaDB leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
2170
+ const ctx = getCtx();
2171
+ const orgRef = await resolveOrg(ctx, org);
2172
+ return (await ctx.client.listLeases(orgRef.id)).leases.filter((l) => l.provider === "mysql");
2173
+ });
2174
+ tool("revoke_mysql_lease", "Revoke a MySQL/MariaDB lease now (drops the user immediately).", {
2175
+ org: z.string().optional(),
2176
+ leaseId: z.string()
2177
+ }, async ({ org, leaseId }) => {
2178
+ const ctx = getCtx();
2179
+ const orgRef = await resolveOrg(ctx, org);
2180
+ await ctx.client.revokeLease(orgRef.id, leaseId);
2181
+ return {
2182
+ ok: true,
2183
+ leaseId
2184
+ };
2185
+ });
2186
+ 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.", {
2187
+ org: z.string(),
2188
+ app: z.string(),
2189
+ dir: z.string().optional()
2190
+ }, async ({ org, app, dir }) => {
2191
+ const ctx = getCtx();
2192
+ const orgRef = await resolveOrg(ctx, org);
2193
+ const { apps } = await ctx.client.listApps(orgRef.id);
2194
+ const appRow = apps.find((a) => a.slug === app || a.id === app);
2195
+ if (!appRow) throw new Error(`no app "${app}" in ${orgRef.slug}`);
2196
+ return {
2197
+ path: writeProjectConfig({
2198
+ org: orgRef.slug,
2199
+ app: appRow.slug
2200
+ }, dir),
2201
+ guidance: "Run commands with `seekrit run -- <cmd>` (or the seekrit-run binary) and a SEEKRIT_TOKEN bound to the desired environment; the token selects org+app+env."
2202
+ };
2203
+ });
2204
+ await server.connect(new StdioServerTransport());
2205
+ }
2206
+ //#endregion
2207
+ //#region src/index.ts
2208
+ /**
2209
+ * `@seekrit/mcp` — a standalone, `npx`-able entrypoint for seekrit's MCP server.
2210
+ *
2211
+ * This is a thin wrapper: the server itself (all 27 tools, the crypto plane of
2212
+ * the two-server design) lives in `@seekrit/cli` and is shared with the
2213
+ * `seekrit mcp` subcommand — this package just publishes it as its own binary so
2214
+ * an agent can run it with zero prior install:
2215
+ *
2216
+ * npx -y @seekrit/mcp
2217
+ *
2218
+ * Everything else — auth resolution (`SEEKRIT_TOKEN` / `SEEKRIT_API_URL` /
2219
+ * `SEEKRIT_DEV_USER` or `~/.config/seekrit/config.json`), the zero-knowledge
2220
+ * invariant (decryption only ever happens here, on the client) — is identical to
2221
+ * `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
2222
+ * published package is self-contained and needs no `@seekrit/cli` install.
2223
+ */
2224
+ runMcpServer({ version: "0.1.0" }).catch((err) => {
2225
+ const message = err instanceof Error ? err.message : String(err);
2226
+ process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
2227
+ process.exit(1);
2228
+ });
2229
+ //#endregion
2230
+ export {};