@seekrit/cli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +497 -91
- package/dist/{mcp-hxTidFyj.js → mcp-Dw3hVhbC.js} +59 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { dirname, join, parse } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { Writable } from "node:stream";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
//#region ../../packages/crypto/src/encoding.ts
|
|
10
11
|
const CHUNK = 32768;
|
|
11
12
|
/** Base64url (no padding) — portable across browsers, Workers, and Node. */
|
|
@@ -21,6 +22,16 @@ function fromBase64Url(text) {
|
|
|
21
22
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
22
23
|
return bytes;
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Standard base64 (with `+`, `/`, and `=` padding). Most seekrit blobs use
|
|
27
|
+
* base64url, but some external wire formats mandate standard base64 — notably
|
|
28
|
+
* PostgreSQL SCRAM-SHA-256 verifier strings (see scram.ts).
|
|
29
|
+
*/
|
|
30
|
+
function toBase64(bytes) {
|
|
31
|
+
let binary = "";
|
|
32
|
+
for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
33
|
+
return btoa(binary);
|
|
34
|
+
}
|
|
24
35
|
function utf8Encode(text) {
|
|
25
36
|
return new TextEncoder().encode(text);
|
|
26
37
|
}
|
|
@@ -188,6 +199,65 @@ async function decryptPrivateKey(passphrase, blob) {
|
|
|
188
199
|
throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
|
|
189
200
|
}
|
|
190
201
|
}
|
|
202
|
+
const SALT_LENGTH = 16;
|
|
203
|
+
const DEFAULT_PASSWORD_LENGTH = 32;
|
|
204
|
+
const PASSWORD_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
205
|
+
async function hmacSha256(key, message) {
|
|
206
|
+
const k = await crypto.subtle.importKey("raw", key, {
|
|
207
|
+
name: "HMAC",
|
|
208
|
+
hash: "SHA-256"
|
|
209
|
+
}, false, ["sign"]);
|
|
210
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", k, message));
|
|
211
|
+
}
|
|
212
|
+
async function sha256(data) {
|
|
213
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
214
|
+
}
|
|
215
|
+
async function saltPassword(password, salt, iterations) {
|
|
216
|
+
const material = await crypto.subtle.importKey("raw", utf8Encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
217
|
+
const bits = await crypto.subtle.deriveBits({
|
|
218
|
+
name: "PBKDF2",
|
|
219
|
+
hash: "SHA-256",
|
|
220
|
+
salt,
|
|
221
|
+
iterations
|
|
222
|
+
}, material, 256);
|
|
223
|
+
return new Uint8Array(bits);
|
|
224
|
+
}
|
|
225
|
+
function randomPassword(length) {
|
|
226
|
+
let out = "";
|
|
227
|
+
while (out.length < length) {
|
|
228
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
229
|
+
for (const byte of bytes) {
|
|
230
|
+
if (byte < 248) out += PASSWORD_ALPHABET[byte % 62];
|
|
231
|
+
if (out.length === length) break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Compute the `SCRAM-SHA-256$<i>:<salt>$<StoredKey>:<ServerKey>` verifier for a
|
|
238
|
+
* known password. Pass the result straight to `CREATE ROLE … PASSWORD`.
|
|
239
|
+
*/
|
|
240
|
+
async function scramSha256Verifier(password, options = {}) {
|
|
241
|
+
const salt = options.salt ?? crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
242
|
+
const iterations = options.iterations ?? 4096;
|
|
243
|
+
const saltedPassword = await saltPassword(password, salt, iterations);
|
|
244
|
+
const storedKey = await sha256(await hmacSha256(saltedPassword, utf8Encode("Client Key")));
|
|
245
|
+
const serverKey = await hmacSha256(saltedPassword, utf8Encode("Server Key"));
|
|
246
|
+
return `SCRAM-SHA-256$${iterations}:${toBase64(salt)}$${toBase64(storedKey)}:${toBase64(serverKey)}`;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Mint a fresh random password and its SCRAM verifier in one step — the
|
|
250
|
+
* client-side half of a Vault-style dynamic Postgres credential.
|
|
251
|
+
*/
|
|
252
|
+
async function generatePostgresCredential(options = {}) {
|
|
253
|
+
const password = randomPassword(options.length ?? DEFAULT_PASSWORD_LENGTH);
|
|
254
|
+
const iterations = options.iterations ?? 4096;
|
|
255
|
+
return {
|
|
256
|
+
password,
|
|
257
|
+
verifier: await scramSha256Verifier(password, { iterations }),
|
|
258
|
+
iterations
|
|
259
|
+
};
|
|
260
|
+
}
|
|
191
261
|
//#endregion
|
|
192
262
|
//#region ../../packages/crypto/src/token.ts
|
|
193
263
|
/**
|
|
@@ -315,7 +385,7 @@ async function unwrapDek(wrapped, privateKey) {
|
|
|
315
385
|
}
|
|
316
386
|
//#endregion
|
|
317
387
|
//#region package.json
|
|
318
|
-
var version = "0.
|
|
388
|
+
var version = "0.6.0";
|
|
319
389
|
const PROJECT_FILE = "seekrit.json";
|
|
320
390
|
function globalConfigPath() {
|
|
321
391
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
@@ -506,6 +576,29 @@ var SeekritClient = class {
|
|
|
506
576
|
revokeToken(orgId, tokenId) {
|
|
507
577
|
return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
|
|
508
578
|
}
|
|
579
|
+
/** The broker's public key — wrap the admin credential to it before registering a target. */
|
|
580
|
+
getLeaseBrokerKey(orgId) {
|
|
581
|
+
return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
|
|
582
|
+
}
|
|
583
|
+
listLeaseTargets(orgId) {
|
|
584
|
+
return this.request("GET", `/v1/orgs/${orgId}/leases/targets`);
|
|
585
|
+
}
|
|
586
|
+
registerLeaseTarget(orgId, input) {
|
|
587
|
+
return this.request("POST", `/v1/orgs/${orgId}/leases/targets`, input);
|
|
588
|
+
}
|
|
589
|
+
deleteLeaseTarget(orgId, targetId) {
|
|
590
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/leases/targets/${targetId}`);
|
|
591
|
+
}
|
|
592
|
+
listLeases(orgId) {
|
|
593
|
+
return this.request("GET", `/v1/orgs/${orgId}/leases`);
|
|
594
|
+
}
|
|
595
|
+
/** Mint a temporary credential. Send only the client-computed verifier. */
|
|
596
|
+
mintLease(orgId, input) {
|
|
597
|
+
return this.request("POST", `/v1/orgs/${orgId}/leases`, input);
|
|
598
|
+
}
|
|
599
|
+
revokeLease(orgId, leaseId) {
|
|
600
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
|
|
601
|
+
}
|
|
509
602
|
listAudit(orgId, query = {}) {
|
|
510
603
|
const params = new URLSearchParams();
|
|
511
604
|
if (query.cursor) params.set("cursor", query.cursor);
|
|
@@ -640,6 +733,406 @@ function formatSecrets(values, format) {
|
|
|
640
733
|
case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
|
|
641
734
|
}
|
|
642
735
|
}
|
|
736
|
+
z.enum(["postgres"]);
|
|
737
|
+
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
738
|
+
/**
|
|
739
|
+
* A Postgres role name we are willing to create. Deliberately strict — this
|
|
740
|
+
* value is interpolated into a SQL template, so it must be a bare identifier
|
|
741
|
+
* with no way to break out of quoting (no quotes, whitespace, or semicolons).
|
|
742
|
+
*/
|
|
743
|
+
const postgresRoleNameSchema = z.string().regex(/^[a-z_][a-z0-9_]{2,62}$/, "must be 3–63 chars, lowercase letters/digits/underscore, starting with a letter or underscore");
|
|
744
|
+
/**
|
|
745
|
+
* A SCRAM-SHA-256 verifier string as produced by @seekrit/crypto. Validated so
|
|
746
|
+
* it, too, is safe to interpolate into a quoted SQL literal (the alphabet is
|
|
747
|
+
* base64 + the fixed structural characters, none of which is a single quote).
|
|
748
|
+
*/
|
|
749
|
+
const scramVerifierSchema = z.string().regex(/^SCRAM-SHA-256\$\d{3,}:[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+$/, "must be a SCRAM-SHA-256 verifier");
|
|
750
|
+
const postgresAccessLevelSchema = z.enum([
|
|
751
|
+
"readonly",
|
|
752
|
+
"readwrite",
|
|
753
|
+
"custom"
|
|
754
|
+
]);
|
|
755
|
+
/** The group role each preset's leased credentials inherit. */
|
|
756
|
+
const POSTGRES_GROUP_ROLES = {
|
|
757
|
+
readonly: "seekrit_readonly",
|
|
758
|
+
readwrite: "seekrit_readwrite"
|
|
759
|
+
};
|
|
760
|
+
const connectionSchema = z.object({
|
|
761
|
+
host: z.string().min(1),
|
|
762
|
+
port: z.number().int().min(1).max(65535),
|
|
763
|
+
database: z.string().min(1)
|
|
764
|
+
});
|
|
765
|
+
/** A `{{name}}`/`{{verifier}}`/`{{valid_until}}` templated SQL statement. */
|
|
766
|
+
const statementSchema = z.string().min(1).max(4e3);
|
|
767
|
+
/** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
|
|
768
|
+
const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
|
|
769
|
+
const leaseTargetConfigSchema = z.object({
|
|
770
|
+
provider: z.literal("postgres"),
|
|
771
|
+
executor: executorModeSchema,
|
|
772
|
+
accessLevel: postgresAccessLevelSchema.optional(),
|
|
773
|
+
schema: identifierSchema.optional(),
|
|
774
|
+
connection: connectionSchema,
|
|
775
|
+
provisionerUrl: z.url().optional(),
|
|
776
|
+
createStatements: z.array(statementSchema).max(16).optional(),
|
|
777
|
+
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
778
|
+
});
|
|
779
|
+
z.object({
|
|
780
|
+
name: z.string().trim().min(1).max(128),
|
|
781
|
+
config: leaseTargetConfigSchema,
|
|
782
|
+
/**
|
|
783
|
+
* The admin/provisioning credential (e.g. a Postgres connection string),
|
|
784
|
+
* encrypted client-side to the broker's public key (a `wd1.` wrap). The
|
|
785
|
+
* control plane stores only this ciphertext — it never sees the plaintext.
|
|
786
|
+
*/
|
|
787
|
+
wrappedAdminSecret: z.string().min(1)
|
|
788
|
+
});
|
|
789
|
+
z.object({
|
|
790
|
+
targetId: z.string().min(1),
|
|
791
|
+
roleName: postgresRoleNameSchema,
|
|
792
|
+
verifier: scramVerifierSchema,
|
|
793
|
+
ttlSeconds: z.number().int().min(60).max(3600 * 24 * 7)
|
|
794
|
+
});
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region ../../packages/core/src/providers/postgres.ts
|
|
797
|
+
/**
|
|
798
|
+
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
799
|
+
* read-only / read-write target's leased credentials inherit. Idempotent (safe
|
|
800
|
+
* to re-run). Returns null for custom targets (the admin owns their own SQL).
|
|
801
|
+
*
|
|
802
|
+
* Identifiers are interpolated from admin-supplied config (database/schema) and
|
|
803
|
+
* fixed group-role constants — this SQL is displayed for the admin to run in
|
|
804
|
+
* their own database, not executed by seekrit.
|
|
805
|
+
*/
|
|
806
|
+
function postgresGroupBootstrapSql(config) {
|
|
807
|
+
if (config.accessLevel !== "readonly" && config.accessLevel !== "readwrite") return null;
|
|
808
|
+
const group = POSTGRES_GROUP_ROLES[config.accessLevel];
|
|
809
|
+
const schema = config.schema ?? "public";
|
|
810
|
+
const db = config.connection.database;
|
|
811
|
+
const privileges = config.accessLevel === "readonly" ? "SELECT" : "SELECT, INSERT, UPDATE, DELETE";
|
|
812
|
+
const lines = [
|
|
813
|
+
`-- Run once as an admin on "${db}". Temporary ${config.accessLevel} credentials inherit this role.`,
|
|
814
|
+
"DO $$ BEGIN",
|
|
815
|
+
` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${group}') THEN`,
|
|
816
|
+
` CREATE ROLE ${group} NOLOGIN;`,
|
|
817
|
+
" END IF;",
|
|
818
|
+
"END $$;",
|
|
819
|
+
`GRANT CONNECT ON DATABASE "${db}" TO ${group};`,
|
|
820
|
+
`GRANT USAGE ON SCHEMA "${schema}" TO ${group};`,
|
|
821
|
+
`GRANT ${privileges} ON ALL TABLES IN SCHEMA "${schema}" TO ${group};`,
|
|
822
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT ${privileges} ON TABLES TO ${group};`
|
|
823
|
+
];
|
|
824
|
+
if (config.accessLevel === "readwrite") lines.push(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO ${group};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT USAGE, SELECT ON SEQUENCES TO ${group};`);
|
|
825
|
+
return lines.join("\n");
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region ../../packages/core/src/schemas.ts
|
|
829
|
+
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
830
|
+
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
831
|
+
const nameSchema = z.string().trim().min(1).max(128);
|
|
832
|
+
z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
|
|
833
|
+
z.enum([
|
|
834
|
+
"owner",
|
|
835
|
+
"admin",
|
|
836
|
+
"member"
|
|
837
|
+
]);
|
|
838
|
+
const principalTypeSchema = z.enum(["user", "service_token"]);
|
|
839
|
+
/** Org-level capability a service token can hold (never `owner`). */
|
|
840
|
+
const serviceTokenRoleSchema = z.enum(["admin", "member"]);
|
|
841
|
+
z.object({
|
|
842
|
+
name: nameSchema,
|
|
843
|
+
slug: slugSchema
|
|
844
|
+
});
|
|
845
|
+
z.object({
|
|
846
|
+
name: nameSchema,
|
|
847
|
+
slug: slugSchema
|
|
848
|
+
});
|
|
849
|
+
z.object({
|
|
850
|
+
name: nameSchema,
|
|
851
|
+
slug: slugSchema
|
|
852
|
+
});
|
|
853
|
+
z.object({
|
|
854
|
+
groupId: z.string().min(1),
|
|
855
|
+
/** Precedence among an env's groups (higher wins). Appended if omitted. */
|
|
856
|
+
position: z.number().int().min(0).optional()
|
|
857
|
+
});
|
|
858
|
+
z.object({
|
|
859
|
+
name: nameSchema,
|
|
860
|
+
slug: slugSchema,
|
|
861
|
+
/** Environment DEK wrapped to the creator's public key — created client-side. */
|
|
862
|
+
wrappedDek: z.string().min(1)
|
|
863
|
+
});
|
|
864
|
+
z.object({
|
|
865
|
+
/** Opaque versioned ciphertext blob from @seekrit/crypto. */
|
|
866
|
+
ciphertext: z.string().min(1).max(65536) });
|
|
867
|
+
z.object({
|
|
868
|
+
publicKeyJwk: z.string().min(1),
|
|
869
|
+
/**
|
|
870
|
+
* Private key encrypted with a passphrase-derived KEK; opaque to the
|
|
871
|
+
* server. Self-contained blob (embeds KDF salt + iterations).
|
|
872
|
+
*/
|
|
873
|
+
encryptedPrivateKey: z.string().min(1)
|
|
874
|
+
});
|
|
875
|
+
z.object({
|
|
876
|
+
principalType: principalTypeSchema,
|
|
877
|
+
principalId: z.string().min(1),
|
|
878
|
+
wrappedDek: z.string().min(1)
|
|
879
|
+
});
|
|
880
|
+
z.object({
|
|
881
|
+
name: nameSchema,
|
|
882
|
+
tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
|
|
883
|
+
/** SHA-256 hash (base64url) of the full token string. */
|
|
884
|
+
tokenHash: z.string().min(1),
|
|
885
|
+
publicKeyJwk: z.string().min(1),
|
|
886
|
+
/**
|
|
887
|
+
* Org-level capability. Defaults to `member` (a runtime credential); pass
|
|
888
|
+
* `admin` to mint a headless provisioning token. Only an admin caller may
|
|
889
|
+
* create an `admin` token, so capability cannot escalate itself.
|
|
890
|
+
*/
|
|
891
|
+
role: serviceTokenRoleSchema.default("member"),
|
|
892
|
+
/**
|
|
893
|
+
* The application environment this token is bound to (org + app + env).
|
|
894
|
+
* Optional so org-admin tokens can exist, but required for runtime tokens
|
|
895
|
+
* that resolve secrets via `GET /v1/resolve`.
|
|
896
|
+
*/
|
|
897
|
+
environmentId: z.string().min(1).nullish(),
|
|
898
|
+
expiresAt: z.iso.datetime().nullish()
|
|
899
|
+
});
|
|
900
|
+
z.object({
|
|
901
|
+
cursor: z.string().optional(),
|
|
902
|
+
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
903
|
+
action: z.string().optional(),
|
|
904
|
+
resourceType: z.string().optional()
|
|
905
|
+
});
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/target.ts
|
|
908
|
+
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
909
|
+
async function resolveOrg(ctx, orgSlug) {
|
|
910
|
+
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
911
|
+
const { orgs } = await ctx.client.listOrgs();
|
|
912
|
+
if (wanted) {
|
|
913
|
+
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
914
|
+
if (!org) fail(`no accessible org "${wanted}"`);
|
|
915
|
+
return {
|
|
916
|
+
id: org.id,
|
|
917
|
+
slug: org.slug
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const only = orgs[0];
|
|
921
|
+
if (orgs.length === 1 && only) return {
|
|
922
|
+
id: only.id,
|
|
923
|
+
slug: only.slug
|
|
924
|
+
};
|
|
925
|
+
fail("specify --org (or run `seekrit init`)");
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
929
|
+
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
930
|
+
*/
|
|
931
|
+
async function resolveEnvTarget(ctx, opts) {
|
|
932
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
933
|
+
if (!opts.env) fail("specify --env");
|
|
934
|
+
if (opts.group) {
|
|
935
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
936
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
937
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
938
|
+
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
939
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
940
|
+
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
941
|
+
return {
|
|
942
|
+
orgId: org.id,
|
|
943
|
+
envId: env.id,
|
|
944
|
+
label: `${group.slug}@${env.slug}`
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
948
|
+
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
949
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
950
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
951
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
952
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
953
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
954
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
955
|
+
return {
|
|
956
|
+
orgId: org.id,
|
|
957
|
+
envId: env.id,
|
|
958
|
+
label: `${app.slug}/${env.slug}`
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
962
|
+
async function resolveAppEnv(ctx, opts) {
|
|
963
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
964
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
965
|
+
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
966
|
+
if (!opts.env) fail("specify --env");
|
|
967
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
968
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
969
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
970
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
971
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
972
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
973
|
+
return {
|
|
974
|
+
orgId: org.id,
|
|
975
|
+
appId: app.id,
|
|
976
|
+
appSlug: app.slug,
|
|
977
|
+
envId: env.id,
|
|
978
|
+
envSlug: env.slug
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/** Resolve a group by slug within the target org. */
|
|
982
|
+
async function resolveGroup(ctx, opts) {
|
|
983
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
984
|
+
if (!opts.group) fail("specify --group");
|
|
985
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
986
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
987
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
988
|
+
return {
|
|
989
|
+
orgId: org.id,
|
|
990
|
+
id: group.id,
|
|
991
|
+
slug: group.slug
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/pg.ts
|
|
996
|
+
/**
|
|
997
|
+
* `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
|
|
998
|
+
*
|
|
999
|
+
* Zero-knowledge: minting generates the password and its SCRAM verifier on THIS
|
|
1000
|
+
* machine and sends only the verifier; the plaintext password never reaches the
|
|
1001
|
+
* API or gets stored. Registering a target wraps the admin connection string to
|
|
1002
|
+
* the broker's public key locally, so the control plane only ever stores
|
|
1003
|
+
* ciphertext.
|
|
1004
|
+
*/
|
|
1005
|
+
/** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
|
|
1006
|
+
function parseTtlSeconds(input) {
|
|
1007
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1008
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
|
|
1009
|
+
return Number(m[1]) * ({
|
|
1010
|
+
s: 1,
|
|
1011
|
+
m: 60,
|
|
1012
|
+
h: 3600,
|
|
1013
|
+
d: 86400
|
|
1014
|
+
}[m[2] || "s"] ?? 1);
|
|
1015
|
+
}
|
|
1016
|
+
/** A fresh, valid Postgres role name: `tmp_` + lowercase alphanumerics. */
|
|
1017
|
+
function generateRoleName(prefix = "tmp") {
|
|
1018
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1019
|
+
let out = "";
|
|
1020
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
|
|
1021
|
+
for (const b of bytes) out += alphabet[b % 36];
|
|
1022
|
+
return `${prefix}_${out}`;
|
|
1023
|
+
}
|
|
1024
|
+
function registerPgCommands(program) {
|
|
1025
|
+
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
1026
|
+
const target = pg.command("target").description("manage provisioning targets");
|
|
1027
|
+
target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--admin-url <url>", "admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect, []).action(async (options) => {
|
|
1028
|
+
const ctx = buildContext();
|
|
1029
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1030
|
+
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
1031
|
+
if (executor === "remote" && !options.provisionerUrl) fail("--provisioner-url is required for the remote executor");
|
|
1032
|
+
if (![
|
|
1033
|
+
"readonly",
|
|
1034
|
+
"readwrite",
|
|
1035
|
+
"custom"
|
|
1036
|
+
].includes(options.access)) fail("--access must be readonly, readwrite, or custom");
|
|
1037
|
+
const accessLevel = options.access;
|
|
1038
|
+
const adminSecret = options.adminUrl ?? process.env.SEEKRIT_PG_ADMIN_URL;
|
|
1039
|
+
if (!adminSecret) fail(executor === "remote" ? "provide the shared HMAC key via --admin-url or SEEKRIT_PG_ADMIN_URL" : "provide the admin connection string via --admin-url or SEEKRIT_PG_ADMIN_URL");
|
|
1040
|
+
const config = {
|
|
1041
|
+
provider: "postgres",
|
|
1042
|
+
executor,
|
|
1043
|
+
accessLevel,
|
|
1044
|
+
connection: {
|
|
1045
|
+
host: options.host,
|
|
1046
|
+
port: Number.parseInt(options.port, 10),
|
|
1047
|
+
database: options.database
|
|
1048
|
+
},
|
|
1049
|
+
...accessLevel === "custom" ? {
|
|
1050
|
+
...options.createStatement.length ? { createStatements: options.createStatement } : {},
|
|
1051
|
+
...options.revokeStatement.length ? { revokeStatements: options.revokeStatement } : {}
|
|
1052
|
+
} : { schema: options.schema },
|
|
1053
|
+
...options.provisionerUrl ? { provisionerUrl: options.provisionerUrl } : {}
|
|
1054
|
+
};
|
|
1055
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
1056
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminSecret), publicKeyJwk);
|
|
1057
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
1058
|
+
name: options.name,
|
|
1059
|
+
config,
|
|
1060
|
+
wrappedAdminSecret
|
|
1061
|
+
});
|
|
1062
|
+
console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
|
|
1063
|
+
const bootstrap = postgresGroupBootstrapSql(config);
|
|
1064
|
+
if (bootstrap) {
|
|
1065
|
+
console.error("\nRun this once in your database as an admin (safe to re-run):\n");
|
|
1066
|
+
console.log(bootstrap);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
1070
|
+
const ctx = buildContext();
|
|
1071
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1072
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1073
|
+
for (const t of targets) {
|
|
1074
|
+
const cfg = t.config;
|
|
1075
|
+
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
target.command("setup-sql <targetId>").description("print the one-time group-role setup SQL for a preset target").option("--org <slug>").action(async (targetId, options) => {
|
|
1079
|
+
const ctx = buildContext();
|
|
1080
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1081
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1082
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
1083
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
1084
|
+
const bootstrap = postgresGroupBootstrapSql(t.config);
|
|
1085
|
+
if (!bootstrap) fail("this is a custom target — it has no generated setup SQL");
|
|
1086
|
+
console.log(bootstrap);
|
|
1087
|
+
});
|
|
1088
|
+
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
1089
|
+
const ctx = buildContext();
|
|
1090
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1091
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
1092
|
+
console.error(`removed ${targetId}`);
|
|
1093
|
+
});
|
|
1094
|
+
pg.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--role <name>", "role name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
1095
|
+
const ctx = buildContext();
|
|
1096
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1097
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1098
|
+
const target = targets.find((t) => t.id === targetRef || t.name === targetRef);
|
|
1099
|
+
if (!target) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1100
|
+
const roleName = options.role ?? generateRoleName();
|
|
1101
|
+
const ttlSeconds = parseTtlSeconds(options.ttl);
|
|
1102
|
+
const { password, verifier } = await generatePostgresCredential();
|
|
1103
|
+
const { connection } = await ctx.client.mintLease(org.id, {
|
|
1104
|
+
targetId: target.id,
|
|
1105
|
+
roleName,
|
|
1106
|
+
verifier,
|
|
1107
|
+
ttlSeconds
|
|
1108
|
+
});
|
|
1109
|
+
const url = `postgres://${roleName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
|
|
1110
|
+
console.error(`leased ${roleName} on ${connection.host}/${connection.database} — expires ${connection.expiresAt}`);
|
|
1111
|
+
if (options.json) console.log(JSON.stringify({
|
|
1112
|
+
...connection,
|
|
1113
|
+
password,
|
|
1114
|
+
url
|
|
1115
|
+
}, null, 2));
|
|
1116
|
+
else console.log(url);
|
|
1117
|
+
});
|
|
1118
|
+
pg.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
1119
|
+
const ctx = buildContext();
|
|
1120
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1121
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
1122
|
+
for (const l of leases) console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
|
|
1123
|
+
});
|
|
1124
|
+
pg.command("revoke <leaseId>").description("revoke a lease now (drops the role immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
1125
|
+
const ctx = buildContext();
|
|
1126
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1127
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
1128
|
+
console.error(`revoked ${leaseId}`);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
/** Collect a repeatable option into an array. */
|
|
1132
|
+
function collect(value, acc) {
|
|
1133
|
+
acc.push(value);
|
|
1134
|
+
return acc;
|
|
1135
|
+
}
|
|
643
1136
|
//#endregion
|
|
644
1137
|
//#region src/dotenv.ts
|
|
645
1138
|
/**
|
|
@@ -741,94 +1234,6 @@ function printExplain(provenance) {
|
|
|
741
1234
|
for (const name of names) process.stderr.write(`${name.padEnd(width)} ${provenance[name]}\n`);
|
|
742
1235
|
}
|
|
743
1236
|
//#endregion
|
|
744
|
-
//#region src/target.ts
|
|
745
|
-
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
746
|
-
async function resolveOrg(ctx, orgSlug) {
|
|
747
|
-
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
748
|
-
const { orgs } = await ctx.client.listOrgs();
|
|
749
|
-
if (wanted) {
|
|
750
|
-
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
751
|
-
if (!org) fail(`no accessible org "${wanted}"`);
|
|
752
|
-
return {
|
|
753
|
-
id: org.id,
|
|
754
|
-
slug: org.slug
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
const only = orgs[0];
|
|
758
|
-
if (orgs.length === 1 && only) return {
|
|
759
|
-
id: only.id,
|
|
760
|
-
slug: only.slug
|
|
761
|
-
};
|
|
762
|
-
fail("specify --org (or run `seekrit init`)");
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
766
|
-
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
767
|
-
*/
|
|
768
|
-
async function resolveEnvTarget(ctx, opts) {
|
|
769
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
770
|
-
if (!opts.env) fail("specify --env");
|
|
771
|
-
if (opts.group) {
|
|
772
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
773
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
774
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
775
|
-
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
776
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
777
|
-
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
778
|
-
return {
|
|
779
|
-
orgId: org.id,
|
|
780
|
-
envId: env.id,
|
|
781
|
-
label: `${group.slug}@${env.slug}`
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
785
|
-
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
786
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
787
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
788
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
789
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
790
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
791
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
792
|
-
return {
|
|
793
|
-
orgId: org.id,
|
|
794
|
-
envId: env.id,
|
|
795
|
-
label: `${app.slug}/${env.slug}`
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
799
|
-
async function resolveAppEnv(ctx, opts) {
|
|
800
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
801
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
802
|
-
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
803
|
-
if (!opts.env) fail("specify --env");
|
|
804
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
805
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
806
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
807
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
808
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
809
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
810
|
-
return {
|
|
811
|
-
orgId: org.id,
|
|
812
|
-
appId: app.id,
|
|
813
|
-
appSlug: app.slug,
|
|
814
|
-
envId: env.id,
|
|
815
|
-
envSlug: env.slug
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
/** Resolve a group by slug within the target org. */
|
|
819
|
-
async function resolveGroup(ctx, opts) {
|
|
820
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
821
|
-
if (!opts.group) fail("specify --group");
|
|
822
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
823
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
824
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
825
|
-
return {
|
|
826
|
-
orgId: org.id,
|
|
827
|
-
id: group.id,
|
|
828
|
-
slug: group.slug
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
//#endregion
|
|
832
1237
|
//#region src/index.ts
|
|
833
1238
|
/** Collect repeated `--with group=env` flags into a map. */
|
|
834
1239
|
function collectKv(value, acc = {}) {
|
|
@@ -1185,8 +1590,9 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
|
|
|
1185
1590
|
await ctx.client.revokeToken(orgRef.id, tokenId);
|
|
1186
1591
|
console.error(`${tokenId} revoked`);
|
|
1187
1592
|
});
|
|
1593
|
+
registerPgCommands(program);
|
|
1188
1594
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
1189
|
-
const { runMcpServer } = await import("./mcp-
|
|
1595
|
+
const { runMcpServer } = await import("./mcp-Dw3hVhbC.js");
|
|
1190
1596
|
await runMcpServer();
|
|
1191
1597
|
});
|
|
1192
1598
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -1199,4 +1605,4 @@ program.parseAsync().catch((err) => {
|
|
|
1199
1605
|
fail(err instanceof Error ? err.message : String(err));
|
|
1200
1606
|
});
|
|
1201
1607
|
//#endregion
|
|
1202
|
-
export { parseServiceToken as _,
|
|
1608
|
+
export { parseServiceToken as _, resolveEnvTarget as a, getDek as c, setFailThrows as d, writeProjectConfig as f, isServiceToken as g, createServiceToken as h, resolveAppEnv as i, isTokenAuth as l, wrapDek as m, fetchDecryptedSecrets as n, resolveGroup as o, version as p, materializeEnv as r, resolveOrg as s, encryptAndSetSecret as t, tryBuildContext as u, generatePostgresCredential as v, generateDek as y };
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { _ as parseServiceToken, a as
|
|
1
|
+
import { _ as parseServiceToken, a as resolveEnvTarget, c as getDek, d as setFailThrows, f as writeProjectConfig, g as isServiceToken, h as createServiceToken, i as resolveAppEnv, l as isTokenAuth, m as wrapDek, n as fetchDecryptedSecrets, o as resolveGroup, p as version, r as materializeEnv, s as resolveOrg, t as encryptAndSetSecret, u as tryBuildContext, v as generatePostgresCredential, y as generateDek } from "./index.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
6
|
//#region src/mcp.ts
|
|
7
|
+
/** Lowercase-alphanumeric string for a fresh Postgres role name. */
|
|
8
|
+
function randomLower(length) {
|
|
9
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
10
|
+
let out = "";
|
|
11
|
+
for (const b of crypto.getRandomValues(new Uint8Array(length))) out += alphabet[b % 36];
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
7
14
|
function jsonText(data) {
|
|
8
15
|
return { content: [{
|
|
9
16
|
type: "text",
|
|
@@ -534,6 +541,56 @@ async function runMcpServer() {
|
|
|
534
541
|
principalId
|
|
535
542
|
};
|
|
536
543
|
});
|
|
544
|
+
tool("list_pg_targets", "List registered Postgres provisioning targets for temporary credentials.", { org: z.string().optional() }, async ({ org }) => {
|
|
545
|
+
const ctx = getCtx();
|
|
546
|
+
const orgRef = await resolveOrg(ctx, org);
|
|
547
|
+
return (await ctx.client.listLeaseTargets(orgRef.id)).targets;
|
|
548
|
+
});
|
|
549
|
+
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.", {
|
|
550
|
+
org: z.string().optional(),
|
|
551
|
+
target: z.string().describe("target id or name"),
|
|
552
|
+
role: z.string().optional().describe("role name to create (default: random tmp_ name)"),
|
|
553
|
+
ttlSeconds: z.number().int().min(60).max(604800).optional().describe("lifetime (default 3600)")
|
|
554
|
+
}, async ({ org, target, role, ttlSeconds }) => {
|
|
555
|
+
const ctx = getCtx();
|
|
556
|
+
const orgRef = await resolveOrg(ctx, org);
|
|
557
|
+
const { targets } = await ctx.client.listLeaseTargets(orgRef.id);
|
|
558
|
+
const t = targets.find((x) => x.id === target || x.name === target);
|
|
559
|
+
if (!t) throw new Error(`no target "${target}" in ${orgRef.slug}`);
|
|
560
|
+
const roleName = role ?? `tmp_${randomLower(12)}`;
|
|
561
|
+
const { password, verifier } = await generatePostgresCredential();
|
|
562
|
+
const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
|
|
563
|
+
targetId: t.id,
|
|
564
|
+
roleName,
|
|
565
|
+
verifier,
|
|
566
|
+
ttlSeconds: ttlSeconds ?? 3600
|
|
567
|
+
});
|
|
568
|
+
const url = `postgres://${roleName}:${encodeURIComponent(password)}@${connection.host}:${connection.port}/${connection.database}`;
|
|
569
|
+
return {
|
|
570
|
+
leaseId: lease.id,
|
|
571
|
+
url,
|
|
572
|
+
username: roleName,
|
|
573
|
+
expiresAt: connection.expiresAt,
|
|
574
|
+
note: "short-lived credential; it auto-expires and the plaintext is not stored anywhere"
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
tool("list_pg_leases", "List Postgres leases (the ledger — never secret material).", { org: z.string().optional() }, async ({ org }) => {
|
|
578
|
+
const ctx = getCtx();
|
|
579
|
+
const orgRef = await resolveOrg(ctx, org);
|
|
580
|
+
return (await ctx.client.listLeases(orgRef.id)).leases;
|
|
581
|
+
});
|
|
582
|
+
tool("revoke_pg_lease", "Revoke a Postgres lease now (drops the role immediately).", {
|
|
583
|
+
org: z.string().optional(),
|
|
584
|
+
leaseId: z.string()
|
|
585
|
+
}, async ({ org, leaseId }) => {
|
|
586
|
+
const ctx = getCtx();
|
|
587
|
+
const orgRef = await resolveOrg(ctx, org);
|
|
588
|
+
await ctx.client.revokeLease(orgRef.id, leaseId);
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
leaseId
|
|
592
|
+
};
|
|
593
|
+
});
|
|
537
594
|
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.", {
|
|
538
595
|
org: z.string(),
|
|
539
596
|
app: z.string(),
|