run402 4.18.2 → 4.20.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/README.md +13 -0
- package/cli.mjs +6 -0
- package/lib/command-manifest.mjs +9 -0
- package/lib/credentials.mjs +288 -9
- package/lib/init.mjs +104 -0
- package/lib/redeem.mjs +80 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +5 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +4 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/credentials.d.ts +92 -0
- package/sdk/dist/namespaces/credentials.d.ts.map +1 -1
- package/sdk/dist/namespaces/credentials.js +117 -0
- package/sdk/dist/namespaces/credentials.js.map +1 -1
- package/sdk/dist/namespaces/credentials.types.d.ts +83 -0
- package/sdk/dist/namespaces/credentials.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/credentials.types.js +8 -0
- package/sdk/dist/namespaces/credentials.types.js.map +1 -0
- package/sdk/dist/namespaces/vouchers.d.ts +76 -0
- package/sdk/dist/namespaces/vouchers.d.ts.map +1 -0
- package/sdk/dist/namespaces/vouchers.js +55 -0
- package/sdk/dist/namespaces/vouchers.js.map +1 -0
package/README.md
CHANGED
|
@@ -61,6 +61,19 @@ run402 allowance balance # mainnet + testnet + billing balance
|
|
|
61
61
|
run402 allowance export # print {"address":"0x..."} for funding
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Promo codes
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
run402 redeem R402-K8F3-Q2W9 # any time, before or after `run402 init`
|
|
68
|
+
run402 init --voucher R402-K8F3-Q2W9 # same redemption, folded into setup
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A promo code credits the organization with prepaid credit that settles tiers and
|
|
72
|
+
priced calls with no on-chain payment. Case-insensitive, hyphens optional.
|
|
73
|
+
Retrying is safe — a repeat returns the original result and never credits twice.
|
|
74
|
+
A bad code never blocks `init`; it warns and records `voucher_error` in the JSON
|
|
75
|
+
summary.
|
|
76
|
+
|
|
64
77
|
### Public Buzz/Nostr identity attribution
|
|
65
78
|
|
|
66
79
|
Human accounts connect through <https://console.run402.com/identity-links/connect>: a normal browser, fresh passkey, and Buzz approval, with no terminal/event/passkey credential handling. The CLI commands below are the agent EOA ceremony. `identity link list` preserves every active/revoked record and its proof protocol; public identity links and organization memberships are independently revocable.
|
package/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ Commands:
|
|
|
23
23
|
init Set up allowance, funding, and check tier status (x402 default)
|
|
24
24
|
init mpp Set up with MPP payment rail (Tempo Moderato testnet)
|
|
25
25
|
pay Call an arbitrary x402-priced URL with a bounded payment
|
|
26
|
+
redeem Redeem a promo code for run402 credit
|
|
26
27
|
status Show full account state (allowance, balance, tier, projects)
|
|
27
28
|
wallets Manage multiple named wallets (list, new, use, rename, bind, import)
|
|
28
29
|
credentials Manage local credential material (project-keys)
|
|
@@ -165,6 +166,11 @@ switch (cmd) {
|
|
|
165
166
|
await run([sub, ...rest].filter(Boolean));
|
|
166
167
|
break;
|
|
167
168
|
}
|
|
169
|
+
case "redeem": {
|
|
170
|
+
const { run } = await import("./lib/redeem.mjs");
|
|
171
|
+
await run([sub, ...rest].filter(Boolean));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
168
174
|
case "status": {
|
|
169
175
|
const { run } = await import("./lib/status.mjs");
|
|
170
176
|
await run([sub, ...rest].filter(Boolean));
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export const COMMAND_MANIFEST = [
|
|
|
64
64
|
{ path: ["init"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "flat", skipBehavioral: "creates a wallet and polls funding" },
|
|
65
65
|
{ path: ["pay"], positionals: [p("url")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["https://example.com/"], runStyle: "flat", skipBehavioral: "calls an external URL and may authorize an x402 payment" },
|
|
66
66
|
{ path: ["status"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "flat" },
|
|
67
|
+
{ path: ["redeem"], positionals: [p("code")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["R402-K8F3-Q2W9"], runStyle: "flat", skipBehavioral: "credits real money against the live organization" },
|
|
67
68
|
|
|
68
69
|
// ── wallets ──────────────────────────────────────────────────────────────
|
|
69
70
|
{ path: ["wallets", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
@@ -76,6 +77,14 @@ export const COMMAND_MANIFEST = [
|
|
|
76
77
|
{ path: ["wallets", "import"], positionals: [p("name")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["gate-import-wallet", "--key", "__FIXTURE_FILE__"] },
|
|
77
78
|
{ path: ["wallets", "rm"], positionals: [p("name")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["no-such-wallet", "--yes"] },
|
|
78
79
|
|
|
80
|
+
// ── credentials (gateway project credentials) ────────────────────────────
|
|
81
|
+
{ path: ["credentials", "issue"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--kind", "service", "--name", "gate-cred"] },
|
|
82
|
+
{ path: ["credentials", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [] },
|
|
83
|
+
{ path: ["credentials", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [] },
|
|
84
|
+
{ path: ["credentials", "rotate"], positionals: [p("credential_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["pcr_gate1"] },
|
|
85
|
+
{ path: ["credentials", "revoke"], positionals: [p("credential_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["pcr_gate1"] },
|
|
86
|
+
{ path: ["credentials", "token"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [] },
|
|
87
|
+
|
|
79
88
|
// ── credentials (project-keys group) ─────────────────────────────────────
|
|
80
89
|
{ path: ["credentials", "project-keys", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
81
90
|
{ path: ["credentials", "project-keys", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [] },
|
package/lib/credentials.mjs
CHANGED
|
@@ -8,22 +8,55 @@ import {
|
|
|
8
8
|
removeProject,
|
|
9
9
|
saveProject,
|
|
10
10
|
} from "./config.mjs";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
assertKnownFlags,
|
|
13
|
+
flagValue,
|
|
14
|
+
normalizeArgv,
|
|
15
|
+
positionalArgs,
|
|
16
|
+
failUnknownSubcommand,
|
|
17
|
+
requirePositionalCount,
|
|
18
|
+
resolveProjectSelector,
|
|
19
|
+
} from "./argparse.mjs";
|
|
20
|
+
import { fail, reportSdkError } from "./sdk-errors.mjs";
|
|
21
|
+
import { getSdk } from "./sdk.mjs";
|
|
13
22
|
|
|
14
|
-
const HELP = `run402 credentials —
|
|
23
|
+
const HELP = `run402 credentials — Project credentials, and the local key cache
|
|
15
24
|
|
|
16
25
|
Usage:
|
|
17
26
|
run402 credentials <subcommand> [args...]
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
Project credentials (on the gateway — named, revocable, rotatable):
|
|
29
|
+
issue --kind <anon|service> --name <name> [--project <id>] [--expires <iso8601>]
|
|
30
|
+
Mint one. The secret is printed ONCE.
|
|
31
|
+
list [--project <id>] [--include-revoked]
|
|
32
|
+
List credentials (metadata only, never secrets)
|
|
33
|
+
status [--project <id>] Are you still on the retiring legacy key?
|
|
34
|
+
rotate <credential_id> [--project <id>]
|
|
35
|
+
Replace in one step; new secret printed ONCE
|
|
36
|
+
revoke <credential_id> [--project <id>] [--reason <text>]
|
|
37
|
+
Revoke immediately, freeing the name
|
|
38
|
+
token [--project <id>] [--kind <anon|service>]
|
|
39
|
+
Mint a SHORT-LIVED token. Works with only a
|
|
40
|
+
delegate — the unattended recovery path.
|
|
41
|
+
|
|
42
|
+
Local cache (on this machine):
|
|
43
|
+
project-keys Manage the local project-key cache
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
- 'issue', 'rotate' and 'revoke' need owner membership on the project's org
|
|
47
|
+
plus a fresh step-up, so a scoped agent credential can never escalate
|
|
48
|
+
itself into a permanent root. Authenticate with a wallet (SIWX) or a
|
|
49
|
+
control-plane session ('run402 operator login --step-up').
|
|
50
|
+
- 'token' is the exception: a delegate can mint one with no human present.
|
|
51
|
+
- Secrets are returned EXACTLY ONCE and are never recoverable. Full JSON goes
|
|
52
|
+
to stdout so you can pipe it; the warnings go to stderr.
|
|
21
53
|
|
|
22
54
|
Examples:
|
|
55
|
+
run402 credentials status --project prj_abc123
|
|
56
|
+
run402 credentials issue --kind service --name ci-deploy --project prj_abc123
|
|
57
|
+
run402 credentials issue --kind service --name ci-deploy | jq -r .secret
|
|
58
|
+
run402 credentials rotate pcr_123 --project prj_abc123
|
|
23
59
|
run402 credentials project-keys list
|
|
24
|
-
run402 credentials project-keys status --project prj_abc123
|
|
25
|
-
run402 credentials project-keys import --project prj_abc123 --service-key-stdin
|
|
26
|
-
run402 credentials project-keys export --project prj_abc123 --reveal
|
|
27
60
|
`;
|
|
28
61
|
|
|
29
62
|
const PROJECT_KEYS_HELP = `run402 credentials project-keys — Manage local project-key cache entries
|
|
@@ -50,6 +83,79 @@ Notes:
|
|
|
50
83
|
service key, so an anon rotation never puts a service key through a shell.
|
|
51
84
|
`;
|
|
52
85
|
|
|
86
|
+
const SUB_HELP = {
|
|
87
|
+
issue: `run402 credentials issue — mint a named project credential
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
run402 credentials issue --kind <anon|service> --name <name> [--project <id>]
|
|
91
|
+
[--expires <iso8601>]
|
|
92
|
+
|
|
93
|
+
--kind "anon" is the tenant-facing key; "service" is the privileged one.
|
|
94
|
+
--name Unique among this project's LIVE credentials. Re-using a live name
|
|
95
|
+
returns 409 CREDENTIAL_NAME_TAKEN — that collision is the idempotency
|
|
96
|
+
story, so a retried create never mints a second credential by accident.
|
|
97
|
+
--expires Optional; must be in the future and within one year.
|
|
98
|
+
|
|
99
|
+
The secret is printed ONCE, on stdout, inside the JSON. Pipe it:
|
|
100
|
+
run402 credentials issue --kind service --name ci | jq -r .secret
|
|
101
|
+
|
|
102
|
+
Requires owner membership on the project's org plus a fresh step-up. A delegate
|
|
103
|
+
can NEVER do this; use 'run402 credentials token' instead.
|
|
104
|
+
`,
|
|
105
|
+
list: `run402 credentials list — list a project's credentials
|
|
106
|
+
|
|
107
|
+
Usage:
|
|
108
|
+
run402 credentials list [--project <id>] [--include-revoked]
|
|
109
|
+
|
|
110
|
+
Metadata only — never a secret or a secret hash. Only project.read is needed.
|
|
111
|
+
`,
|
|
112
|
+
status: `run402 credentials status — are you still on the retiring legacy key?
|
|
113
|
+
|
|
114
|
+
Usage:
|
|
115
|
+
run402 credentials status [--project <id>]
|
|
116
|
+
|
|
117
|
+
Returns state "legacy" while the project still depends on the derived
|
|
118
|
+
anon/service keys, or "rotatable" once it holds credentials it can revoke
|
|
119
|
+
individually.
|
|
120
|
+
|
|
121
|
+
There is deliberately NO deadline: retirement is gated on conditions (every
|
|
122
|
+
tenant migrated, 30 consecutive days of zero legacy-key use, explicit operator
|
|
123
|
+
approval), not a date. Read retirement.gated_on rather than planning against a
|
|
124
|
+
date the platform has not committed to.
|
|
125
|
+
`,
|
|
126
|
+
rotate: `run402 credentials rotate — replace a credential in one step
|
|
127
|
+
|
|
128
|
+
Usage:
|
|
129
|
+
run402 credentials rotate <credential_id> [--project <id>]
|
|
130
|
+
|
|
131
|
+
Mints a replacement and revokes the old one in a single transaction, keeping
|
|
132
|
+
the name. The NEW secret is printed once.
|
|
133
|
+
|
|
134
|
+
For a rotation with no downtime window, prefer issuing a SECOND credential,
|
|
135
|
+
deploying it, then revoking the first — several may be live per kind at once,
|
|
136
|
+
and that overlap is the point. Use 'rotate' when the old secret is already
|
|
137
|
+
compromised.
|
|
138
|
+
`,
|
|
139
|
+
revoke: `run402 credentials revoke — revoke a credential immediately
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
run402 credentials revoke <credential_id> [--project <id>] [--reason <text>]
|
|
143
|
+
|
|
144
|
+
Takes effect for every subsequent request and frees the name for reuse.
|
|
145
|
+
Requires owner membership plus step-up.
|
|
146
|
+
`,
|
|
147
|
+
token: `run402 credentials token — mint a short-lived project token
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
run402 credentials token [--project <id>] [--kind <anon|service>]
|
|
151
|
+
|
|
152
|
+
The cold-restart recovery path, and the ONE credential call an agent can make
|
|
153
|
+
with no human present: a delegate is accepted here. There is no step-up because
|
|
154
|
+
there is nobody to prompt, and what you get back expires, so it cannot become a
|
|
155
|
+
durable root. Defaults to --kind service.
|
|
156
|
+
`,
|
|
157
|
+
};
|
|
158
|
+
|
|
53
159
|
function parseProjectKeyFlags(args, extraKnown = [], valueFlagsExtra = []) {
|
|
54
160
|
const parsed = normalizeArgv(args);
|
|
55
161
|
const valueFlags = ["--project", ...valueFlagsExtra];
|
|
@@ -273,6 +379,166 @@ async function runProjectKeys(sub, args) {
|
|
|
273
379
|
}
|
|
274
380
|
}
|
|
275
381
|
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Project credentials — the gateway rows, not the local cache.
|
|
384
|
+
//
|
|
385
|
+
// These five routes shipped with no client at all, so the documented way off
|
|
386
|
+
// the legacy derived anon/service keys was to hand-roll a SIWX-signed request.
|
|
387
|
+
// `run402 pay` is NOT that escape hatch (it is the x402 buyer path and 401s
|
|
388
|
+
// here), which is how an agent could read the docs, find the route, and still
|
|
389
|
+
// have no way to call it.
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
/** Full JSON on stdout (pipeable); the one-shot warning on stderr. */
|
|
393
|
+
function emitIssued(res) {
|
|
394
|
+
console.log(JSON.stringify(res, null, 2));
|
|
395
|
+
if (res?.secret) {
|
|
396
|
+
console.error("");
|
|
397
|
+
console.error("The secret above is shown ONCE and cannot be read back.");
|
|
398
|
+
console.error("Store it now — 'rotate' is the only way to get a new one.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const ISSUE_VALUE_FLAGS = ["--project", "--kind", "--name", "--expires"];
|
|
403
|
+
|
|
404
|
+
async function issue(args) {
|
|
405
|
+
const a = normalizeArgv(args);
|
|
406
|
+
assertKnownFlags(a, [...ISSUE_VALUE_FLAGS, "--help", "-h"], ISSUE_VALUE_FLAGS);
|
|
407
|
+
const kind = flagValue(a, "--kind");
|
|
408
|
+
const name = flagValue(a, "--name");
|
|
409
|
+
const expiresAt = flagValue(a, "--expires");
|
|
410
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ISSUE_VALUE_FLAGS });
|
|
411
|
+
requirePositionalCount(rest, ISSUE_VALUE_FLAGS, {
|
|
412
|
+
min: 0,
|
|
413
|
+
max: 0,
|
|
414
|
+
command: "run402 credentials issue --kind <anon|service> --name <name> [--project <id>]",
|
|
415
|
+
missing: "",
|
|
416
|
+
});
|
|
417
|
+
if (kind !== "anon" && kind !== "service") {
|
|
418
|
+
fail({
|
|
419
|
+
message: 'Missing or invalid --kind. Use "anon" (tenant-facing) or "service" (privileged).',
|
|
420
|
+
code: "BAD_USAGE",
|
|
421
|
+
hint: "run402 credentials issue --kind service --name ci-deploy",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (!name) {
|
|
425
|
+
fail({
|
|
426
|
+
message: "Missing --name.",
|
|
427
|
+
code: "BAD_USAGE",
|
|
428
|
+
hint: "The name identifies this credential in 'list' and is how you rotate it later, e.g. --name ci-deploy",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
emitIssued(await getSdk().credentials.issue(projectId, { kind, name, expiresAt: expiresAt || undefined }));
|
|
433
|
+
} catch (err) {
|
|
434
|
+
reportSdkError(err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function listCredentials(args) {
|
|
439
|
+
const a = normalizeArgv(args);
|
|
440
|
+
assertKnownFlags(a, ["--project", "--include-revoked", "--help", "-h"], ["--project"]);
|
|
441
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project"] });
|
|
442
|
+
requirePositionalCount(rest, ["--project"], {
|
|
443
|
+
min: 0,
|
|
444
|
+
max: 0,
|
|
445
|
+
command: "run402 credentials list [--project <id>]",
|
|
446
|
+
missing: "",
|
|
447
|
+
});
|
|
448
|
+
try {
|
|
449
|
+
const includeRevoked = a.includes("--include-revoked");
|
|
450
|
+
console.log(JSON.stringify(await getSdk().credentials.list(projectId, { includeRevoked }), null, 2));
|
|
451
|
+
} catch (err) {
|
|
452
|
+
reportSdkError(err);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function credentialStatus(args) {
|
|
457
|
+
const a = normalizeArgv(args);
|
|
458
|
+
assertKnownFlags(a, ["--project", "--help", "-h"], ["--project"]);
|
|
459
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project"] });
|
|
460
|
+
requirePositionalCount(rest, ["--project"], {
|
|
461
|
+
min: 0,
|
|
462
|
+
max: 0,
|
|
463
|
+
command: "run402 credentials status [--project <id>]",
|
|
464
|
+
missing: "",
|
|
465
|
+
});
|
|
466
|
+
try {
|
|
467
|
+
const res = await getSdk().credentials.status(projectId);
|
|
468
|
+
console.log(JSON.stringify(res, null, 2));
|
|
469
|
+
if (res?.state === "legacy") {
|
|
470
|
+
console.error("");
|
|
471
|
+
console.error("This project still depends on the derived anon/service keys, whose signing");
|
|
472
|
+
console.error("key is being retired. Issue a credential to stop depending on them:");
|
|
473
|
+
console.error(` run402 credentials issue --kind service --name primary --project ${projectId}`);
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
reportSdkError(err);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function rotate(args) {
|
|
481
|
+
const a = normalizeArgv(args);
|
|
482
|
+
assertKnownFlags(a, ["--project", "--help", "-h"], ["--project"]);
|
|
483
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project"] });
|
|
484
|
+
const [credentialId] = requirePositionalCount(rest, ["--project"], {
|
|
485
|
+
min: 1,
|
|
486
|
+
max: 1,
|
|
487
|
+
command: "run402 credentials rotate <credential_id> [--project <id>]",
|
|
488
|
+
missing: "Missing <credential_id>. Find it with: run402 credentials list",
|
|
489
|
+
});
|
|
490
|
+
try {
|
|
491
|
+
emitIssued(await getSdk().credentials.rotate(projectId, credentialId));
|
|
492
|
+
} catch (err) {
|
|
493
|
+
reportSdkError(err);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function revoke(args) {
|
|
498
|
+
const a = normalizeArgv(args);
|
|
499
|
+
assertKnownFlags(a, ["--project", "--reason", "--help", "-h"], ["--project", "--reason"]);
|
|
500
|
+
const reason = flagValue(a, "--reason");
|
|
501
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project", "--reason"] });
|
|
502
|
+
const [credentialId] = requirePositionalCount(rest, ["--project", "--reason"], {
|
|
503
|
+
min: 1,
|
|
504
|
+
max: 1,
|
|
505
|
+
command: "run402 credentials revoke <credential_id> [--project <id>]",
|
|
506
|
+
missing: "Missing <credential_id>. Find it with: run402 credentials list",
|
|
507
|
+
});
|
|
508
|
+
try {
|
|
509
|
+
const res = await getSdk().credentials.revoke(projectId, credentialId, reason ? { reason } : {});
|
|
510
|
+
console.log(JSON.stringify(res, null, 2));
|
|
511
|
+
} catch (err) {
|
|
512
|
+
reportSdkError(err);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function token(args) {
|
|
517
|
+
const a = normalizeArgv(args);
|
|
518
|
+
assertKnownFlags(a, ["--project", "--kind", "--help", "-h"], ["--project", "--kind"]);
|
|
519
|
+
const kind = flagValue(a, "--kind");
|
|
520
|
+
const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project", "--kind"] });
|
|
521
|
+
requirePositionalCount(rest, ["--project", "--kind"], {
|
|
522
|
+
min: 0,
|
|
523
|
+
max: 0,
|
|
524
|
+
command: "run402 credentials token [--project <id>]",
|
|
525
|
+
missing: "",
|
|
526
|
+
});
|
|
527
|
+
if (kind && kind !== "anon" && kind !== "service") {
|
|
528
|
+
fail({ message: '--kind must be "anon" or "service".', code: "BAD_USAGE" });
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const res = await getSdk().credentials.mintToken(projectId, kind ? { kind } : {});
|
|
532
|
+
console.log(JSON.stringify(res, null, 2));
|
|
533
|
+
if (res?.secret) {
|
|
534
|
+
console.error("");
|
|
535
|
+
console.error(`The token above is shown ONCE and expires in ${res.expires_in ?? "?"}s.`);
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
reportSdkError(err);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
276
542
|
export async function run(sub, args = []) {
|
|
277
543
|
if (!sub || sub === "--help" || sub === "-h") {
|
|
278
544
|
console.log(HELP);
|
|
@@ -283,5 +549,18 @@ export async function run(sub, args = []) {
|
|
|
283
549
|
await runProjectKeys(projectKeySub, rest);
|
|
284
550
|
return;
|
|
285
551
|
}
|
|
286
|
-
|
|
552
|
+
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
|
|
553
|
+
console.log(SUB_HELP[sub] || HELP);
|
|
554
|
+
process.exit(0);
|
|
555
|
+
}
|
|
556
|
+
switch (sub) {
|
|
557
|
+
case "issue": await issue(args); break;
|
|
558
|
+
case "list": await listCredentials(args); break;
|
|
559
|
+
case "status": await credentialStatus(args); break;
|
|
560
|
+
case "rotate": await rotate(args); break;
|
|
561
|
+
case "revoke": await revoke(args); break;
|
|
562
|
+
case "token": await token(args); break;
|
|
563
|
+
default:
|
|
564
|
+
failUnknownSubcommand("credentials", sub);
|
|
565
|
+
}
|
|
287
566
|
}
|
package/lib/init.mjs
CHANGED
|
@@ -26,6 +26,11 @@ Usage:
|
|
|
26
26
|
silently flipping billing networks.
|
|
27
27
|
|
|
28
28
|
Options:
|
|
29
|
+
--voucher <code> Redeem a promo code after setup and credit this
|
|
30
|
+
organization. Never blocks setup: if the code is invalid,
|
|
31
|
+
expired, already used, or the call fails, init warns and
|
|
32
|
+
finishes normally with 'voucher_error' in the summary.
|
|
33
|
+
Equivalent to running 'run402 redeem <code>' afterwards.
|
|
29
34
|
--api-base <url> Configure the active profile to use this API base. Use this
|
|
30
35
|
for a self-hosted Run402 Core Gateway, e.g.
|
|
31
36
|
http://my-core:4020.
|
|
@@ -81,6 +86,43 @@ function parseApiBaseFlag(args) {
|
|
|
81
86
|
return { value: null, args };
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Pull `--voucher <code>` / `--voucher=<code>` out of argv, mirroring
|
|
91
|
+
* `parseApiBaseFlag`. A MISSING VALUE still fails fast — that is a usage error
|
|
92
|
+
* the caller can fix, unlike a redemption failure, which must never take init
|
|
93
|
+
* down with it (see the redemption step at the end of `run`).
|
|
94
|
+
*/
|
|
95
|
+
function parseVoucherFlag(args) {
|
|
96
|
+
for (let i = 0; i < args.length; i++) {
|
|
97
|
+
const arg = args[i];
|
|
98
|
+
if (arg === "--voucher") {
|
|
99
|
+
const value = args[i + 1];
|
|
100
|
+
if (value === undefined || String(value).startsWith("--")) {
|
|
101
|
+
fail({
|
|
102
|
+
code: "BAD_USAGE",
|
|
103
|
+
message: "--voucher requires a value.",
|
|
104
|
+
hint: "run402 init --voucher R402-K8F3-Q2W9",
|
|
105
|
+
details: { flag: "--voucher" },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { value, args: [...args.slice(0, i), ...args.slice(i + 2)] };
|
|
109
|
+
}
|
|
110
|
+
if (typeof arg === "string" && arg.startsWith("--voucher=")) {
|
|
111
|
+
const value = arg.slice("--voucher=".length);
|
|
112
|
+
if (!value) {
|
|
113
|
+
fail({
|
|
114
|
+
code: "BAD_USAGE",
|
|
115
|
+
message: "--voucher requires a non-empty value.",
|
|
116
|
+
hint: "run402 init --voucher R402-K8F3-Q2W9",
|
|
117
|
+
details: { flag: "--voucher" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return { value, args: [...args.slice(0, i), ...args.slice(i + 1)] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { value: null, args };
|
|
124
|
+
}
|
|
125
|
+
|
|
84
126
|
function sameOrigin(a, b) {
|
|
85
127
|
try {
|
|
86
128
|
return new URL(a).origin === new URL(b).origin;
|
|
@@ -128,6 +170,13 @@ export async function run(args = []) {
|
|
|
128
170
|
|
|
129
171
|
if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
|
|
130
172
|
|
|
173
|
+
// Strip --voucher before anything else parses argv, so the rail/positional
|
|
174
|
+
// logic below never sees it (an unrecognized token there would be read as a
|
|
175
|
+
// rail name).
|
|
176
|
+
const parsedVoucher = parseVoucherFlag(args);
|
|
177
|
+
args = parsedVoucher.args;
|
|
178
|
+
const voucherCode = parsedVoucher.value;
|
|
179
|
+
|
|
131
180
|
const parsedApiBase = parseApiBaseFlag(args);
|
|
132
181
|
if (parsedApiBase.value) {
|
|
133
182
|
if (parsedApiBase.args.some((arg) => typeof arg === "string" && !arg.startsWith("--"))) {
|
|
@@ -137,6 +186,17 @@ export async function run(args = []) {
|
|
|
137
186
|
hint: "Run `run402 init --api-base=http://my-core:4020` for Core, or `run402 init` for Run402 Cloud.",
|
|
138
187
|
});
|
|
139
188
|
}
|
|
189
|
+
// This branch configures a Core/API target and returns without setting up
|
|
190
|
+
// an allowance — there is no organization here to credit, and promo
|
|
191
|
+
// vouchers are a Run402 Cloud concept. Say so instead of accepting the
|
|
192
|
+
// flag and silently dropping it.
|
|
193
|
+
if (voucherCode) {
|
|
194
|
+
fail({
|
|
195
|
+
code: "BAD_USAGE",
|
|
196
|
+
message: "run402 init --api-base cannot be combined with --voucher.",
|
|
197
|
+
hint: "Configure the target first (`run402 init --api-base=…`), then redeem against Run402 Cloud with `run402 redeem <code>`.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
140
200
|
const CONFIG_DIR = configDir();
|
|
141
201
|
const detected = await detectTarget(parsedApiBase.value);
|
|
142
202
|
const config = configureApiBase(parsedApiBase.value, {
|
|
@@ -199,6 +259,10 @@ export async function run(args = []) {
|
|
|
199
259
|
rail: null,
|
|
200
260
|
network: null,
|
|
201
261
|
balances: null,
|
|
262
|
+
// Present (null or an object) only when --voucher was passed, so its
|
|
263
|
+
// absence means "no code was offered" rather than "a code silently
|
|
264
|
+
// vanished". `voucher_error` appears alongside a null `voucher`.
|
|
265
|
+
...(voucherCode ? { voucher: null } : {}),
|
|
202
266
|
tier: null,
|
|
203
267
|
projects_saved: 0,
|
|
204
268
|
next_actions: [],
|
|
@@ -335,6 +399,46 @@ export async function run(args = []) {
|
|
|
335
399
|
}
|
|
336
400
|
}
|
|
337
401
|
|
|
402
|
+
// 3b. Promo code, when one was handed to us.
|
|
403
|
+
//
|
|
404
|
+
// This runs BEFORE the balance read below so the credited amount shows up in
|
|
405
|
+
// `prepaid_credit_usd_micros` without a second round-trip, and AFTER the
|
|
406
|
+
// wallet exists so the redemption authenticates as this agent.
|
|
407
|
+
//
|
|
408
|
+
// NOTHING here may fail init. An advertised gift that dead-ends a build is
|
|
409
|
+
// worse than no gift at all: the agent was told to run one command, and that
|
|
410
|
+
// command must still leave it set up and able to work. Every failure — bad
|
|
411
|
+
// code, expired, already used by someone else, org at its ceiling, network
|
|
412
|
+
// down, gateway too old to know the route — warns on stderr, records
|
|
413
|
+
// `voucher_error` in the JSON summary, and lets setup finish.
|
|
414
|
+
if (voucherCode) {
|
|
415
|
+
try {
|
|
416
|
+
const redemption = await getSdk().vouchers.redeem(voucherCode);
|
|
417
|
+
const credited = (redemption.amount_usd_micros / 1_000_000).toFixed(2);
|
|
418
|
+
line(
|
|
419
|
+
"Voucher",
|
|
420
|
+
redemption.already_redeemed
|
|
421
|
+
? `$${credited} already credited (no second credit)`
|
|
422
|
+
: `$${credited} credited`,
|
|
423
|
+
);
|
|
424
|
+
summary.voucher = {
|
|
425
|
+
voucher_id: redemption.voucher_id,
|
|
426
|
+
amount_usd_micros: redemption.amount_usd_micros,
|
|
427
|
+
already_redeemed: redemption.already_redeemed,
|
|
428
|
+
};
|
|
429
|
+
} catch (err) {
|
|
430
|
+
// Faithful: name what failed and keep going. `voucher_error` is a
|
|
431
|
+
// first-class summary field, not an omission the caller has to infer.
|
|
432
|
+
const reason = errorMessage(err);
|
|
433
|
+
line("Voucher", `not applied: ${reason}`);
|
|
434
|
+
summary.voucher = null;
|
|
435
|
+
summary.voucher_error = {
|
|
436
|
+
code: err?.body?.code ?? err?.code ?? "VOUCHER_REDEEM_FAILED",
|
|
437
|
+
message: reason,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
338
442
|
// Balances mirror `run402 status`: the on-chain figure above plus the
|
|
339
443
|
// Run402-held prepaid credit (rail-independent). Prepaid credit is fetched
|
|
340
444
|
// best-effort so a billing read failure never blocks setup.
|
package/lib/redeem.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getSdk } from "./sdk.mjs";
|
|
2
|
+
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
3
|
+
import { assertKnownFlags, normalizeArgv, positionalArgs } from "./argparse.mjs";
|
|
4
|
+
|
|
5
|
+
const HELP = `run402 redeem — Redeem a promo code for run402 credit
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
run402 redeem <code> [--json]
|
|
9
|
+
|
|
10
|
+
A promo code credits your organization with run402 prepaid credit. That credit
|
|
11
|
+
spends like any other prepaid balance — a tier purchase settles from it with no
|
|
12
|
+
on-chain payment.
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
- Order does not matter. This works as your very first authenticated call
|
|
16
|
+
(the organization is created on demand) or long after 'run402 init'.
|
|
17
|
+
- Codes are forgiving: case-insensitive and hyphens are optional, so
|
|
18
|
+
'R402-K8F3-Q2W9' and 'r402k8f3q2w9' are the same code.
|
|
19
|
+
- Retrying is safe. A repeat by the same organization returns the original
|
|
20
|
+
result with "already_redeemed": true and never credits twice.
|
|
21
|
+
- Being in a grace state does not block you. Redeeming is how an owner funds
|
|
22
|
+
a renewal, so it is never gated.
|
|
23
|
+
- One gift per organization: past the lifetime ceiling you get 403
|
|
24
|
+
PROMO_LIMIT_REACHED with the exact numbers.
|
|
25
|
+
|
|
26
|
+
To fold this into first-time setup instead, use:
|
|
27
|
+
run402 init --voucher <code>
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
run402 redeem R402-K8F3-Q2W9
|
|
31
|
+
run402 redeem r402k8f3q2w9
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export async function run(args = []) {
|
|
35
|
+
const parsedArgs = normalizeArgv(args);
|
|
36
|
+
if (parsedArgs.includes("--help") || parsedArgs.includes("-h")) {
|
|
37
|
+
console.log(HELP);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
assertKnownFlags(parsedArgs, ["--help", "-h"], []);
|
|
41
|
+
const positionals = positionalArgs(parsedArgs, []);
|
|
42
|
+
if (positionals.length > 1) {
|
|
43
|
+
fail({
|
|
44
|
+
code: "BAD_USAGE",
|
|
45
|
+
message: `Unexpected argument for redeem: ${positionals[1]}`,
|
|
46
|
+
hint: "Use `run402 redeem <code>`.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const code = positionals[0];
|
|
50
|
+
if (!code) {
|
|
51
|
+
fail({
|
|
52
|
+
code: "BAD_USAGE",
|
|
53
|
+
message: "Missing <code>.",
|
|
54
|
+
hint: "run402 redeem <code> (e.g. run402 redeem R402-K8F3-Q2W9)",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const data = await getSdk().vouchers.redeem(code);
|
|
60
|
+
// Progress to stderr, JSON to stdout — the pipe contract. The one-liner
|
|
61
|
+
// exists because the interesting fact (money arrived) is otherwise buried
|
|
62
|
+
// in a field a human skims past.
|
|
63
|
+
console.error("");
|
|
64
|
+
console.error(
|
|
65
|
+
` Voucher ${usd(data.amount_usd_micros)} credited` +
|
|
66
|
+
`${data.already_redeemed ? " (already redeemed " + data.redeemed_at + " — no second credit)" : ""}`,
|
|
67
|
+
);
|
|
68
|
+
console.error(` Balance ${usd(data.balance_usd_micros)} available`);
|
|
69
|
+
const next = Array.isArray(data.next_actions) ? data.next_actions.find((a) => a?.cli) : null;
|
|
70
|
+
if (next?.cli) console.error(`\n Next: ${next.cli}`);
|
|
71
|
+
console.error("");
|
|
72
|
+
console.log(JSON.stringify(data, null, 2));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
reportSdkError(err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function usd(micros) {
|
|
79
|
+
return `$${(Number(micros ?? 0) / 1_000_000).toFixed(2)}`;
|
|
80
|
+
}
|
package/package.json
CHANGED
package/sdk/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { Ai } from "./namespaces/ai.js";
|
|
|
22
22
|
import { Auth } from "./namespaces/auth.js";
|
|
23
23
|
import { SenderDomain } from "./namespaces/sender-domain.js";
|
|
24
24
|
import { Billing } from "./namespaces/billing.js";
|
|
25
|
+
import { Vouchers } from "./namespaces/vouchers.js";
|
|
25
26
|
import { Wallets, ScopedWallet } from "./namespaces/wallets.js";
|
|
26
27
|
import { Apps } from "./namespaces/apps.js";
|
|
27
28
|
import { Email } from "./namespaces/email.js";
|
|
@@ -86,6 +87,7 @@ export declare class Run402 {
|
|
|
86
87
|
readonly auth: Auth;
|
|
87
88
|
readonly senderDomain: SenderDomain;
|
|
88
89
|
readonly billing: Billing;
|
|
90
|
+
readonly vouchers: Vouchers;
|
|
89
91
|
readonly wallets: Wallets;
|
|
90
92
|
readonly apps: Apps;
|
|
91
93
|
readonly email: Email;
|
|
@@ -286,6 +288,7 @@ export type * from "./namespaces/assets.types.js";
|
|
|
286
288
|
export type * from "./namespaces/ci.types.js";
|
|
287
289
|
export type * from "./namespaces/contracts.js";
|
|
288
290
|
export type * from "./namespaces/credentials.js";
|
|
291
|
+
export type * from "./namespaces/credentials.types.js";
|
|
289
292
|
export type * from "./namespaces/deploy.types.js";
|
|
290
293
|
export { Deploy } from "./namespaces/deploy.js";
|
|
291
294
|
export type { ByteReader } from "./namespaces/deploy.js";
|
|
@@ -309,6 +312,8 @@ export { Rooms, ScopedRoom } from "./namespaces/rooms.js";
|
|
|
309
312
|
export type * from "./namespaces/rooms.types.js";
|
|
310
313
|
export { Errors } from "./namespaces/errors.js";
|
|
311
314
|
export type * from "./namespaces/errors.types.js";
|
|
315
|
+
export { Vouchers } from "./namespaces/vouchers.js";
|
|
316
|
+
export type * from "./namespaces/vouchers.js";
|
|
312
317
|
export { Pay, PaymentBuyerError, PaymentPolicyError, DEFAULT_PAYMENT_MAX_USD_MICROS, X402_COMMERCE_RESULT_SCHEMA_VERSION, X402_EVIDENCE_STATUSES, X402_GATEWAY_AVAILABILITY_ERROR_CODE, X402_MUTATION_STATES, X402_PAYMENT_POLICY_ERROR_CODES, X402_RECOVERY_ACTIONS, RUN402_PENDING_CLASSIFIER_VERSION, isPaymentBuyerError, isPaymentPolicyError, isTrustedRun402PaymentUrl, isTrustedRun402PendingResponse, payFetchResultToJson, } from "./namespaces/pay.js";
|
|
313
318
|
export type * from "./namespaces/pay.js";
|
|
314
319
|
export { Archives } from "./namespaces/archives.js";
|