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.
- package/core-dist/wallet-auth.js +62 -0
- package/core-dist/wallet.js +25 -0
- package/lib/auth.mjs +368 -24
- package/lib/projects.mjs +1 -1
- package/package.json +1 -1
- package/sdk/core-dist/wallet-auth.js +62 -0
- package/sdk/core-dist/wallet.js +25 -0
- package/sdk/dist/namespaces/auth.d.ts +98 -1
- package/sdk/dist/namespaces/auth.d.ts.map +1 -1
- package/sdk/dist/namespaces/auth.js +161 -4
- package/sdk/dist/namespaces/auth.js.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts +6 -4
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +7 -8
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/scoped.d.ts +12 -2
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +24 -0
- package/sdk/dist/scoped.js.map +1 -1
|
@@ -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
|
|
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 --
|
|
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
|
|
142
|
+
run402 auth settings [options]
|
|
86
143
|
|
|
87
144
|
Options:
|
|
88
|
-
--allow-password-set <true|false>
|
|
89
|
-
--
|
|
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 --
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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: "
|
|
416
|
+
message: "Set at least one auth setting flag",
|
|
188
417
|
});
|
|
189
418
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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: "--
|
|
199
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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;
|
|
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
|
@@ -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
|