run402 1.57.5 → 1.58.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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Wallet auth helper — generates EIP-191 signature headers for Run402 API.
3
+ * Uses @noble/curves (lighter than viem) for signing.
4
+ */
5
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
6
+ import { keccak_256 } from "@noble/hashes/sha3.js";
7
+ import { bytesToHex } from "@noble/hashes/utils.js";
8
+ import { readWallet } from "./wallet.js";
9
+ /**
10
+ * EIP-191 personal_sign: sign a message with the wallet's private key.
11
+ */
12
+ function personalSign(privateKeyHex, address, message) {
13
+ const msgBytes = new TextEncoder().encode(message);
14
+ const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
15
+ const prefixed = new Uint8Array(prefix.length + msgBytes.length);
16
+ prefixed.set(prefix);
17
+ prefixed.set(msgBytes, prefix.length);
18
+ const hash = keccak_256(prefixed);
19
+ const pkHex = privateKeyHex.startsWith("0x")
20
+ ? privateKeyHex.slice(2)
21
+ : privateKeyHex;
22
+ const pkBytes = Uint8Array.from(Buffer.from(pkHex, "hex"));
23
+ const rawSig = secp256k1.sign(hash, pkBytes);
24
+ const sig = secp256k1.Signature.fromBytes(rawSig);
25
+ // Determine recovery bit by trying both and matching the address
26
+ let recovery = 0;
27
+ for (const v of [0, 1]) {
28
+ try {
29
+ const recovered = sig.addRecoveryBit(v).recoverPublicKey(hash);
30
+ const pubBytes = recovered.toBytes(false).slice(1); // uncompressed, drop 04 prefix
31
+ const addrBytes = keccak_256(pubBytes).slice(-20);
32
+ if ("0x" + bytesToHex(addrBytes) === address.toLowerCase()) {
33
+ recovery = v;
34
+ break;
35
+ }
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ }
41
+ const r = sig.r.toString(16).padStart(64, "0");
42
+ const s = sig.s.toString(16).padStart(64, "0");
43
+ const vHex = (recovery + 27).toString(16).padStart(2, "0");
44
+ return "0x" + r + s + vHex;
45
+ }
46
+ /**
47
+ * Get wallet auth headers for the Run402 API.
48
+ * Returns null if no wallet is configured.
49
+ */
50
+ export function getWalletAuthHeaders(walletPath) {
51
+ const wallet = readWallet(walletPath);
52
+ if (!wallet || !wallet.address || !wallet.privateKey)
53
+ return null;
54
+ const timestamp = Math.floor(Date.now() / 1000).toString();
55
+ const signature = personalSign(wallet.privateKey, wallet.address, `run402:${timestamp}`);
56
+ return {
57
+ "X-Run402-Wallet": wallet.address,
58
+ "X-Run402-Signature": signature,
59
+ "X-Run402-Timestamp": timestamp,
60
+ };
61
+ }
62
+ //# sourceMappingURL=wallet-auth.js.map
@@ -0,0 +1,25 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { getWalletPath } from "./config.js";
5
+ export function readWallet(path) {
6
+ const p = path ?? getWalletPath();
7
+ if (!existsSync(p))
8
+ return null;
9
+ try {
10
+ return JSON.parse(readFileSync(p, "utf-8"));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function saveWallet(data, path) {
17
+ const p = path ?? getWalletPath();
18
+ const dir = dirname(p);
19
+ mkdirSync(dir, { recursive: true });
20
+ const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
21
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
22
+ renameSync(tmp, p);
23
+ chmodSync(p, 0o600);
24
+ }
25
+ //# sourceMappingURL=wallet.js.map
package/lib/auth.mjs CHANGED
@@ -9,25 +9,50 @@ Usage:
9
9
 
10
10
  Subcommands:
11
11
  magic-link --email <addr> --redirect <url> [--project <id>]
12
- Send a passwordless login link to the given email. Auto-creates user on first use.
12
+ Send a passwordless login link. Use --intent invite with a service key-backed project.
13
13
 
14
14
  verify --token <token> [--project <id>]
15
15
  Exchange a magic link token for access_token + refresh_token.
16
16
 
17
+ create-user --email <addr> [--admin <true|false>] [--invite] [--redirect <url>] [--project <id>]
18
+ Create or update a project auth user with the service key.
19
+
20
+ invite-user --email <addr> --redirect <url> [--admin <true|false>] [--project <id>]
21
+ Create/update a user and send a trusted invite magic link.
22
+
17
23
  set-password --token <bearer> --new <password> [--current <password>] [--project <id>]
18
24
  Change, reset, or set a user's password. Requires the user's access_token.
19
25
 
20
- settings --allow-password-set <true|false> [--project <id>]
26
+ settings [--allow-password-set <true|false>] [--preferred <method|null>] [--public-signup <policy>] [--require-admin-passkey <true|false>] [--project <id>]
21
27
  Update project auth settings (requires service_key).
22
28
 
29
+ passkey-register-options --token <bearer> --app-origin <origin> [--project <id>]
30
+ Create WebAuthn registration options for the authenticated user.
31
+
32
+ passkey-register-verify --token <bearer> --challenge <id> --response <json> [--label <text>] [--project <id>]
33
+ Verify and store a passkey registration response.
34
+
35
+ passkey-login-options --app-origin <origin> [--email <addr>] [--project <id>]
36
+ Create WebAuthn login options.
37
+
38
+ passkey-login-verify --challenge <id> --response <json> [--project <id>]
39
+ Verify a passkey login response and return session tokens.
40
+
41
+ passkeys --token <bearer> [--project <id>]
42
+ List the authenticated user's passkeys.
43
+
44
+ delete-passkey --token <bearer> --id <passkey_id> [--project <id>]
45
+ Delete one authenticated-user passkey.
46
+
23
47
  providers [--project <id>]
24
48
  List available auth providers for the project.
25
49
 
26
50
  Examples:
27
51
  run402 auth magic-link --email user@example.com --redirect https://myapp.run402.com/cb
28
52
  run402 auth verify --token abc123def456
53
+ run402 auth invite-user --email admin@example.com --redirect https://myapp.run402.com/cb --admin true
29
54
  run402 auth set-password --token eyJ... --new "new-pass" --current "old-pass"
30
- run402 auth settings --allow-password-set true
55
+ run402 auth settings --preferred passkey --require-admin-passkey true
31
56
  run402 auth providers
32
57
  `;
33
58
 
@@ -40,6 +65,8 @@ Usage:
40
65
  Options:
41
66
  --email <addr> Required: recipient email address
42
67
  --redirect <url> Required: URL to redirect to after the user clicks
68
+ --intent <intent> signin (default), invite, claim, or recovery
69
+ --state <value> Optional client_state preserved through verification
43
70
  --project <id> Project ID (defaults to active project)
44
71
 
45
72
  Notes:
@@ -48,6 +75,36 @@ Notes:
48
75
  Examples:
49
76
  run402 auth magic-link --email user@example.com \\
50
77
  --redirect https://myapp.run402.com/cb
78
+ `,
79
+ "create-user": `run402 auth create-user — Create or update an auth user
80
+
81
+ Usage:
82
+ run402 auth create-user --email <addr> [options]
83
+
84
+ Options:
85
+ --email <addr> Required: auth user email
86
+ --admin <bool> Optional: set project_admin status
87
+ --invite Send a trusted invite magic link
88
+ --redirect <url> Required when --invite is used
89
+ --state <value> Optional client_state for the invite
90
+ --project <id> Project ID (defaults to active project)
91
+
92
+ Examples:
93
+ run402 auth create-user --email user@example.com
94
+ run402 auth create-user --email admin@example.com --admin true \\
95
+ --invite --redirect https://myapp.run402.com/cb
96
+ `,
97
+ "invite-user": `run402 auth invite-user — Send a trusted auth invite
98
+
99
+ Usage:
100
+ run402 auth invite-user --email <addr> --redirect <url> [options]
101
+
102
+ Options:
103
+ --email <addr> Required: auth user email
104
+ --redirect <url> Required: allowed auth redirect URL
105
+ --admin <bool> Optional: set project_admin status before inviting
106
+ --state <value> Optional client_state for the invite
107
+ --project <id> Project ID (defaults to active project)
51
108
  `,
52
109
  verify: `run402 auth verify — Exchange a magic-link token for session tokens
53
110
 
@@ -82,18 +139,82 @@ Examples:
82
139
  settings: `run402 auth settings — Update project auth settings
83
140
 
84
141
  Usage:
85
- run402 auth settings --allow-password-set <true|false> [options]
142
+ run402 auth settings [options]
86
143
 
87
144
  Options:
88
- --allow-password-set <true|false> Required: toggle password-set flow
89
- --project <id> Project ID (defaults to active project)
145
+ --allow-password-set <true|false> Toggle password-set flow
146
+ --preferred <method|null> password, magic_link, oauth_google, passkey, or null
147
+ --public-signup <policy> open, known_email, or invite_only
148
+ --require-admin-passkey <true|false> Require passkey auth for project_admin sessions
149
+ --project <id> Project ID (defaults to active project)
90
150
 
91
151
  Notes:
92
152
  Requires the project's service_key (admin-level).
93
153
 
94
154
  Examples:
95
155
  run402 auth settings --allow-password-set true
96
- run402 auth settings --allow-password-set false --project prj_abc123
156
+ run402 auth settings --preferred passkey --require-admin-passkey true
157
+ `,
158
+ "passkey-register-options": `run402 auth passkey-register-options — Create passkey registration options
159
+
160
+ Usage:
161
+ run402 auth passkey-register-options --token <bearer> --app-origin <origin> [options]
162
+
163
+ Options:
164
+ --token <bearer> Required: authenticated user's access_token
165
+ --app-origin <origin> Required: exact app origin for WebAuthn
166
+ --project <id> Project ID (defaults to active project)
167
+ `,
168
+ "passkey-register-verify": `run402 auth passkey-register-verify — Verify passkey registration
169
+
170
+ Usage:
171
+ run402 auth passkey-register-verify --token <bearer> --challenge <id> --response <json> [options]
172
+
173
+ Options:
174
+ --token <bearer> Required: authenticated user's access_token
175
+ --challenge <id> Required: challenge_id returned by passkey-register-options
176
+ --response <json> Required: browser PublicKeyCredential JSON
177
+ --label <text> Optional passkey label
178
+ --project <id> Project ID (defaults to active project)
179
+ `,
180
+ "passkey-login-options": `run402 auth passkey-login-options — Create passkey login options
181
+
182
+ Usage:
183
+ run402 auth passkey-login-options --app-origin <origin> [options]
184
+
185
+ Options:
186
+ --app-origin <origin> Required: exact app origin for WebAuthn
187
+ --email <addr> Optional email hint
188
+ --project <id> Project ID (defaults to active project)
189
+ `,
190
+ "passkey-login-verify": `run402 auth passkey-login-verify — Verify passkey login
191
+
192
+ Usage:
193
+ run402 auth passkey-login-verify --challenge <id> --response <json> [options]
194
+
195
+ Options:
196
+ --challenge <id> Required: challenge_id returned by passkey-login-options
197
+ --response <json> Required: browser PublicKeyCredential JSON
198
+ --project <id> Project ID (defaults to active project)
199
+ `,
200
+ passkeys: `run402 auth passkeys — List passkeys
201
+
202
+ Usage:
203
+ run402 auth passkeys --token <bearer> [options]
204
+
205
+ Options:
206
+ --token <bearer> Required: authenticated user's access_token
207
+ --project <id> Project ID (defaults to active project)
208
+ `,
209
+ "delete-passkey": `run402 auth delete-passkey — Delete a passkey
210
+
211
+ Usage:
212
+ run402 auth delete-passkey --token <bearer> --id <passkey_id> [options]
213
+
214
+ Options:
215
+ --token <bearer> Required: authenticated user's access_token
216
+ --id <passkey_id> Required: passkey ID to delete
217
+ --project <id> Project ID (defaults to active project)
97
218
  `,
98
219
  providers: `run402 auth providers — List available auth providers
99
220
 
@@ -116,6 +237,40 @@ function parseFlag(args, flag) {
116
237
  return null;
117
238
  }
118
239
 
240
+ function parseOptionalBool(args, flag) {
241
+ const value = parseFlag(args, flag);
242
+ if (value === null) {
243
+ if (args.includes(flag)) {
244
+ fail({ code: "BAD_USAGE", message: `Missing ${flag} <true|false>` });
245
+ }
246
+ return undefined;
247
+ }
248
+ if (value !== "true" && value !== "false") {
249
+ fail({
250
+ code: "BAD_FLAG",
251
+ message: `${flag} must be 'true' or 'false'`,
252
+ hint: "Use the literal strings 'true' or 'false'.",
253
+ });
254
+ }
255
+ return value === "true";
256
+ }
257
+
258
+ function parseJsonFlag(args, flag) {
259
+ const value = parseFlag(args, flag);
260
+ if (!value) {
261
+ fail({ code: "BAD_USAGE", message: `Missing ${flag} <json>` });
262
+ }
263
+ try {
264
+ return JSON.parse(value);
265
+ } catch (err) {
266
+ fail({
267
+ code: "BAD_JSON",
268
+ message: `${flag} must be valid JSON`,
269
+ hint: err instanceof Error ? err.message : undefined,
270
+ });
271
+ }
272
+ }
273
+
119
274
  async function magicLink(args) {
120
275
  const email = parseFlag(args, "--email");
121
276
  const redirect = parseFlag(args, "--redirect");
@@ -129,8 +284,74 @@ async function magicLink(args) {
129
284
  }
130
285
 
131
286
  try {
132
- await getSdk().auth.requestMagicLink(projectId, { email, redirectUrl: redirect });
133
- console.log(JSON.stringify({ status: "ok", email, redirect_url: redirect }));
287
+ const intent = parseFlag(args, "--intent");
288
+ if (intent && !["signin", "invite", "claim", "recovery"].includes(intent)) {
289
+ fail({ code: "BAD_FLAG", message: "--intent must be signin, invite, claim, or recovery" });
290
+ }
291
+ const state = parseFlag(args, "--state");
292
+ await getSdk().auth.requestMagicLink(projectId, {
293
+ email,
294
+ redirectUrl: redirect,
295
+ intent: intent ?? undefined,
296
+ clientState: state ?? undefined,
297
+ });
298
+ console.log(JSON.stringify({ status: "ok", email, redirect_url: redirect, intent: intent || "signin" }));
299
+ } catch (err) {
300
+ reportSdkError(err);
301
+ }
302
+ }
303
+
304
+ async function createUser(args) {
305
+ const email = parseFlag(args, "--email");
306
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
307
+ const isAdmin = parseOptionalBool(args, "--admin");
308
+ const sendInvite = args.includes("--invite");
309
+ const redirectUrl = parseFlag(args, "--redirect");
310
+ const clientState = parseFlag(args, "--state");
311
+
312
+ if (!email) {
313
+ fail({ code: "BAD_USAGE", message: "Missing --email" });
314
+ }
315
+ if (sendInvite && !redirectUrl) {
316
+ fail({ code: "BAD_USAGE", message: "Missing --redirect <url> when --invite is used" });
317
+ }
318
+
319
+ try {
320
+ const data = await getSdk().auth.createUser(projectId, {
321
+ email,
322
+ isAdmin,
323
+ sendInvite,
324
+ redirectUrl: redirectUrl ?? undefined,
325
+ clientState: clientState ?? undefined,
326
+ });
327
+ console.log(JSON.stringify({ status: "ok", ...data }));
328
+ } catch (err) {
329
+ reportSdkError(err);
330
+ }
331
+ }
332
+
333
+ async function inviteUser(args) {
334
+ const email = parseFlag(args, "--email");
335
+ const redirectUrl = parseFlag(args, "--redirect");
336
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
337
+ const isAdmin = parseOptionalBool(args, "--admin");
338
+ const clientState = parseFlag(args, "--state");
339
+
340
+ if (!email) {
341
+ fail({ code: "BAD_USAGE", message: "Missing --email" });
342
+ }
343
+ if (!redirectUrl) {
344
+ fail({ code: "BAD_USAGE", message: "Missing --redirect <url>" });
345
+ }
346
+
347
+ try {
348
+ const data = await getSdk().auth.inviteUser(projectId, {
349
+ email,
350
+ redirectUrl,
351
+ isAdmin,
352
+ clientState: clientState ?? undefined,
353
+ });
354
+ console.log(JSON.stringify({ status: "ok", ...data }));
134
355
  } catch (err) {
135
356
  reportSdkError(err);
136
357
  }
@@ -178,32 +399,147 @@ async function setPassword(args) {
178
399
  }
179
400
 
180
401
  async function settings(args) {
181
- const allowPasswordSet = parseFlag(args, "--allow-password-set");
182
402
  const projectId = resolveProjectId(parseFlag(args, "--project"));
183
-
184
- if (allowPasswordSet === null) {
403
+ const allow = parseOptionalBool(args, "--allow-password-set");
404
+ const requireAdminPasskey = parseOptionalBool(args, "--require-admin-passkey");
405
+ const preferredRaw = parseFlag(args, "--preferred");
406
+ const publicSignup = parseFlag(args, "--public-signup");
407
+
408
+ if (
409
+ allow === undefined &&
410
+ requireAdminPasskey === undefined &&
411
+ preferredRaw === null &&
412
+ publicSignup === null
413
+ ) {
185
414
  fail({
186
415
  code: "BAD_USAGE",
187
- message: "Missing --allow-password-set <true|false>",
416
+ message: "Set at least one auth setting flag",
188
417
  });
189
418
  }
190
- // Reject anything that isn't literally "true" or "false". Without this guard,
191
- // the previous `=== "true"` coercion silently turned every other input
192
- // (including "1", "yes", "TRUE", "bogus") into `false` and printed
193
- // `{"status":"ok"}`, giving the user the OPPOSITE of what they likely
194
- // intended for this security-adjacent flag. See GH-204.
195
- if (allowPasswordSet !== "true" && allowPasswordSet !== "false") {
419
+ if (
420
+ preferredRaw !== null &&
421
+ !["password", "magic_link", "oauth_google", "passkey", "null"].includes(preferredRaw)
422
+ ) {
196
423
  fail({
197
424
  code: "BAD_FLAG",
198
- message: "--allow-password-set must be 'true' or 'false'",
199
- hint: "Use the literal strings 'true' or 'false'.",
425
+ message: "--preferred must be password, magic_link, oauth_google, passkey, or null",
426
+ });
427
+ }
428
+ if (publicSignup !== null && !["open", "known_email", "invite_only"].includes(publicSignup)) {
429
+ fail({
430
+ code: "BAD_FLAG",
431
+ message: "--public-signup must be open, known_email, or invite_only",
200
432
  });
201
433
  }
202
434
 
203
- const allow = allowPasswordSet === "true";
204
435
  try {
205
- await getSdk().auth.settings(projectId, { allow_password_set: allow });
206
- console.log(JSON.stringify({ status: "ok", allow_password_set: allow }));
436
+ const patch = {
437
+ allow_password_set: allow,
438
+ preferred_sign_in_method: preferredRaw === "null" ? null : preferredRaw ?? undefined,
439
+ public_signup: publicSignup ?? undefined,
440
+ require_passkey_for_project_admin: requireAdminPasskey,
441
+ };
442
+ const data = await getSdk().auth.settings(projectId, patch);
443
+ console.log(JSON.stringify({ status: "ok", ...patch, ...data }));
444
+ } catch (err) {
445
+ reportSdkError(err);
446
+ }
447
+ }
448
+
449
+ async function passkeyRegisterOptions(args) {
450
+ const accessToken = parseFlag(args, "--token");
451
+ const appOrigin = parseFlag(args, "--app-origin");
452
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
453
+ if (!accessToken) fail({ code: "BAD_USAGE", message: "Missing --token <bearer_token>" });
454
+ if (!appOrigin) fail({ code: "BAD_USAGE", message: "Missing --app-origin <origin>" });
455
+ try {
456
+ const data = await getSdk().auth.createPasskeyRegistrationOptions(projectId, {
457
+ accessToken,
458
+ appOrigin,
459
+ });
460
+ console.log(JSON.stringify(data, null, 2));
461
+ } catch (err) {
462
+ reportSdkError(err);
463
+ }
464
+ }
465
+
466
+ async function passkeyRegisterVerify(args) {
467
+ const accessToken = parseFlag(args, "--token");
468
+ const challengeId = parseFlag(args, "--challenge");
469
+ const label = parseFlag(args, "--label");
470
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
471
+ if (!accessToken) fail({ code: "BAD_USAGE", message: "Missing --token <bearer_token>" });
472
+ if (!challengeId) fail({ code: "BAD_USAGE", message: "Missing --challenge <id>" });
473
+ const response = parseJsonFlag(args, "--response");
474
+ try {
475
+ const data = await getSdk().auth.verifyPasskeyRegistration(projectId, {
476
+ accessToken,
477
+ challengeId,
478
+ response,
479
+ label: label ?? undefined,
480
+ });
481
+ console.log(JSON.stringify({ status: "ok", ...data }));
482
+ } catch (err) {
483
+ reportSdkError(err);
484
+ }
485
+ }
486
+
487
+ async function passkeyLoginOptions(args) {
488
+ const appOrigin = parseFlag(args, "--app-origin");
489
+ const email = parseFlag(args, "--email");
490
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
491
+ if (!appOrigin) fail({ code: "BAD_USAGE", message: "Missing --app-origin <origin>" });
492
+ try {
493
+ const data = await getSdk().auth.createPasskeyLoginOptions(projectId, {
494
+ appOrigin,
495
+ email: email ?? undefined,
496
+ });
497
+ console.log(JSON.stringify(data, null, 2));
498
+ } catch (err) {
499
+ reportSdkError(err);
500
+ }
501
+ }
502
+
503
+ async function passkeyLoginVerify(args) {
504
+ const challengeId = parseFlag(args, "--challenge");
505
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
506
+ if (!challengeId) fail({ code: "BAD_USAGE", message: "Missing --challenge <id>" });
507
+ const response = parseJsonFlag(args, "--response");
508
+ try {
509
+ const data = await getSdk().auth.verifyPasskeyLogin(projectId, {
510
+ challengeId,
511
+ response,
512
+ });
513
+ console.log(JSON.stringify({ status: "ok", ...data }));
514
+ } catch (err) {
515
+ reportSdkError(err);
516
+ }
517
+ }
518
+
519
+ async function passkeys(args) {
520
+ const accessToken = parseFlag(args, "--token");
521
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
522
+ if (!accessToken) fail({ code: "BAD_USAGE", message: "Missing --token <bearer_token>" });
523
+ try {
524
+ const data = await getSdk().auth.listPasskeys(projectId, { accessToken });
525
+ console.log(JSON.stringify(data, null, 2));
526
+ } catch (err) {
527
+ reportSdkError(err);
528
+ }
529
+ }
530
+
531
+ async function deletePasskey(args) {
532
+ const accessToken = parseFlag(args, "--token");
533
+ const passkeyId = parseFlag(args, "--id");
534
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
535
+ if (!accessToken) fail({ code: "BAD_USAGE", message: "Missing --token <bearer_token>" });
536
+ if (!passkeyId) fail({ code: "BAD_USAGE", message: "Missing --id <passkey_id>" });
537
+ try {
538
+ await getSdk().auth.deletePasskey(projectId, {
539
+ accessToken,
540
+ passkeyId,
541
+ });
542
+ console.log(JSON.stringify({ status: "ok", passkey_id: passkeyId }));
207
543
  } catch (err) {
208
544
  reportSdkError(err);
209
545
  }
@@ -237,8 +573,16 @@ export async function run(sub, args) {
237
573
  switch (sub) {
238
574
  case "magic-link": await magicLink(args); break;
239
575
  case "verify": await verify(args); break;
576
+ case "create-user": await createUser(args); break;
577
+ case "invite-user": await inviteUser(args); break;
240
578
  case "set-password": await setPassword(args); break;
241
579
  case "settings": await settings(args); break;
580
+ case "passkey-register-options": await passkeyRegisterOptions(args); break;
581
+ case "passkey-register-verify": await passkeyRegisterVerify(args); break;
582
+ case "passkey-login-options": await passkeyLoginOptions(args); break;
583
+ case "passkey-login-verify": await passkeyLoginVerify(args); break;
584
+ case "passkeys": await passkeys(args); break;
585
+ case "delete-passkey": await deletePasskey(args); break;
242
586
  case "providers": await providers(args); break;
243
587
  default:
244
588
  console.error(`Unknown subcommand: ${sub}\n`);
package/lib/projects.mjs CHANGED
@@ -25,7 +25,7 @@ Subcommands:
25
25
  apply-expose [id] --file <path> Apply a manifest from a JSON file
26
26
  get-expose [id] Get the current authorization manifest
27
27
  delete [id] --confirm Immediately and irreversibly delete a project (cascade purge) and remove from local state. Requires --confirm.
28
- pin [id] Pin a project (admin only; project owners get 403 admin_required)
28
+ pin [id] Pin a project (admin only; uses admin allowance wallet)
29
29
  promote-user [id] <email> Promote a user to project_admin role
30
30
  demote-user [id] <email> Demote a user from project_admin role
31
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.57.5",
3
+ "version": "1.58.1",
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": {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Wallet auth helper — generates EIP-191 signature headers for Run402 API.
3
+ * Uses @noble/curves (lighter than viem) for signing.
4
+ */
5
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
6
+ import { keccak_256 } from "@noble/hashes/sha3.js";
7
+ import { bytesToHex } from "@noble/hashes/utils.js";
8
+ import { readWallet } from "./wallet.js";
9
+ /**
10
+ * EIP-191 personal_sign: sign a message with the wallet's private key.
11
+ */
12
+ function personalSign(privateKeyHex, address, message) {
13
+ const msgBytes = new TextEncoder().encode(message);
14
+ const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
15
+ const prefixed = new Uint8Array(prefix.length + msgBytes.length);
16
+ prefixed.set(prefix);
17
+ prefixed.set(msgBytes, prefix.length);
18
+ const hash = keccak_256(prefixed);
19
+ const pkHex = privateKeyHex.startsWith("0x")
20
+ ? privateKeyHex.slice(2)
21
+ : privateKeyHex;
22
+ const pkBytes = Uint8Array.from(Buffer.from(pkHex, "hex"));
23
+ const rawSig = secp256k1.sign(hash, pkBytes);
24
+ const sig = secp256k1.Signature.fromBytes(rawSig);
25
+ // Determine recovery bit by trying both and matching the address
26
+ let recovery = 0;
27
+ for (const v of [0, 1]) {
28
+ try {
29
+ const recovered = sig.addRecoveryBit(v).recoverPublicKey(hash);
30
+ const pubBytes = recovered.toBytes(false).slice(1); // uncompressed, drop 04 prefix
31
+ const addrBytes = keccak_256(pubBytes).slice(-20);
32
+ if ("0x" + bytesToHex(addrBytes) === address.toLowerCase()) {
33
+ recovery = v;
34
+ break;
35
+ }
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ }
41
+ const r = sig.r.toString(16).padStart(64, "0");
42
+ const s = sig.s.toString(16).padStart(64, "0");
43
+ const vHex = (recovery + 27).toString(16).padStart(2, "0");
44
+ return "0x" + r + s + vHex;
45
+ }
46
+ /**
47
+ * Get wallet auth headers for the Run402 API.
48
+ * Returns null if no wallet is configured.
49
+ */
50
+ export function getWalletAuthHeaders(walletPath) {
51
+ const wallet = readWallet(walletPath);
52
+ if (!wallet || !wallet.address || !wallet.privateKey)
53
+ return null;
54
+ const timestamp = Math.floor(Date.now() / 1000).toString();
55
+ const signature = personalSign(wallet.privateKey, wallet.address, `run402:${timestamp}`);
56
+ return {
57
+ "X-Run402-Wallet": wallet.address,
58
+ "X-Run402-Signature": signature,
59
+ "X-Run402-Timestamp": timestamp,
60
+ };
61
+ }
62
+ //# sourceMappingURL=wallet-auth.js.map
@@ -0,0 +1,25 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { getWalletPath } from "./config.js";
5
+ export function readWallet(path) {
6
+ const p = path ?? getWalletPath();
7
+ if (!existsSync(p))
8
+ return null;
9
+ try {
10
+ return JSON.parse(readFileSync(p, "utf-8"));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function saveWallet(data, path) {
17
+ const p = path ?? getWalletPath();
18
+ const dir = dirname(p);
19
+ mkdirSync(dir, { recursive: true });
20
+ const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
21
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
22
+ renameSync(tmp, p);
23
+ chmodSync(p, 0o600);
24
+ }
25
+ //# sourceMappingURL=wallet.js.map