run402 2.32.0 → 2.33.1
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/README.md +11 -0
- package/lib/auth.mjs +102 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -212,6 +212,17 @@ Hold several wallets on one machine and select between them:
|
|
|
212
212
|
|
|
213
213
|
The CLI handles all x402 payment signing automatically — never ask the human for a private key or set up payment libraries by hand.
|
|
214
214
|
|
|
215
|
+
### Operator (human / email session)
|
|
216
|
+
|
|
217
|
+
The **operator** is YOU, the human, identified by email — distinct from the agent (your wallet). One browser login spans every wallet that verified your email, so the overview is a cross-wallet union. For a single wallet's account state, use `run402 status`.
|
|
218
|
+
|
|
219
|
+
- `run402 operator login` — browser-delegated sign-in (device-authorization, RFC 8628, like `aws sso login`): magic-link or passkey in the browser, no WebAuthn in the CLI. Caches an email-scoped session at the base config dir (shared across named wallets).
|
|
220
|
+
- `run402 operator overview` — account view across ALL wallets controlling your email (requires login; never falls back to a single wallet).
|
|
221
|
+
- `run402 operator whoami` — show the cached session (email, wallets, expiry); local, no network.
|
|
222
|
+
- `run402 operator logout` — revoke the session server-side and clear the local cache.
|
|
223
|
+
|
|
224
|
+
Not exposed as MCP tools by design — MCP authenticates as the agent (wallet), and the human session must not be handed to it.
|
|
225
|
+
|
|
215
226
|
## Active project (sticky default)
|
|
216
227
|
|
|
217
228
|
After `provision`, the new project becomes the active one. `run402 projects use <id>` switches it. Most commands that take `<id>` default to the active project when omitted.
|
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