run402 2.32.0 → 2.33.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 (2) hide show
  1. package/lib/auth.mjs +102 -0
  2. package/package.json +1 -1
package/lib/auth.mjs CHANGED
@@ -48,6 +48,9 @@ Subcommands:
48
48
  providers [--project <id>]
49
49
  List available auth providers for the project.
50
50
 
51
+ scaffold-roles [--table <name>] [--user-col <col>] [--role-col <col>] [--roles <csv>] [--cache-ttl <secs>]
52
+ Generate a role-table migration + requireRole gate snippet (offline; no project or network).
53
+
51
54
  Examples:
52
55
  run402 auth magic-link --email user@example.com --redirect https://myapp.run402.com/cb
53
56
  run402 auth verify --token abc123def456
@@ -55,6 +58,7 @@ Examples:
55
58
  run402 auth set-password --token eyJ... --new "new-pass" --current "old-pass"
56
59
  run402 auth settings --preferred passkey --require-admin-passkey true
57
60
  run402 auth providers
61
+ run402 auth scaffold-roles --roles operator,editor | jq -r .migration
58
62
  `;
59
63
 
60
64
  const SUB_HELP = {
@@ -233,6 +237,31 @@ Options:
233
237
  Examples:
234
238
  run402 auth providers
235
239
  run402 auth providers --project prj_abc123
240
+ `,
241
+ "scaffold-roles": `run402 auth scaffold-roles — Generate a role-table migration + requireRole gate snippet
242
+
243
+ Usage:
244
+ run402 auth scaffold-roles [options]
245
+
246
+ Generates (offline — no project or network):
247
+ - migration: idempotent CREATE TABLE for the conventional role table
248
+ - gate: a requireRole deploy-spec snippet pointing at it
249
+ - bootstrap: a service-role INSERT to grant the first role
250
+
251
+ Options:
252
+ --table <name> Role table name (default: app_roles)
253
+ --user-col <col> User-id column (default: user_id) — matches the tenant user id (JWT 'sub')
254
+ --role-col <col> Role column (default: role)
255
+ --roles <csv> Allowed roles, comma-separated (default: operator)
256
+ --cache-ttl <secs> Role-lookup cache seconds, 0-600 (default: 60; 0 = instant revocation)
257
+
258
+ Output is a single JSON object; pipe through jq to extract a part:
259
+ run402 auth scaffold-roles --roles operator | jq -r .migration
260
+ run402 auth scaffold-roles --roles operator,editor | jq .gate
261
+
262
+ Notes:
263
+ requireRole(x) requires x in 'allowed'; for multi-role gates read auth.role() and branch.
264
+ The gate accepts any table/columns — this is just the blessed default.
236
265
  `,
237
266
  };
238
267
 
@@ -289,6 +318,10 @@ const AUTH_FLAGS = {
289
318
  known: ["--project", "--help", "-h"],
290
319
  values: ["--project"],
291
320
  },
321
+ "scaffold-roles": {
322
+ known: ["--table", "--user-col", "--role-col", "--roles", "--cache-ttl", "--help", "-h"],
323
+ values: ["--table", "--user-col", "--role-col", "--roles", "--cache-ttl"],
324
+ },
292
325
  };
293
326
 
294
327
  function parseFlag(args, flag) {
@@ -629,6 +662,74 @@ async function providers(args) {
629
662
  }
630
663
  }
631
664
 
665
+ const ROLE_SCAFFOLD_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
666
+
667
+ function scaffoldIdent(args, flag, def) {
668
+ const value = parseFlag(args, flag) || def;
669
+ if (!ROLE_SCAFFOLD_IDENT.test(value)) {
670
+ fail({
671
+ code: "BAD_FLAG",
672
+ message: `${flag} must be an unquoted SQL identifier matching ${ROLE_SCAFFOLD_IDENT.source}`,
673
+ });
674
+ }
675
+ return value;
676
+ }
677
+
678
+ // Pure, offline generator — no SDK / network / project. Emits the conventional
679
+ // role-table migration, the matching requireRole gate snippet, and a
680
+ // first-operator bootstrap. (Keep in sync with the MCP `scaffold_roles` tool in
681
+ // src/tools/scaffold-roles.ts — same artifacts, different presentation.)
682
+ function scaffoldRoles(args) {
683
+ const table = scaffoldIdent(args, "--table", "app_roles");
684
+ const userCol = scaffoldIdent(args, "--user-col", "user_id");
685
+ const roleCol = scaffoldIdent(args, "--role-col", "role");
686
+ const allowed = (parseFlag(args, "--roles") || "operator")
687
+ .split(",")
688
+ .map((r) => r.trim())
689
+ .filter(Boolean);
690
+ if (allowed.length === 0) {
691
+ fail({ code: "BAD_FLAG", message: "--roles must list at least one role (comma-separated)" });
692
+ }
693
+ let cacheTtl = 60;
694
+ const ttlRaw = parseFlag(args, "--cache-ttl");
695
+ if (ttlRaw !== null) {
696
+ const n = Number(ttlRaw);
697
+ if (!Number.isInteger(n) || n < 0 || n > 600) {
698
+ fail({ code: "BAD_FLAG", message: "--cache-ttl must be an integer in [0, 600]" });
699
+ }
700
+ cacheTtl = n;
701
+ }
702
+ const firstRole = allowed[0];
703
+ const migration = `-- Conventional Run402 role table: single role per user, keyed on the tenant user id.
704
+ CREATE TABLE IF NOT EXISTS ${table} (
705
+ ${userCol} uuid NOT NULL,
706
+ ${roleCol} text NOT NULL,
707
+ PRIMARY KEY (${userCol})
708
+ );`;
709
+ const gate = { table, idColumn: userCol, roleColumn: roleCol, allowed, cacheTtl };
710
+ const bootstrap = `-- First-operator bootstrap: run ONCE with the SERVICE key (bypasses RLS).
711
+ -- Replace <FIRST_OPERATOR_USER_ID> with the tenant user id (internal.users.id /
712
+ -- the JWT 'sub') of the first '${firstRole}' — NOT a wallet address.
713
+ INSERT INTO ${table} (${userCol}, ${roleCol})
714
+ VALUES ('<FIRST_OPERATOR_USER_ID>', '${firstRole}')
715
+ ON CONFLICT (${userCol}) DO NOTHING;`;
716
+ console.log(
717
+ JSON.stringify({
718
+ table,
719
+ migration,
720
+ gate,
721
+ bootstrap,
722
+ notes: [
723
+ `Put 'gate' on the function's requireRole deploy-spec field, then call auth.requireRole('${firstRole}') in the function — or auth.role() to branch when 'allowed' has multiple roles.`,
724
+ "requireRole(x) requires x in 'allowed'; for multi-role gates read auth.role() and branch instead of re-asserting.",
725
+ "cacheTtl is the role-lookup cache in seconds; set it to 0 for instant revocation (fresh DB read per request).",
726
+ "The gate keys on the tenant USER id (internal.users.id / JWT 'sub'), NOT a wallet address.",
727
+ `The gate accepts any table/columns — '${table}'(${userCol},${roleCol}) is the blessed default; point requireRole at your own table if you already have one.`,
728
+ ],
729
+ }),
730
+ );
731
+ }
732
+
632
733
  export async function run(sub, args) {
633
734
  if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
634
735
  args = normalizeArgv(args);
@@ -654,6 +755,7 @@ export async function run(sub, args) {
654
755
  case "passkeys": await passkeys(args); break;
655
756
  case "delete-passkey": await deletePasskey(args); break;
656
757
  case "providers": await providers(args); break;
758
+ case "scaffold-roles": scaffoldRoles(args); break;
657
759
  default:
658
760
  console.error(`Unknown subcommand: ${sub}\n`);
659
761
  console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.32.0",
3
+ "version": "2.33.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {