@seekrit/cli 0.5.0 → 0.7.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 +205 -9
- package/package.json +11 -11
- package/dist/{mcp-CAX4E0Zg.js → mcp-Dw3hVhbC.js} +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. */
|
|
@@ -384,7 +385,7 @@ async function unwrapDek(wrapped, privateKey) {
|
|
|
384
385
|
}
|
|
385
386
|
//#endregion
|
|
386
387
|
//#region package.json
|
|
387
|
-
var version = "0.
|
|
388
|
+
var version = "0.7.0";
|
|
388
389
|
const PROJECT_FILE = "seekrit.json";
|
|
389
390
|
function globalConfigPath() {
|
|
390
391
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
@@ -732,6 +733,176 @@ function formatSecrets(values, format) {
|
|
|
732
733
|
case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
|
|
733
734
|
}
|
|
734
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
|
+
});
|
|
735
906
|
//#endregion
|
|
736
907
|
//#region src/target.ts
|
|
737
908
|
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
@@ -853,24 +1024,33 @@ function generateRoleName(prefix = "tmp") {
|
|
|
853
1024
|
function registerPgCommands(program) {
|
|
854
1025
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
855
1026
|
const target = pg.command("target").description("manage provisioning targets");
|
|
856
|
-
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("--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) => {
|
|
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) => {
|
|
857
1028
|
const ctx = buildContext();
|
|
858
1029
|
const org = await resolveOrg(ctx, options.org);
|
|
859
1030
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
860
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;
|
|
861
1038
|
const adminSecret = options.adminUrl ?? process.env.SEEKRIT_PG_ADMIN_URL;
|
|
862
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");
|
|
863
1040
|
const config = {
|
|
864
1041
|
provider: "postgres",
|
|
865
1042
|
executor,
|
|
1043
|
+
accessLevel,
|
|
866
1044
|
connection: {
|
|
867
1045
|
host: options.host,
|
|
868
1046
|
port: Number.parseInt(options.port, 10),
|
|
869
1047
|
database: options.database
|
|
870
1048
|
},
|
|
871
|
-
...
|
|
872
|
-
|
|
873
|
-
|
|
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 } : {}
|
|
874
1054
|
};
|
|
875
1055
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
876
1056
|
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminSecret), publicKeyJwk);
|
|
@@ -879,7 +1059,12 @@ function registerPgCommands(program) {
|
|
|
879
1059
|
config,
|
|
880
1060
|
wrappedAdminSecret
|
|
881
1061
|
});
|
|
882
|
-
console.error(`registered target ${created.name} (${created.id})`);
|
|
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
|
+
}
|
|
883
1068
|
});
|
|
884
1069
|
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
885
1070
|
const ctx = buildContext();
|
|
@@ -887,9 +1072,19 @@ function registerPgCommands(program) {
|
|
|
887
1072
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
888
1073
|
for (const t of targets) {
|
|
889
1074
|
const cfg = t.config;
|
|
890
|
-
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.executor}`);
|
|
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}`);
|
|
891
1076
|
}
|
|
892
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
|
+
});
|
|
893
1088
|
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
894
1089
|
const ctx = buildContext();
|
|
895
1090
|
const org = await resolveOrg(ctx, options.org);
|
|
@@ -1397,7 +1592,7 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
|
|
|
1397
1592
|
});
|
|
1398
1593
|
registerPgCommands(program);
|
|
1399
1594
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
1400
|
-
const { runMcpServer } = await import("./mcp-
|
|
1595
|
+
const { runMcpServer } = await import("./mcp-Dw3hVhbC.js");
|
|
1401
1596
|
await runMcpServer();
|
|
1402
1597
|
});
|
|
1403
1598
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -1406,7 +1601,8 @@ program.command("audit").description("show the org audit trail").option("--org <
|
|
|
1406
1601
|
const { entries } = await ctx.client.listAudit(orgRef.id, { limit: Number.parseInt(options.limit, 10) || 50 });
|
|
1407
1602
|
for (const entry of entries) console.log(`${entry.createdAt}\t${entry.action}\t${entry.actorType}:${entry.actorId}\t${entry.resourceType}${entry.resourceId ? `:${entry.resourceId}` : ""}`);
|
|
1408
1603
|
});
|
|
1409
|
-
|
|
1604
|
+
const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
|
|
1605
|
+
program.parseAsync(argv).catch((err) => {
|
|
1410
1606
|
fail(err instanceof Error ? err.message : String(err));
|
|
1411
1607
|
});
|
|
1412
1608
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsdown",
|
|
20
|
-
"dev": "tsdown --watch",
|
|
21
|
-
"typecheck": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
18
|
"dependencies": {
|
|
24
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
20
|
"commander": "^15.0.0",
|
|
26
21
|
"zod": "^4.4.3"
|
|
27
22
|
},
|
|
28
23
|
"devDependencies": {
|
|
29
|
-
"@seekrit/api-client": "workspace:*",
|
|
30
|
-
"@seekrit/core": "workspace:*",
|
|
31
|
-
"@seekrit/crypto": "workspace:*",
|
|
32
24
|
"@types/node": "^26.1.0",
|
|
33
|
-
"tsdown": "^0.22.3"
|
|
25
|
+
"tsdown": "^0.22.3",
|
|
26
|
+
"@seekrit/api-client": "0.0.1",
|
|
27
|
+
"@seekrit/core": "0.0.1",
|
|
28
|
+
"@seekrit/crypto": "0.0.1"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsdown",
|
|
32
|
+
"dev": "tsdown --watch",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
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
7
|
/** Lowercase-alphanumeric string for a fresh Postgres role name. */
|
|
8
8
|
function randomLower(length) {
|