@seekrit/cli 0.5.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 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.5.0";
388
+ var version = "0.6.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
- ...options.provisionerUrl ? { provisionerUrl: options.provisionerUrl } : {},
872
- ...options.createStatement.length ? { createStatements: options.createStatement } : {},
873
- ...options.revokeStatement.length ? { revokeStatements: options.revokeStatement } : {}
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-CAX4E0Zg.js");
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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) {