run402 4.96.0 → 4.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +2 -2
  2. package/cli.mjs +1 -7
  3. package/lib/buzz.mjs +1 -1
  4. package/lib/command-manifest.mjs +3 -6
  5. package/lib/credentials.mjs +4 -4
  6. package/lib/deploy-v2.mjs +27 -27
  7. package/lib/errors.mjs +1 -1
  8. package/lib/events.mjs +1 -1
  9. package/lib/grants.mjs +148 -46
  10. package/lib/logs.mjs +1 -1
  11. package/lib/projects.mjs +1 -1
  12. package/lib/sites.mjs +5 -5
  13. package/package.json +1 -1
  14. package/sdk/dist/errors.d.ts +1 -1
  15. package/sdk/dist/errors.js +2 -2
  16. package/sdk/dist/errors.js.map +1 -1
  17. package/sdk/dist/grant-key-credentials.d.ts +38 -0
  18. package/sdk/dist/grant-key-credentials.d.ts.map +1 -0
  19. package/sdk/dist/grant-key-credentials.js +42 -0
  20. package/sdk/dist/grant-key-credentials.js.map +1 -0
  21. package/sdk/dist/index.d.ts +7 -14
  22. package/sdk/dist/index.d.ts.map +1 -1
  23. package/sdk/dist/index.js +6 -13
  24. package/sdk/dist/index.js.map +1 -1
  25. package/sdk/dist/namespaces/billing.d.ts +1 -1
  26. package/sdk/dist/namespaces/billing.js +1 -1
  27. package/sdk/dist/namespaces/buzz.js +1 -1
  28. package/sdk/dist/namespaces/buzz.js.map +1 -1
  29. package/sdk/dist/namespaces/credentials.d.ts +4 -4
  30. package/sdk/dist/namespaces/credentials.js +4 -4
  31. package/sdk/dist/namespaces/deploy.js +5 -5
  32. package/sdk/dist/namespaces/deploy.js.map +1 -1
  33. package/sdk/dist/namespaces/domains.d.ts +1 -1
  34. package/sdk/dist/namespaces/domains.js +1 -1
  35. package/sdk/dist/namespaces/errors.d.ts +1 -1
  36. package/sdk/dist/namespaces/errors.js +2 -2
  37. package/sdk/dist/namespaces/escalations.d.ts +2 -2
  38. package/sdk/dist/namespaces/escalations.js +2 -2
  39. package/sdk/dist/namespaces/escalations.types.d.ts +3 -3
  40. package/sdk/dist/namespaces/escalations.types.d.ts.map +1 -1
  41. package/sdk/dist/namespaces/events.d.ts +1 -1
  42. package/sdk/dist/namespaces/events.js +1 -1
  43. package/sdk/dist/namespaces/functions.js +1 -1
  44. package/sdk/dist/namespaces/functions.js.map +1 -1
  45. package/sdk/dist/namespaces/grants.d.ts +43 -9
  46. package/sdk/dist/namespaces/grants.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/grants.js +99 -10
  48. package/sdk/dist/namespaces/grants.js.map +1 -1
  49. package/sdk/dist/namespaces/grants.types.d.ts +120 -15
  50. package/sdk/dist/namespaces/grants.types.d.ts.map +1 -1
  51. package/sdk/dist/namespaces/grants.types.js +12 -3
  52. package/sdk/dist/namespaces/grants.types.js.map +1 -1
  53. package/sdk/dist/namespaces/identity-links.types.d.ts +2 -2
  54. package/sdk/dist/namespaces/identity-links.types.d.ts.map +1 -1
  55. package/sdk/dist/namespaces/org.d.ts +1 -1
  56. package/sdk/dist/namespaces/org.js +1 -1
  57. package/sdk/dist/namespaces/projects.d.ts +1 -1
  58. package/sdk/dist/namespaces/projects.js +1 -1
  59. package/sdk/dist/namespaces/repos.js +2 -2
  60. package/sdk/dist/namespaces/repos.js.map +1 -1
  61. package/sdk/dist/namespaces/rooms.d.ts +3 -3
  62. package/sdk/dist/namespaces/rooms.js +3 -3
  63. package/sdk/dist/node/credentials.d.ts +9 -9
  64. package/sdk/dist/node/credentials.d.ts.map +1 -1
  65. package/sdk/dist/node/credentials.js +12 -12
  66. package/sdk/dist/node/credentials.js.map +1 -1
  67. package/sdk/dist/node/index.d.ts +5 -5
  68. package/sdk/dist/node/index.d.ts.map +1 -1
  69. package/sdk/dist/node/index.js +2 -2
  70. package/sdk/dist/node/index.js.map +1 -1
  71. package/sdk/dist/project-auth-classification.d.ts +8 -8
  72. package/sdk/dist/project-auth-classification.d.ts.map +1 -1
  73. package/sdk/dist/project-auth-classification.js +7 -7
  74. package/sdk/dist/project-auth-classification.js.map +1 -1
  75. package/sdk/dist/scoped.d.ts +6 -14
  76. package/sdk/dist/scoped.d.ts.map +1 -1
  77. package/sdk/dist/scoped.js +10 -21
  78. package/sdk/dist/scoped.js.map +1 -1
  79. package/vault-surface.json +1 -1
  80. package/lib/delegates.mjs +0 -189
  81. package/sdk/dist/delegate-credentials.d.ts +0 -37
  82. package/sdk/dist/delegate-credentials.d.ts.map +0 -1
  83. package/sdk/dist/delegate-credentials.js +0 -41
  84. package/sdk/dist/delegate-credentials.js.map +0 -1
  85. package/sdk/dist/namespaces/delegates.d.ts +0 -50
  86. package/sdk/dist/namespaces/delegates.d.ts.map +0 -1
  87. package/sdk/dist/namespaces/delegates.js +0 -99
  88. package/sdk/dist/namespaces/delegates.js.map +0 -1
  89. package/sdk/dist/namespaces/delegates.types.d.ts +0 -88
  90. package/sdk/dist/namespaces/delegates.types.d.ts.map +0 -1
  91. package/sdk/dist/namespaces/delegates.types.js +0 -10
  92. package/sdk/dist/namespaces/delegates.types.js.map +0 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Command-line interface for [Run402](https://run402.com) — provision Postgres databases, deploy static sites, run Node 22 serverless functions, host content-addressed CDN assets, send email, sign on-chain. Paid autonomously with x402 (USDC on Base) or MPP (pathUSD on Tempo, or sats over Bitcoin Lightning). **Prototype tier is free on testnet.**
6
6
 
7
- The CLI lets an agent act as its own first-class Run402 principal rather than borrowing a human account. Identity records who invoked an action; organization roles, grants, delegates, freshness, and spend policy determine what that principal may do. People remain first-class owners and members without having to run the CLI themselves.
7
+ The CLI lets an agent act as its own first-class Run402 principal rather than borrowing a human account. Identity records who invoked an action; organization roles, grants, grant keys, freshness, and spend policy determine what that principal may do. People remain first-class owners and members without having to run the CLI themselves.
8
8
 
9
9
  For the full CLI reference (every flag, every subcommand) start at **<https://docs.run402.com/llms-cli.txt>** — the index holds the first-deploy contract and links one fetchable `/llms-cli-<slice>.txt` per topic (deploy, commands, repos, orgs, functions, assets, ops, errors, frontend, platform); <https://docs.run402.com/llms-cli-full.txt> is the whole reference as one document.
10
10
 
@@ -121,7 +121,7 @@ This links two separately held public identities through fresh signatures; it ne
121
121
 
122
122
  ### Buzz community control plane
123
123
 
124
- `run402 buzz status` reports skill installation, human-adoption offers/adoptions, community installation, and this agent's enrollment independently, and safely reports `supported: false` against an older gateway. The canonical ownership workflow is `buzz adopt offer --org … --identity-link …`: it capability-checks before mutation and returns a normal durable HTTPS handoff. Poll with `buzz adopt offer show <buzzhao_id>` and cancel with `buzz adopt offer cancel <buzzhao_id>`. The raw challenge command is explicitly advanced as `buzz adopt direct …`; direct completion/cancellation remain `buzz adopt complete|cancel`. Other goal workflows are `buzz install` and `buzz enroll`; decisions are `buzz install activate|update|revoke` and `buzz approve|deny|revoke`. `buzz install discover --community <buzz:community:host>` lists Run402-owned active descriptors without authentication. MCP intentionally has no signing/passkey mutation and only renders exact HTTPS/CLI handoffs. All commands print JSON to stdout, advice to stderr, have zero spend impact, and never accept Nostr/wallet secrets, session material, service keys, delegates, or payment credentials. Failure JSON preserves the gateway's code-specific `next_actions`, exact repair `field`, and retry safety; never replace it with a generic edit-and-retry. See the [Fizz/Honey examples](../buzz/references/community-control-plane.md).
124
+ `run402 buzz status` reports skill installation, human-adoption offers/adoptions, community installation, and this agent's enrollment independently, and safely reports `supported: false` against an older gateway. The canonical ownership workflow is `buzz adopt offer --org … --identity-link …`: it capability-checks before mutation and returns a normal durable HTTPS handoff. Poll with `buzz adopt offer show <buzzhao_id>` and cancel with `buzz adopt offer cancel <buzzhao_id>`. The raw challenge command is explicitly advanced as `buzz adopt direct …`; direct completion/cancellation remain `buzz adopt complete|cancel`. Other goal workflows are `buzz install` and `buzz enroll`; decisions are `buzz install activate|update|revoke` and `buzz approve|deny|revoke`. `buzz install discover --community <buzz:community:host>` lists Run402-owned active descriptors without authentication. MCP intentionally has no signing/passkey mutation and only renders exact HTTPS/CLI handoffs. All commands print JSON to stdout, advice to stderr, have zero spend impact, and never accept Nostr/wallet secrets, session material, service keys, grant keys, or payment credentials. Failure JSON preserves the gateway's code-specific `next_actions`, exact repair `field`, and retry safety; never replace it with a generic edit-and-retry. See the [Fizz/Honey examples](../buzz/references/community-control-plane.md).
125
125
 
126
126
  ### Buy from an x402 URL
127
127
 
package/cli.mjs CHANGED
@@ -70,8 +70,7 @@ COORDINATE — work alongside other agents and humans
70
70
 
71
71
  AUTHORITY — who may act, and with what credential
72
72
  credentials Manage local credential material (project-keys)
73
- delegates Scoped deploy credentials for agents (create, list, revoke, rotate)
74
- grants Per-project capability grants for agent/CI principals (create, revoke)
73
+ grants Project grants and grant keys for agents (create, list, revoke, revoke-key, rotate-key)
75
74
  orgs Organizations, members, invites & audit (list, get, members, invite, audit)
76
75
  identity Public proof-backed external agent identity links
77
76
  auth Manage project user authentication (magic link, passwords, settings)
@@ -285,11 +284,6 @@ switch (cmd) {
285
284
  await run(sub, rest);
286
285
  break;
287
286
  }
288
- case "delegates": {
289
- const { run } = await import("./lib/delegates.mjs");
290
- await run(sub, rest);
291
- break;
292
- }
293
287
  case "events": {
294
288
  const { run } = await import("./lib/events.mjs");
295
289
  await run(sub, rest);
package/lib/buzz.mjs CHANGED
@@ -67,7 +67,7 @@ Advanced recovery reads:
67
67
  Security:
68
68
  - JSON stdout only; progress and pending-state advice go to stderr.
69
69
  - This command never accepts an nsec, private key, mnemonic, seed, service key,
70
- delegate token, recovery code, or payment material.
70
+ grant key, recovery code, or payment material.
71
71
  - Buzz signatures are public evidence. Run402 authority still comes only from
72
72
  human/agent principals, org membership, and project grants.
73
73
  - Every command in this group has zero spend impact.
@@ -235,13 +235,10 @@ export const COMMAND_MANIFEST = [
235
235
 
236
236
  // ── grants ───────────────────────────────────────────────────────────────
237
237
  { path: ["grants", "create"], positionals: [p("wallet")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["0x1111111111111111111111111111111111111111", "--capability", "deploy"] },
238
+ { path: ["grants", "list"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [] },
238
239
  { path: ["grants", "revoke"], positionals: [p("grant_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["grt_gate1"] },
239
-
240
- // ── delegates ────────────────────────────────────────────────────────────
241
- { path: ["delegates", "create"], legacyPositionalProject: false, positionals: [], projectScoped: true, minimalArgs: ["--grant", "11111111-1111-1111-1111-111111111111"] },
242
- { path: ["delegates", "list"], legacyPositionalProject: false, positionals: [], projectScoped: true, minimalArgs: [] },
243
- { path: ["delegates", "revoke"], legacyPositionalProject: false, positionals: [p("delegate_id")], projectScoped: true, minimalArgs: ["dlg_gate1"] },
244
- { path: ["delegates", "rotate"], legacyPositionalProject: false, positionals: [p("delegate_id")], projectScoped: true, minimalArgs: ["dlg_gate1"] },
240
+ { path: ["grants", "revoke-key"], positionals: [p("key_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["key_gate1"] },
241
+ { path: ["grants", "rotate-key"], positionals: [p("key_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["key_gate1"] },
245
242
 
246
243
  // ── events / live ────────────────────────────────────────────────────────
247
244
  { path: ["events", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
@@ -39,7 +39,7 @@ Project credentials (on the gateway — named, revocable, rotatable):
39
39
  Revoke immediately, freeing the name
40
40
  token [--project <project_id>] [--kind <anon|service>]
41
41
  Mint a SHORT-LIVED token. Works with only a
42
- delegate — the unattended recovery path.
42
+ grant key — the unattended recovery path.
43
43
 
44
44
  Local cache (on this machine):
45
45
  project-keys Manage the local project-key cache
@@ -49,7 +49,7 @@ Notes:
49
49
  plus a fresh step-up, so a scoped agent credential can never escalate
50
50
  itself into a permanent root. Authenticate with a wallet (SIWX) or a
51
51
  sign-in session ('run402 login').
52
- - 'token' is the exception: a delegate can mint one with no human present.
52
+ - 'token' is the exception: a grant key can mint one with no human present.
53
53
  - Secrets are returned EXACTLY ONCE and are never recoverable. Full JSON goes
54
54
  to stdout so you can pipe it; the warnings go to stderr.
55
55
 
@@ -106,7 +106,7 @@ Usage:
106
106
  The secret is printed ONCE, on stdout, inside the JSON. Pipe it:
107
107
  run402 credentials issue --kind service --name ci | jq -r .secret
108
108
 
109
- Requires owner membership on the project's org plus a fresh step-up. A delegate
109
+ Requires owner membership on the project's org plus a fresh step-up. A grant key
110
110
  can NEVER do this; use 'run402 credentials token' instead.
111
111
  `,
112
112
  list: `run402 credentials list — list a project's credentials
@@ -157,7 +157,7 @@ Usage:
157
157
  run402 credentials token [--project <project_id>] [--kind <anon|service>]
158
158
 
159
159
  The cold-restart recovery path, and the ONE credential call an agent can make
160
- with no human present: a delegate is accepted here. There is no step-up because
160
+ with no human present: a grant key is accepted here. There is no step-up because
161
161
  there is nobody to prompt, and what you get back expires, so it cannot become a
162
162
  durable root. Defaults to --kind service.
163
163
  `,
package/lib/deploy-v2.mjs CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  import { getSdk } from "./sdk.mjs";
45
45
  import { reportSdkError, fail } from "./sdk-errors.mjs";
46
46
  import { API, walletAuthHeaders, getActiveProjectId, resolveProjectId, isCoreApiTarget, updateProject } from "./config.mjs";
47
- import { delegateTokenFromEnv } from "#sdk/node";
47
+ import { grantKeyFromEnv } from "#sdk/node";
48
48
  import { flagValue, normalizeArgv } from "./argparse.mjs";
49
49
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
50
50
  import { withAutoApprove } from "./sign-in.mjs";
@@ -486,10 +486,10 @@ async function rehearseCmd(rawArgs) {
486
486
  fail({ code: "BAD_USAGE", message: "--teardown must be one of: on_pass, keep, always", details: { flag: "--teardown", value: teardown } });
487
487
  }
488
488
  const project = flagValue(args, "--project") ?? undefined;
489
- // A delegate is a complete deploy credential and the gateway explicitly
489
+ // A grant key is a complete deploy credential and the gateway explicitly
490
490
  // supports rehearsing with one (the route rejects only CI sessions).
491
491
  // Rehearsal is the SAFE path; never make it the harder one.
492
- if (!isCoreApiTarget() && !loadLiveControlPlaneSession() && !delegateTokenFromEnv()) {
492
+ if (!isCoreApiTarget() && !loadLiveControlPlaneSession() && !grantKeyFromEnv()) {
493
493
  walletAuthHeaders(`/apply/v1/plans/${givenPlanId ?? "_"}/rehearse`);
494
494
  }
495
495
  const sdk = getSdk();
@@ -723,9 +723,9 @@ async function promoteCmd(args) {
723
723
 
724
724
  // Preserve the aggressive early-exit when no local wallet is configured
725
725
  // — same as apply.
726
- // A delegate holds `deploy` and the gateway accepts it on this route; refusing
726
+ // A grant key holds `deploy` and the gateway accepts it on this route; refusing
727
727
  // locally would tell a wallet-less holder to run `run402 init` (see 4.11.2).
728
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/releases");
728
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/releases");
729
729
 
730
730
  try {
731
731
  // Call the engine directly (matches the pattern used by apply / resume
@@ -1295,20 +1295,20 @@ async function deployCmd(args) {
1295
1295
 
1296
1296
 
1297
1297
  let sdkOpts;
1298
- const delegateToken = delegateTokenFromEnv();
1298
+ const grantKey = grantKeyFromEnv();
1299
1299
  if (useGithubActionsOidc) {
1300
1300
  sdkOpts = {
1301
1301
  credentials: githubActionsCredentials({ projectId: releaseSpec.project_id, apiBase: API }),
1302
1302
  disablePaidFetch: true,
1303
1303
  };
1304
- } else if (delegateToken) {
1305
- // A delegate is a complete, self-contained deploy credential: the owner
1304
+ } else if (grantKey) {
1305
+ // A grant key is a complete, self-contained deploy credential: the owner
1306
1306
  // minted it with their wallet and handed it over, so this process needs no
1307
1307
  // wallet of its own. Skipping the guard is the point — an agent that
1308
1308
  // lost its local state (or never had a wallet) can still deploy. Paid fetch
1309
- // is disabled for the same reason it is under CI: a delegate authorizes
1309
+ // is disabled for the same reason it is under CI: a grant key authorizes
1310
1310
  // deploys, not spending.
1311
- sdkOpts = { delegateToken, disablePaidFetch: true };
1311
+ sdkOpts = { grantKey, disablePaidFetch: true };
1312
1312
  } else if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) {
1313
1313
  // Aggressive early exit when no local wallet is configured — unless a
1314
1314
  // wallet-less person is deploying via their sign-in session
@@ -1766,9 +1766,9 @@ async function resumeCmd(args) {
1766
1766
  emitDeployUpdateNotice("resume", args, { quiet: opts.quiet });
1767
1767
  const project = resolveProjectId(opts.project);
1768
1768
 
1769
- // A delegate holds `deploy` and the gateway accepts it on this route; refusing
1769
+ // A grant key holds `deploy` and the gateway accepts it on this route; refusing
1770
1770
  // locally would tell a wallet-less holder to run `run402 init` (see 4.11.2).
1771
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/operations");
1771
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/operations");
1772
1772
 
1773
1773
  try {
1774
1774
  const result = await getSdk({ disablePaidFetch: true })._applyEngine.resume(opts.operationId, {
@@ -1794,9 +1794,9 @@ async function statusCmd(args) {
1794
1794
  missing: "Missing <operation_id>.",
1795
1795
  });
1796
1796
  const project = resolveProjectId(parsed.flags["--project"] ?? null);
1797
- // A delegate holds `deploy` and the gateway accepts it on this route; refusing
1797
+ // A grant key holds `deploy` and the gateway accepts it on this route; refusing
1798
1798
  // locally would tell a wallet-less holder to run `run402 init` (see 4.11.2).
1799
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/operations");
1799
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/operations");
1800
1800
 
1801
1801
  try {
1802
1802
  const result = await getSdk()._applyEngine.status(operationId, { project });
@@ -1822,24 +1822,24 @@ async function listCmd(args) {
1822
1822
  };
1823
1823
 
1824
1824
  const project = resolveProjectId(opts.project);
1825
- // NOT ungated for delegates, deliberately: /apply/v1/operations (list) is
1825
+ // NOT ungated for grant keys, deliberately: /apply/v1/operations (list) is
1826
1826
  // `apikeyAuth` only — unlike get/events/resume/promote, which accept a
1827
- // delegate bearer. Listing every operation on a project is broader than
1827
+ // grant-key bearer. Listing every operation on a project is broader than
1828
1828
  // the per-operation reads a deploy credential is scoped to. If this is
1829
1829
  // ever widened, widen the ROUTE first, then this guard.
1830
1830
  //
1831
1831
  // The refusal is correct; the REMEDY must still be honest. Falling through
1832
- // to NO_WALLET would tell a delegate holder to run `run402 init`, which
1832
+ // to NO_WALLET would tell a grant-key holder to run `run402 init`, which
1833
1833
  // is wrong for a credential that is wallet-less by design — the same
1834
1834
  // misleading-remedy shape removed from the payment path in 4.11.2.
1835
- if (delegateTokenFromEnv()) {
1835
+ if (grantKeyFromEnv()) {
1836
1836
  fail({
1837
- code: "DELEGATE_SCOPE_INSUFFICIENT",
1838
- message: "Listing deploy operations is not available to a delegate.",
1839
- hint: "Use `run402 deploy status <operation_id>`, `deploy events <operation_id>` or `deploy verify <operation_id>` for a specific operation, which a delegate CAN read. A full listing needs the project apikey or the owner wallet.",
1840
- details: { command: "deploy list", credential: "delegate", route: "GET /apply/v1/operations" },
1837
+ code: "GRANT_KEY_SCOPE_INSUFFICIENT",
1838
+ message: "Listing deploy operations is not available to a grant key.",
1839
+ hint: "Use `run402 deploy status <operation_id>`, `deploy events <operation_id>` or `deploy verify <operation_id>` for a specific operation, which a grant key CAN read. A full listing needs the project apikey or the owner wallet.",
1840
+ details: { command: "deploy list", credential: "grant_key", route: "GET /apply/v1/operations" },
1841
1841
  next_actions: [
1842
- { type: "run_command", command: "run402 deploy status <operation_id>", why: "Per-operation reads are within a delegate's deploy scope." },
1842
+ { type: "run_command", command: "run402 deploy status <operation_id>", why: "Per-operation reads are within a grant key's deploy scope." },
1843
1843
  ],
1844
1844
  });
1845
1845
  }
@@ -1870,9 +1870,9 @@ async function eventsCmd(args) {
1870
1870
  const opts = { operationId, project: parsed.flags["--project"] ?? null };
1871
1871
 
1872
1872
  const project = resolveProjectId(opts.project);
1873
- // A delegate holds `deploy` and the gateway accepts it on this route; refusing
1873
+ // A grant key holds `deploy` and the gateway accepts it on this route; refusing
1874
1874
  // locally would tell a wallet-less holder to run `run402 init` (see 4.11.2).
1875
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/operations");
1875
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/operations");
1876
1876
 
1877
1877
  try {
1878
1878
  const result = await getSdk()._applyEngine.events(opts.operationId, { project });
@@ -1916,9 +1916,9 @@ async function verifyCmd(args) {
1916
1916
  ? 60
1917
1917
  : parsePositiveInt(parsed.flags["--timeout"], "--timeout");
1918
1918
 
1919
- // A delegate holds `deploy` and the gateway accepts it on this route; refusing
1919
+ // A grant key holds `deploy` and the gateway accepts it on this route; refusing
1920
1920
  // locally would tell a wallet-less holder to run `run402 init` (see 4.11.2).
1921
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/operations");
1921
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/operations");
1922
1922
 
1923
1923
  try {
1924
1924
  let result;
package/lib/errors.mjs CHANGED
@@ -110,7 +110,7 @@ Exit codes (the promote gate — only when --fail-on-new is set):
110
110
 
111
111
  Auth:
112
112
  The addressed project's own service_key when it is cached locally; otherwise
113
- your wallet / session / delegate, which the gateway authorizes with
113
+ your wallet / session / grant key, which the gateway authorizes with
114
114
  project.read — an org member or teammate agent needs no project key. A key
115
115
  for project A requesting project B's errors gets 403 (never a 404 that
116
116
  leaks existence). Read-only; never lifecycle-gated.
package/lib/events.mjs CHANGED
@@ -95,7 +95,7 @@ Event shape:
95
95
 
96
96
  Auth:
97
97
  --project: the project's own service_key, your SIWX wallet, a control-plane
98
- session, or a scoped delegate with project.read.
98
+ session, or a grant key with project.read.
99
99
  --org: wallet / control-plane session with an active org membership
100
100
  (a project service_key cannot read sibling projects' events).
101
101
  The feed is read-only and never lifecycle-gated: a frozen project's feed
package/lib/grants.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
- import { reportSdkError, parseFlagJson } from "./sdk-errors.mjs";
2
+ import { reportSdkError, parseFlagJson, fail } from "./sdk-errors.mjs";
3
3
  import {
4
4
  normalizeArgv,
5
5
  assertKnownFlags,
@@ -9,114 +9,213 @@ import {
9
9
  failUnknownSubcommand,
10
10
  } from "./argparse.mjs";
11
11
 
12
- const HELP = `run402 grants — per-project capability grants (agent/CI principals)
12
+ const HELP = `run402 grants — per-project capability grants and their grant keys
13
13
 
14
14
  Usage:
15
15
  run402 grants <subcommand> [args...]
16
16
 
17
17
  Subcommands:
18
- create <wallet> --capability <cap> [--project <project_id>] [--policy <json>] [--expires <iso8601>]
19
- Issue a capability grant (owner of the project's org)
18
+ create <wallet> --capability <cap> [--project <project_id>] [--policy <json>] [--expires-at <iso8601>]
19
+ [--key [--kind <kind>] [--scope <json>] [--spend-cap <json>]]
20
+ Issue a grant; with --key also mint its first
21
+ grant key (the token is printed ONCE)
22
+ list [--project <project_id>] List grants with their keys nested (never tokens)
20
23
  revoke <grant_id> [--project <project_id>]
21
- Revoke a capability grant
22
-
23
- Legacy (still supported):
24
- run402 grants create <project_id> <wallet> <capability> [...]
25
- run402 grants revoke <project_id> <grant_id>
24
+ Revoke a grant and every key minted against it
25
+ revoke-key <key_id> [--project <project_id>]
26
+ Revoke one grant key; the grant stays
27
+ rotate-key <key_id> [--project <project_id>]
28
+ Replace a lost key; the new token is printed ONCE
26
29
 
27
30
  Notes:
28
- - Grants let a non-member wallet (an agent or CI principal) act on ONE project,
29
- without making it a broad org member. Mutations require owner of the project's org.
31
+ - A grant is the permission row: this wallet may do this capability on this
32
+ project until this date. An agent with its own wallet needs only the grant.
33
+ - A grant key is a credential minted against exactly one grant: narrower than
34
+ it, spend-capped, expiring, revocable on its own, never an owner. Hand it to
35
+ an agent without a wallet: RUN402_GRANT_KEY=<token> run402 deploy ...
36
+ - Every subcommand requires owner of the project's org. Minting a key with a
37
+ sign-in session takes a fresh step-up.
38
+ - The key token is shown ONCE and cannot be read back. Store it immediately;
39
+ if you lose it, 'rotate-key' issues a new one.
30
40
  - capability examples: deploy, functions:write.
31
41
  - JSON in, JSON out.
32
42
 
33
43
  Examples:
34
44
  run402 grants create 0xf39Fd6...92266 --capability deploy --project prj_abc
35
- run402 grants create 0xf39Fd6...92266 --capability functions:write --expires 2026-12-31T00:00:00Z
36
- run402 grants revoke grt_xyz --project prj_abc
45
+ run402 grants create 0x90F3eB...F80a --capability deploy --key --project prj_abc
46
+ run402 grants list --project prj_abc
47
+ run402 grants revoke-key 5845f17b-... --project prj_abc
48
+ run402 grants revoke 1d86452e-... --project prj_abc
37
49
  `;
38
50
 
39
51
  const SUB_HELP = {
40
- create: `run402 grants create — issue a per-project capability grant
52
+ create: `run402 grants create — issue a per-project capability grant, optionally with a grant key
41
53
 
42
54
  Usage:
43
- run402 grants create <wallet> --capability <cap> [--project <project_id>] [--policy <json>] [--expires <iso8601>]
44
-
45
- Legacy (still supported):
46
- run402 grants create <project_id> <wallet> <capability> [options]
55
+ run402 grants create <wallet> --capability <cap> [--project <project_id>] [--policy <json>] [--expires-at <iso8601>]
56
+ [--key [--kind <kind>] [--scope <json>] [--spend-cap <json>]]
47
57
 
48
58
  Arguments:
49
- <wallet> EVM address or named wallet the grant is issued to
59
+ <wallet> EVM address or named wallet the grant is issued to
50
60
 
51
61
  Options:
52
- --project <project_id> Project to grant access to (defaults to the active project)
53
- --capability <cap> e.g. deploy, functions:write (alternative to the legacy positional)
54
- --policy <json> Capability-scoping policy object (gateway-interpreted)
55
- --expires <iso8601> Expiry timestamp; omit for a non-expiring grant
62
+ --project <project_id> Project to grant access to (defaults to the active project)
63
+ --capability <cap> e.g. deploy, functions:write
64
+ --policy <json> Capability-scoping policy object (gateway-interpreted)
65
+ --expires-at <iso8601> Expiry; omit for a non-expiring grant. With --key the
66
+ key takes the same expiry.
67
+ --key Also mint the grant's first grant key in the same
68
+ transaction. Its token is printed ONCE.
69
+ --kind <kind> Key kind (with --key). Default run402_agent_key, the
70
+ only kind that returns a bearer token.
71
+ --scope <json> Key scope (with --key), e.g. {"v":1,"capabilities":["deploy"]}.
72
+ Defaults to the grant's own capability; never wider.
73
+ --spend-cap <json> Key spend cap (with --key), e.g.
74
+ {"v":1,"currency":"usd_micros","per_period":5000000,"period":"month"}
56
75
 
57
76
  Requires you to be an owner of the project's org.
77
+ `,
78
+ list: `run402 grants list — list a project's grants with their grant keys
79
+
80
+ Usage:
81
+ run402 grants list [--project <project_id>]
82
+
83
+ Each grant carries its keys under "keys" (kind, scope, spend cap, expiry,
84
+ revocation). Never returns a token or secret material.
58
85
  `,
59
86
  revoke: `run402 grants revoke — revoke a per-project capability grant
60
87
 
61
88
  Usage:
62
89
  run402 grants revoke <grant_id> [--project <project_id>]
63
90
 
64
- Legacy (still supported):
65
- run402 grants revoke <project_id> <grant_id>
91
+ Revokes the grant and every grant key minted against it. Requires you to be an
92
+ owner of the project's org.
93
+ `,
94
+ "revoke-key": `run402 grants revoke-key — revoke one grant key
66
95
 
96
+ Usage:
97
+ run402 grants revoke-key <key_id> [--project <project_id>]
98
+
99
+ Takes effect for every subsequent request. The grant and its other keys stay.
67
100
  Requires you to be an owner of the project's org.
101
+ `,
102
+ "rotate-key": `run402 grants rotate-key — replace a grant key
103
+
104
+ Usage:
105
+ run402 grants rotate-key <key_id> [--project <project_id>]
106
+
107
+ Revokes the key and mints a fresh one against the same grant with the same
108
+ kind, scope, spend cap and expiry. The NEW token is printed once. Use this when
109
+ a token is lost or possibly exposed.
68
110
  `,
69
111
  };
70
112
 
71
- const CREATE_VALUE_FLAGS = ["--project", "--capability", "--policy", "--expires"];
113
+ const CREATE_VALUE_FLAGS = ["--project", "--capability", "--policy", "--expires-at", "--kind", "--scope", "--spend-cap"];
114
+ const KEY_ONLY_FLAGS = ["--kind", "--scope", "--spend-cap"];
115
+
116
+ function warnTokenOnce(key) {
117
+ if (!key?.token) return;
118
+ console.error("");
119
+ console.error("The grant key token above is shown ONCE and cannot be read back.");
120
+ console.error("Store it now, then use it as: RUN402_GRANT_KEY=<token>");
121
+ }
72
122
 
73
123
  async function create(args) {
74
124
  const a = normalizeArgv(args);
75
- assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h"], CREATE_VALUE_FLAGS);
125
+ assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--key", "--help", "-h"], CREATE_VALUE_FLAGS);
76
126
  const policyRaw = flagValue(a, "--policy");
77
- const expiresAt = flagValue(a, "--expires");
78
- const capabilityFlag = flagValue(a, "--capability");
127
+ const expiresAt = flagValue(a, "--expires-at") || undefined;
128
+ const capability = flagValue(a, "--capability");
129
+ const wantsKey = a.includes("--key");
130
+ const kind = flagValue(a, "--kind") || undefined;
131
+ const scopeRaw = flagValue(a, "--scope");
132
+ const spendCapRaw = flagValue(a, "--spend-cap");
79
133
  const { projectId, rest } = resolveProjectSelector(a, { valueFlags: CREATE_VALUE_FLAGS });
80
- const expected = capabilityFlag ? 1 : 2;
81
- const pos = requirePositionalCount(rest, CREATE_VALUE_FLAGS, {
82
- min: expected,
83
- max: expected,
84
- command: "run402 grants create <wallet> --capability <cap> [--project <project_id>] [--policy <json>] [--expires <iso8601>]",
85
- missing: capabilityFlag ? "Missing <wallet>." : "Missing <wallet> and/or <capability>.",
134
+ const [wallet] = requirePositionalCount(rest, CREATE_VALUE_FLAGS, {
135
+ min: 1,
136
+ max: 1,
137
+ command: "run402 grants create <wallet> --capability <cap> [--project <project_id>] [--key]",
138
+ missing: "Missing <wallet>.",
86
139
  });
87
- const wallet = pos[0];
88
- const capability = capabilityFlag ?? pos[1];
140
+ if (!capability) {
141
+ fail({
142
+ code: "BAD_USAGE",
143
+ message: "Missing --capability <cap>.",
144
+ hint: "run402 grants create <wallet> --capability deploy [--project <project_id>]",
145
+ });
146
+ }
147
+ if (!wantsKey) {
148
+ const stray = KEY_ONLY_FLAGS.filter((f) => a.includes(f));
149
+ if (stray.length > 0) {
150
+ fail({
151
+ code: "BAD_USAGE",
152
+ message: `${stray.join(", ")} describe a grant key; add --key to mint one.`,
153
+ hint: "run402 grants create <wallet> --capability <cap> --key [--kind <kind>] [--scope <json>] [--spend-cap <json>]",
154
+ });
155
+ }
156
+ }
89
157
  const policy = policyRaw != null ? parseFlagJson("--policy", policyRaw) : undefined;
158
+ let key;
159
+ if (wantsKey) {
160
+ key = {};
161
+ if (kind) key.kind = kind;
162
+ if (scopeRaw != null) key.scope = parseFlagJson("--scope", scopeRaw);
163
+ if (spendCapRaw != null) key.spendCap = parseFlagJson("--spend-cap", spendCapRaw);
164
+ if (expiresAt) key.expiresAt = expiresAt;
165
+ }
90
166
  try {
91
167
  const res = await getSdk().grants.create(projectId, {
92
168
  wallet,
93
169
  capability,
94
170
  policy,
95
- expiresAt: expiresAt || undefined,
171
+ expiresAt,
172
+ key,
96
173
  });
97
174
  console.log(JSON.stringify(res, null, 2));
175
+ warnTokenOnce(res?.key);
98
176
  } catch (err) {
99
177
  reportSdkError(err);
100
178
  }
101
179
  }
102
180
 
103
- async function revoke(args) {
181
+ async function list(args) {
104
182
  const a = normalizeArgv(args);
105
183
  assertKnownFlags(a, ["--project", "--help", "-h"], ["--project"]);
106
184
  const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project"] });
107
- const [grantId] = requirePositionalCount(rest, ["--project"], {
108
- min: 1,
109
- max: 1,
110
- command: "run402 grants revoke <grant_id> [--project <project_id>]",
111
- missing: "Missing <grant_id>.",
185
+ requirePositionalCount(rest, ["--project"], {
186
+ min: 0,
187
+ max: 0,
188
+ command: "run402 grants list [--project <project_id>]",
189
+ missing: "",
112
190
  });
113
191
  try {
114
- console.log(JSON.stringify(await getSdk().grants.revoke(projectId, grantId), null, 2));
192
+ console.log(JSON.stringify(await getSdk().grants.list(projectId), null, 2));
115
193
  } catch (err) {
116
194
  reportSdkError(err);
117
195
  }
118
196
  }
119
197
 
198
+ function oneIdCommand({ verb, idName, method }) {
199
+ return async (args) => {
200
+ const a = normalizeArgv(args);
201
+ assertKnownFlags(a, ["--project", "--help", "-h"], ["--project"]);
202
+ const { projectId, rest } = resolveProjectSelector(a, { valueFlags: ["--project"] });
203
+ const [id] = requirePositionalCount(rest, ["--project"], {
204
+ min: 1,
205
+ max: 1,
206
+ command: `run402 grants ${verb} <${idName}> [--project <project_id>]`,
207
+ missing: `Missing <${idName}>.`,
208
+ });
209
+ try {
210
+ const res = await getSdk().grants[method](projectId, id);
211
+ console.log(JSON.stringify(res, null, 2));
212
+ warnTokenOnce(res?.key);
213
+ } catch (err) {
214
+ reportSdkError(err);
215
+ }
216
+ };
217
+ }
218
+
120
219
  export async function run(sub, args) {
121
220
  if (!sub || sub === "--help" || sub === "-h") {
122
221
  console.log(HELP);
@@ -128,7 +227,10 @@ export async function run(sub, args) {
128
227
  }
129
228
  switch (sub) {
130
229
  case "create": await create(args); break;
131
- case "revoke": await revoke(args); break;
230
+ case "list": await list(args); break;
231
+ case "revoke": await oneIdCommand({ verb: "revoke", idName: "grant_id", method: "revoke" })(args); break;
232
+ case "revoke-key": await oneIdCommand({ verb: "revoke-key", idName: "key_id", method: "revokeKey" })(args); break;
233
+ case "rotate-key": await oneIdCommand({ verb: "rotate-key", idName: "key_id", method: "rotateKey" })(args); break;
132
234
  default:
133
235
  failUnknownSubcommand("grants", sub);
134
236
  }
package/lib/logs.mjs CHANGED
@@ -62,7 +62,7 @@ Output:
62
62
 
63
63
  Auth:
64
64
  The project's cached service_key when present; otherwise your wallet /
65
- session / delegate, which the gateway authorizes with project.read — an org
65
+ session / grant key, which the gateway authorizes with project.read — an org
66
66
  member or teammate agent following an error fingerprint's drill-down needs
67
67
  no project key.
68
68
 
package/lib/projects.mjs CHANGED
@@ -164,7 +164,7 @@ Options:
164
164
 
165
165
  Notes:
166
166
  - Requires project.tenant_payments.read: org developer+ or a read-scoped
167
- project grant/delegate.
167
+ project grant or grant key.
168
168
  - Output is the gateway's redacted payment page. Raw X-PAYMENT headers,
169
169
  authorization hashes, and internal metadata are never returned.
170
170
  `,
package/lib/sites.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { dirname, join, resolve } from "path";
4
- import { fileSetFromDir, delegateTokenFromEnv } from "#sdk/node";
4
+ import { fileSetFromDir, grantKeyFromEnv } from "#sdk/node";
5
5
  import { walletAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
6
6
  import { resolveFilePathsInManifest } from "./manifest.mjs";
7
7
  import { getSdk } from "./sdk.mjs";
@@ -220,9 +220,9 @@ async function deploy(args) {
220
220
  if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
221
221
 
222
222
  // Preserve the aggressive early exit when no local wallet is configured.
223
- // A delegate holds `deploy` and /apply/v1/plans accepts it; refusing locally
223
+ // A grant key holds `deploy` and /apply/v1/plans accepts it; refusing locally
224
224
  // would tell a wallet-less holder to run `run402 init` (see 4.11.2).
225
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/plans");
225
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/plans");
226
226
 
227
227
  const stage = stageFilesToTempDir(manifest.files || []);
228
228
  try {
@@ -289,9 +289,9 @@ async function deployDir(args) {
289
289
  if (opts.target !== undefined) failUnsupportedTarget();
290
290
 
291
291
  // Preserve the aggressive early exit when no local wallet is configured.
292
- // A delegate holds `deploy` and /apply/v1/plans accepts it; refusing locally
292
+ // A grant key holds `deploy` and /apply/v1/plans accepts it; refusing locally
293
293
  // would tell a wallet-less holder to run `run402 init` (see 4.11.2).
294
- if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/plans");
294
+ if (!grantKeyFromEnv()) walletAuthHeaders("/apply/v1/plans");
295
295
 
296
296
  let fileSet;
297
297
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.96.0",
3
+ "version": "4.97.0",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -120,7 +120,7 @@ export declare class ProjectNotFound extends Run402Error {
120
120
  * `NOT_AUTHORIZED`, `STEP_UP_REQUIRED`) — since 402 is reserved for
121
121
  * genuine payment challenges, this now also covers non-payment 403 denials
122
122
  * such as quota (`QUOTA_EXCEEDED`), lifecycle (`PROJECT_FROZEN` /
123
- * `PROJECT_DORMANT`), and delegate spend-cap denials. Check `body.code` (or
123
+ * `PROJECT_DORMANT`), and grant-key spend-cap denials. Check `body.code` (or
124
124
  * use `formatCanonicalErrorContext`-style parsing) to distinguish these
125
125
  * from a true auth failure.
126
126
  */