run402 4.93.1 → 4.94.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 (259) hide show
  1. package/README.md +43 -41
  2. package/cli.mjs +34 -29
  3. package/core-dist/config.js +52 -18
  4. package/core-dist/control-plane-session.js +31 -6
  5. package/core-dist/keystore.js +7 -7
  6. package/core-dist/profiles.js +9 -7
  7. package/core-dist/{allowance-auth.js → wallet-auth.js} +15 -15
  8. package/{sdk/core-dist/allowance.js → core-dist/wallet.js} +20 -20
  9. package/{sdk/core-dist/write-auth-session.js → core-dist/write-approvals.js} +19 -19
  10. package/gitvault-surface.json +1 -1
  11. package/lib/admin.mjs +1 -1
  12. package/lib/agent.mjs +20 -18
  13. package/lib/approve.mjs +81 -0
  14. package/lib/apps.mjs +3 -3
  15. package/lib/assets.mjs +2 -2
  16. package/lib/billing.mjs +6 -6
  17. package/lib/buzz-doctor.mjs +1 -1
  18. package/lib/buzz-notifications.mjs +1 -1
  19. package/lib/buzz.mjs +1 -1
  20. package/lib/buzz.test.mjs +1 -1
  21. package/lib/ci.mjs +4 -4
  22. package/lib/cold-start.mjs +21 -21
  23. package/lib/command-manifest.mjs +20 -34
  24. package/lib/config.mjs +24 -24
  25. package/lib/contacts.mjs +13 -13
  26. package/lib/contracts.mjs +1 -1
  27. package/lib/credentials.mjs +2 -2
  28. package/lib/daemon-path.mjs +1 -1
  29. package/lib/delegates.mjs +1 -1
  30. package/lib/deliveries.mjs +4 -4
  31. package/lib/deploy-rehearse-discovery.test.mjs +7 -7
  32. package/lib/deploy-v2.mjs +232 -184
  33. package/lib/deploy.mjs +21 -53
  34. package/lib/doctor-source-scan.mjs +1 -1
  35. package/lib/doctor.mjs +57 -57
  36. package/lib/escalations.mjs +1 -1
  37. package/lib/feedback.mjs +10 -10
  38. package/lib/gitvault-daemon.mjs +1 -1
  39. package/lib/image.mjs +3 -3
  40. package/lib/init.mjs +66 -66
  41. package/lib/lightning-wallet.mjs +28 -28
  42. package/lib/login.mjs +207 -0
  43. package/lib/logout.mjs +53 -0
  44. package/lib/next-actions.mjs +15 -15
  45. package/lib/org.mjs +100 -13
  46. package/lib/pay.mjs +1 -1
  47. package/lib/projects.mjs +26 -29
  48. package/lib/redeem.mjs +7 -8
  49. package/lib/remote-helper-session.mjs +12 -12
  50. package/lib/repos.mjs +62 -62
  51. package/lib/rooms.mjs +103 -67
  52. package/lib/sdk-errors.mjs +9 -4
  53. package/lib/sdk.mjs +3 -3
  54. package/lib/service.mjs +5 -5
  55. package/lib/session-proofs.mjs +21 -0
  56. package/lib/sign-in.mjs +296 -0
  57. package/lib/sites.mjs +8 -8
  58. package/lib/stats.mjs +2 -2
  59. package/lib/status.mjs +25 -25
  60. package/lib/subdomains.mjs +19 -19
  61. package/lib/subscriptions.mjs +8 -8
  62. package/lib/tier.mjs +29 -22
  63. package/lib/transfer.mjs +58 -85
  64. package/lib/up.mjs +7 -7
  65. package/lib/wallet-context.mjs +3 -3
  66. package/lib/wallet-context.test.mjs +3 -3
  67. package/lib/wallets.mjs +196 -31
  68. package/lib/webhook-secret.mjs +6 -6
  69. package/lib/whoami.mjs +75 -0
  70. package/package.json +1 -1
  71. package/sdk/core-dist/config.js +52 -18
  72. package/sdk/core-dist/control-plane-session.js +31 -6
  73. package/sdk/core-dist/keystore.js +7 -7
  74. package/sdk/core-dist/profiles.js +9 -7
  75. package/sdk/core-dist/{allowance-auth.js → wallet-auth.js} +15 -15
  76. package/{core-dist/allowance.js → sdk/core-dist/wallet.js} +20 -20
  77. package/{core-dist/write-auth-session.js → sdk/core-dist/write-approvals.js} +19 -19
  78. package/sdk/dist/actions.d.ts +1 -1
  79. package/sdk/dist/actions.d.ts.map +1 -1
  80. package/sdk/dist/control-plane-credentials.d.ts +8 -8
  81. package/sdk/dist/control-plane-credentials.js +7 -7
  82. package/sdk/dist/credentials.d.ts +19 -19
  83. package/sdk/dist/credentials.d.ts.map +1 -1
  84. package/sdk/dist/credentials.js +1 -1
  85. package/sdk/dist/errors.d.ts +24 -26
  86. package/sdk/dist/errors.d.ts.map +1 -1
  87. package/sdk/dist/errors.js +34 -37
  88. package/sdk/dist/errors.js.map +1 -1
  89. package/sdk/dist/index.d.ts +29 -16
  90. package/sdk/dist/index.d.ts.map +1 -1
  91. package/sdk/dist/index.js +29 -16
  92. package/sdk/dist/index.js.map +1 -1
  93. package/sdk/dist/kernel.d.ts +1 -1
  94. package/sdk/dist/kernel.d.ts.map +1 -1
  95. package/sdk/dist/kernel.js +10 -9
  96. package/sdk/dist/kernel.js.map +1 -1
  97. package/sdk/dist/namespaces/admin.d.ts +51 -120
  98. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  99. package/sdk/dist/namespaces/admin.js +41 -45
  100. package/sdk/dist/namespaces/admin.js.map +1 -1
  101. package/sdk/dist/namespaces/agent.d.ts +2 -2
  102. package/sdk/dist/namespaces/agent.d.ts.map +1 -1
  103. package/sdk/dist/namespaces/agent.js +2 -2
  104. package/sdk/dist/namespaces/agent.js.map +1 -1
  105. package/sdk/dist/namespaces/ai.d.ts +1 -1
  106. package/sdk/dist/namespaces/ai.js +1 -1
  107. package/sdk/dist/namespaces/billing.d.ts +6 -3
  108. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  109. package/sdk/dist/namespaces/billing.js.map +1 -1
  110. package/sdk/dist/namespaces/buzz-notifications.types.d.ts +1 -1
  111. package/sdk/dist/namespaces/buzz.d.ts +2 -2
  112. package/sdk/dist/namespaces/buzz.d.ts.map +1 -1
  113. package/sdk/dist/namespaces/buzz.js +2 -2
  114. package/sdk/dist/namespaces/buzz.js.map +1 -1
  115. package/sdk/dist/namespaces/buzz.types.d.ts +2 -2
  116. package/sdk/dist/namespaces/buzz.types.d.ts.map +1 -1
  117. package/sdk/dist/namespaces/credentials.types.d.ts +1 -1
  118. package/sdk/dist/namespaces/deploy.d.ts +1 -1
  119. package/sdk/dist/namespaces/deploy.js +5 -5
  120. package/sdk/dist/namespaces/deploy.js.map +1 -1
  121. package/sdk/dist/namespaces/deploy.types.d.ts +4 -4
  122. package/sdk/dist/namespaces/escalations.d.ts +1 -1
  123. package/sdk/dist/namespaces/escalations.js +1 -1
  124. package/sdk/dist/namespaces/escalations.types.d.ts +2 -2
  125. package/sdk/dist/namespaces/escalations.types.d.ts.map +1 -1
  126. package/sdk/dist/namespaces/events.types.d.ts +1 -1
  127. package/sdk/dist/namespaces/functions.d.ts +1 -1
  128. package/sdk/dist/namespaces/functions.js +1 -1
  129. package/sdk/dist/namespaces/gitvault.crypto.d.ts +1 -1
  130. package/sdk/dist/namespaces/gitvault.d.ts +24 -24
  131. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  132. package/sdk/dist/namespaces/gitvault.js +109 -109
  133. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  134. package/sdk/dist/namespaces/me.d.ts +120 -0
  135. package/sdk/dist/namespaces/me.d.ts.map +1 -0
  136. package/sdk/dist/namespaces/me.js +34 -0
  137. package/sdk/dist/namespaces/me.js.map +1 -0
  138. package/sdk/dist/namespaces/org.d.ts +108 -6
  139. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  140. package/sdk/dist/namespaces/org.js +63 -7
  141. package/sdk/dist/namespaces/org.js.map +1 -1
  142. package/sdk/dist/namespaces/org.types.d.ts +29 -9
  143. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  144. package/sdk/dist/namespaces/pay.js +3 -3
  145. package/sdk/dist/namespaces/pay.js.map +1 -1
  146. package/sdk/dist/namespaces/projects.d.ts +9 -9
  147. package/sdk/dist/namespaces/projects.js +15 -15
  148. package/sdk/dist/namespaces/projects.js.map +1 -1
  149. package/sdk/dist/namespaces/projects.types.d.ts +28 -12
  150. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  151. package/sdk/dist/namespaces/rooms.d.ts +7 -7
  152. package/sdk/dist/namespaces/rooms.js +17 -17
  153. package/sdk/dist/namespaces/rooms.js.map +1 -1
  154. package/sdk/dist/namespaces/rooms.types.d.ts +9 -9
  155. package/sdk/dist/namespaces/rooms.types.d.ts.map +1 -1
  156. package/sdk/dist/namespaces/service.d.ts +1 -1
  157. package/sdk/dist/namespaces/service.js +1 -1
  158. package/sdk/dist/namespaces/{operator-session.d.ts → session.d.ts} +135 -51
  159. package/sdk/dist/namespaces/session.d.ts.map +1 -0
  160. package/sdk/dist/namespaces/{operator-session.js → session.js} +118 -30
  161. package/sdk/dist/namespaces/session.js.map +1 -0
  162. package/sdk/dist/namespaces/subdomains.d.ts +5 -5
  163. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  164. package/sdk/dist/namespaces/subdomains.js +6 -6
  165. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  166. package/sdk/dist/namespaces/tier.d.ts +43 -20
  167. package/sdk/dist/namespaces/tier.d.ts.map +1 -1
  168. package/sdk/dist/namespaces/tier.js +7 -7
  169. package/sdk/dist/namespaces/tier.js.map +1 -1
  170. package/sdk/dist/namespaces/transfers.d.ts +74 -71
  171. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  172. package/sdk/dist/namespaces/transfers.js +31 -38
  173. package/sdk/dist/namespaces/transfers.js.map +1 -1
  174. package/sdk/dist/namespaces/vouchers.d.ts +9 -10
  175. package/sdk/dist/namespaces/vouchers.d.ts.map +1 -1
  176. package/sdk/dist/namespaces/vouchers.js +4 -5
  177. package/sdk/dist/namespaces/vouchers.js.map +1 -1
  178. package/sdk/dist/namespaces/wallets.d.ts +72 -3
  179. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  180. package/sdk/dist/namespaces/wallets.js +141 -3
  181. package/sdk/dist/namespaces/wallets.js.map +1 -1
  182. package/sdk/dist/namespaces/write-approval.d.ts +101 -0
  183. package/sdk/dist/namespaces/write-approval.d.ts.map +1 -0
  184. package/sdk/dist/namespaces/write-approval.js +62 -0
  185. package/sdk/dist/namespaces/write-approval.js.map +1 -0
  186. package/sdk/dist/node/actions-node.d.ts +3 -3
  187. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  188. package/sdk/dist/node/actions-node.js +36 -36
  189. package/sdk/dist/node/actions-node.js.map +1 -1
  190. package/sdk/dist/node/{bearer-claim-key.d.ts → bearer-redeem-key.d.ts} +12 -12
  191. package/sdk/dist/node/bearer-redeem-key.d.ts.map +1 -0
  192. package/sdk/dist/node/{bearer-claim-key.js → bearer-redeem-key.js} +21 -21
  193. package/sdk/dist/node/bearer-redeem-key.js.map +1 -0
  194. package/sdk/dist/node/ci.d.ts +1 -1
  195. package/sdk/dist/node/ci.d.ts.map +1 -1
  196. package/sdk/dist/node/ci.js +6 -6
  197. package/sdk/dist/node/ci.js.map +1 -1
  198. package/sdk/dist/node/credentials.d.ts +17 -16
  199. package/sdk/dist/node/credentials.d.ts.map +1 -1
  200. package/sdk/dist/node/credentials.js +28 -27
  201. package/sdk/dist/node/credentials.js.map +1 -1
  202. package/sdk/dist/node/funding-recovery.d.ts +1 -1
  203. package/sdk/dist/node/funding-recovery.d.ts.map +1 -1
  204. package/sdk/dist/node/funding-recovery.js +1 -1
  205. package/sdk/dist/node/funding-recovery.js.map +1 -1
  206. package/sdk/dist/node/gitvault-apply.d.ts +1 -1
  207. package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
  208. package/sdk/dist/node/gitvault-handoff.d.ts +22 -22
  209. package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
  210. package/sdk/dist/node/gitvault-handoff.js +45 -45
  211. package/sdk/dist/node/gitvault-handoff.js.map +1 -1
  212. package/sdk/dist/node/gitvault-prewarm.js +7 -7
  213. package/sdk/dist/node/gitvault-prewarm.js.map +1 -1
  214. package/sdk/dist/node/gitvault-restore.d.ts +1 -1
  215. package/sdk/dist/node/gitvault-restore.js +1 -1
  216. package/sdk/dist/node/index.d.ts +16 -16
  217. package/sdk/dist/node/index.d.ts.map +1 -1
  218. package/sdk/dist/node/index.js +15 -15
  219. package/sdk/dist/node/index.js.map +1 -1
  220. package/sdk/dist/node/org-adopt.d.ts +49 -0
  221. package/sdk/dist/node/org-adopt.d.ts.map +1 -0
  222. package/sdk/dist/node/org-adopt.js +77 -0
  223. package/sdk/dist/node/org-adopt.js.map +1 -0
  224. package/sdk/dist/node/paid-fetch.d.ts +7 -7
  225. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  226. package/sdk/dist/node/paid-fetch.js +54 -54
  227. package/sdk/dist/node/paid-fetch.js.map +1 -1
  228. package/sdk/dist/node/preflight.d.ts +1 -1
  229. package/sdk/dist/node/preflight.d.ts.map +1 -1
  230. package/sdk/dist/node/preflight.js +1 -1
  231. package/sdk/dist/node/preflight.js.map +1 -1
  232. package/sdk/dist/node/source-scan.d.ts +1 -1
  233. package/sdk/dist/node/source-scan.js +2 -2
  234. package/sdk/dist/node/source-scan.js.map +1 -1
  235. package/sdk/dist/scoped.d.ts +3 -3
  236. package/sdk/dist/scoped.d.ts.map +1 -1
  237. package/sdk/dist/scoped.js +2 -2
  238. package/sdk/dist/scoped.js.map +1 -1
  239. package/core-dist/operator-session.js +0 -154
  240. package/lib/allowance.mjs +0 -355
  241. package/lib/operator-proofs.mjs +0 -22
  242. package/lib/operator.mjs +0 -771
  243. package/sdk/core-dist/operator-session.js +0 -154
  244. package/sdk/dist/namespaces/allowance.d.ts +0 -79
  245. package/sdk/dist/namespaces/allowance.d.ts.map +0 -1
  246. package/sdk/dist/namespaces/allowance.js +0 -149
  247. package/sdk/dist/namespaces/allowance.js.map +0 -1
  248. package/sdk/dist/namespaces/operator-session.d.ts.map +0 -1
  249. package/sdk/dist/namespaces/operator-session.js.map +0 -1
  250. package/sdk/dist/namespaces/operator.d.ts +0 -359
  251. package/sdk/dist/namespaces/operator.d.ts.map +0 -1
  252. package/sdk/dist/namespaces/operator.js +0 -272
  253. package/sdk/dist/namespaces/operator.js.map +0 -1
  254. package/sdk/dist/node/bearer-claim-key.d.ts.map +0 -1
  255. package/sdk/dist/node/bearer-claim-key.js.map +0 -1
  256. package/sdk/dist/node/operator-claim.d.ts +0 -49
  257. package/sdk/dist/node/operator-claim.d.ts.map +0 -1
  258. package/sdk/dist/node/operator-claim.js +0 -77
  259. package/sdk/dist/node/operator-claim.js.map +0 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Shared machinery for a person's command-line sign-in: `run402 login`,
3
+ * `run402 logout`, `run402 whoami`, and `run402 approve`.
4
+ *
5
+ * One sign-in session (wire token class `control_plane_session`), bound to the
6
+ * principal and graded by how it was minted:
7
+ * - `run402 login` — loopback PKCE (RFC 8252): the browser runs the passkey
8
+ * ceremony and redirects to a 127.0.0.1 listener. Grade `loopback`: full and
9
+ * step-up-able.
10
+ * - `run402 login --device` — RFC 8628 device authorization for a terminal
11
+ * with no browser. Grade `device`: read-only (every mutation answers
12
+ * `SESSION_READ_ONLY`).
13
+ *
14
+ * Provisioning, deploying, and writing secrets without a wallet additionally
15
+ * need a **write approval** — passkey-signed, scoped to one (action, target),
16
+ * minted by `run402 approve` and sent as `X-Run402-Write-Approval`. A write
17
+ * approval never satisfies step-up.
18
+ *
19
+ * The session and approvals are cached at the BASE config dir (principal-
20
+ * scoped, shared across named wallets), mode 0600. Agent-first: JSON to stdout;
21
+ * the URL and code a person must open go to stderr.
22
+ */
23
+
24
+ import { spawn } from "node:child_process";
25
+ import { createServer } from "node:http";
26
+ import { randomBytes, createHash } from "node:crypto";
27
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
28
+ import { getSdk } from "./sdk.mjs";
29
+ import { isWriteApprovalRequired } from "#sdk/node";
30
+ import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
31
+ import {
32
+ saveApproval,
33
+ hashControlPlaneSession,
34
+ approvalFromTokenResponse,
35
+ } from "../core-dist/write-approvals.js";
36
+ import { getApiBase } from "../core-dist/config.js";
37
+
38
+ /** Gateway write-approval capabilities → required target scope. */
39
+ export const APPROVAL_ACTIONS = {
40
+ "org.project.create": "org",
41
+ "project.deploy": "project",
42
+ "project.secret.write": "project",
43
+ };
44
+
45
+ /** True when `url` is same-origin as the API base (confirm_url validation). */
46
+ function sameApiOrigin(url) {
47
+ try {
48
+ return new URL(url).origin === new URL(getApiBase()).origin;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Best-effort, cross-platform browser open. Never throws. */
55
+ export function openBrowser(url) {
56
+ try {
57
+ let cmd;
58
+ let cmdArgs;
59
+ if (process.platform === "darwin") {
60
+ cmd = "open";
61
+ cmdArgs = [url];
62
+ } else if (process.platform === "win32") {
63
+ cmd = "cmd";
64
+ cmdArgs = ["/c", "start", "", url];
65
+ } else {
66
+ cmd = "xdg-open";
67
+ cmdArgs = [url];
68
+ }
69
+ const child = spawn(cmd, cmdArgs, { stdio: "ignore", detached: true });
70
+ child.on("error", () => {}); // ignore: the URL is also printed to stderr
71
+ child.unref();
72
+ } catch {
73
+ // Best-effort only — the person can always copy the printed URL.
74
+ }
75
+ }
76
+
77
+ /** Output shape for a cached sign-in session. NEVER includes the token. */
78
+ export function sessionView(session, nowMs = Date.now()) {
79
+ return {
80
+ logged_in: true,
81
+ grade: session.grade,
82
+ read_only: session.grade === "device",
83
+ principal_id: session.principal_id || null,
84
+ amr: session.amr,
85
+ expires_at: new Date(session.expires_at).toISOString(),
86
+ expires_in_seconds: Math.max(0, Math.round((session.expires_at - nowMs) / 1000)),
87
+ };
88
+ }
89
+
90
+ const base64url = (buf) => buf.toString("base64url");
91
+
92
+ /** Generate PKCE (S256) + CSRF state + replay nonce for the loopback flow. */
93
+ export function pkce() {
94
+ const codeVerifier = base64url(randomBytes(32));
95
+ const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
96
+ return { codeVerifier, codeChallenge, state: base64url(randomBytes(16)), nonce: base64url(randomBytes(16)) };
97
+ }
98
+
99
+ /**
100
+ * Start a 127.0.0.1 loopback server (RFC 8252) that captures exactly one
101
+ * redirect. Returns the bound port (via `ready`), a promise for the auth code,
102
+ * and a `close()`. State is validated here to reject CSRF before exchange.
103
+ */
104
+ export function startLoopbackServer({ expectedState, timeoutMs }) {
105
+ let resolveCode;
106
+ let rejectCode;
107
+ const codePromise = new Promise((res, rej) => {
108
+ resolveCode = res;
109
+ rejectCode = rej;
110
+ });
111
+ let timer;
112
+ // Track live sockets: server.close() alone stops NEW connections but leaves
113
+ // the browser's keep-alive socket open, which keeps Node's event loop alive
114
+ // and hangs the CLI after a successful login. close() must destroy them.
115
+ const sockets = new Set();
116
+ const server = createServer((req, res) => {
117
+ let u;
118
+ try {
119
+ u = new URL(req.url, "http://127.0.0.1");
120
+ } catch {
121
+ res.writeHead(400).end("bad request");
122
+ return;
123
+ }
124
+ if (u.pathname !== "/callback") {
125
+ res.writeHead(404).end("not found");
126
+ return;
127
+ }
128
+ const code = u.searchParams.get("code");
129
+ const gotState = u.searchParams.get("state");
130
+ const errParam = u.searchParams.get("error");
131
+ // `connection: close` so the browser does not keep the socket alive.
132
+ res.writeHead(200, { "content-type": "text/html", connection: "close" });
133
+ res.end(
134
+ "<!doctype html><html><body style=\"font-family:system-ui;padding:3rem\">" +
135
+ "<h2>run402 - you're signed in.</h2><p>You can close this window and return to your terminal.</p></body></html>",
136
+ );
137
+ // Do NOT tear down here — let the response flush. The caller calls close()
138
+ // once it has the code (or on any failure path).
139
+ if (errParam) rejectCode(new Error(`authorization error: ${errParam}`));
140
+ else if (!code) rejectCode(new Error("no authorization code on the loopback redirect"));
141
+ else if (gotState !== expectedState) rejectCode(new Error("state mismatch on the loopback redirect (possible CSRF) - aborted"));
142
+ else resolveCode(code);
143
+ });
144
+ server.on("connection", (s) => {
145
+ sockets.add(s);
146
+ s.once("close", () => sockets.delete(s));
147
+ });
148
+ function close() {
149
+ clearTimeout(timer);
150
+ for (const s of sockets) {
151
+ try {
152
+ s.destroy();
153
+ } catch {
154
+ // already gone
155
+ }
156
+ }
157
+ try {
158
+ server.close();
159
+ } catch {
160
+ // already closing
161
+ }
162
+ }
163
+ timer = setTimeout(() => {
164
+ close();
165
+ rejectCode(new Error("timed out waiting for browser approval"));
166
+ }, timeoutMs);
167
+ server.on("error", (e) => {
168
+ close();
169
+ rejectCode(e);
170
+ });
171
+ const ready = new Promise((res, rej) => {
172
+ server.once("error", rej);
173
+ server.listen(0, "127.0.0.1", () => res(server.address().port));
174
+ });
175
+ return { ready, codePromise, close };
176
+ }
177
+
178
+ /**
179
+ * Run the write-approval ceremony (loopback + passkey) and cache the minted
180
+ * approval. Requires a live, write-capable sign-in session. Returns the cached
181
+ * approval (or exits non-zero via fail/reportSdkError). Reused by
182
+ * `run402 approve` AND the TTY auto-approve path in provision/deploy.
183
+ */
184
+ export async function mintApproval({ action, orgId, projectId, noOpen = false }) {
185
+ const cp = loadLiveControlPlaneSession();
186
+ if (!cp) {
187
+ return fail({
188
+ code: "LOGIN_REQUIRED",
189
+ message: "No sign-in session. Run 'run402 login' first, then approve.",
190
+ hint: "A write approval is minted on top of your sign-in session.",
191
+ next_actions: [{ type: "authenticate", command: "run402 login" }],
192
+ });
193
+ }
194
+ if (cp.grade === "device") {
195
+ return fail({
196
+ code: "SESSION_READ_ONLY",
197
+ message: "This sign-in session came from 'run402 login --device' and can only read; it cannot mint a write approval.",
198
+ hint: "Run 'run402 login' (the browser passkey sign-in), then approve.",
199
+ next_actions: [{ type: "authenticate", command: "run402 login" }],
200
+ });
201
+ }
202
+ const sdk = getSdk();
203
+ const { codeVerifier, codeChallenge, state } = pkce();
204
+ const { ready, codePromise, close } = startLoopbackServer({ expectedState: state, timeoutMs: 300_000 });
205
+
206
+ let port;
207
+ try {
208
+ port = await ready;
209
+ } catch (err) {
210
+ close();
211
+ return fail({ code: "APPROVE_FAILED", message: `Could not start the loopback server: ${err.message}` });
212
+ }
213
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
214
+
215
+ let challenge;
216
+ try {
217
+ challenge = await sdk.writeApproval.requestChallenge({
218
+ action,
219
+ orgId: orgId ?? undefined,
220
+ projectId: projectId ?? undefined,
221
+ cliRedirectUri: redirectUri,
222
+ codeChallenge,
223
+ state,
224
+ token: cp.control_plane_session_token,
225
+ });
226
+ } catch (err) {
227
+ close();
228
+ return reportSdkError(err);
229
+ }
230
+
231
+ const confirmUrl = challenge?.confirm_url;
232
+ if (!confirmUrl || !sameApiOrigin(confirmUrl)) {
233
+ close();
234
+ return fail({
235
+ code: "APPROVE_BAD_CONFIRM_URL",
236
+ message: "The gateway returned a confirm_url that is not same-origin as the API base; aborting.",
237
+ });
238
+ }
239
+ const targetLabel = orgId ? `org ${orgId}` : projectId ? `project ${projectId}` : "(no target)";
240
+ process.stderr.write(
241
+ `\nTo approve '${action}' for ${targetLabel}, open and approve with your passkey:\n ${confirmUrl}\n\n`,
242
+ );
243
+ if (!noOpen && process.stderr.isTTY) {
244
+ openBrowser(confirmUrl);
245
+ process.stderr.write("(opening your browser…)\n\n");
246
+ }
247
+ process.stderr.write("Waiting for passkey approval…\n");
248
+
249
+ let code;
250
+ try {
251
+ code = await codePromise;
252
+ } catch (err) {
253
+ close();
254
+ return fail({ code: "APPROVE_FAILED", message: err.message, hint: "Run 'run402 approve' to try again." });
255
+ }
256
+
257
+ let token;
258
+ try {
259
+ token = await sdk.writeApproval.exchangeClaimCode({ code, codeVerifier, state });
260
+ } catch (err) {
261
+ close();
262
+ return reportSdkError(err);
263
+ }
264
+ close();
265
+
266
+ const approval = approvalFromTokenResponse(token, {
267
+ action,
268
+ target: { org_id: orgId ?? undefined, project_id: projectId ?? undefined },
269
+ apiOrigin: new URL(getApiBase()).origin,
270
+ controlPlaneSessionHash: hashControlPlaneSession(cp.control_plane_session_token),
271
+ controlPlanePrincipalId: cp.principal_id || "",
272
+ });
273
+ saveApproval(approval);
274
+ process.stderr.write(`\nApproved '${action}' for ${targetLabel}.\n`);
275
+ return approval;
276
+ }
277
+
278
+ /**
279
+ * Run a write action; on `WriteApprovalRequiredError` AND an interactive TTY
280
+ * (CLI), run the scoped approval ceremony and retry once. In MCP / CI /
281
+ * non-TTY the typed error is rethrown unchanged — the agent relays the resolved
282
+ * `run402 approve …` command rather than a browser opening unexpectedly.
283
+ */
284
+ export async function withAutoApprove(fn) {
285
+ try {
286
+ return await fn();
287
+ } catch (err) {
288
+ if (!isWriteApprovalRequired(err) || !process.stderr.isTTY) throw err;
289
+ const action = err.capability;
290
+ const target = err.target || {};
291
+ if (!action) throw err;
292
+ process.stderr.write(`\nThis '${action}' needs a one-time write approval. Opening the browser to approve…\n`);
293
+ await mintApproval({ action, orgId: target.org_id ?? undefined, projectId: target.project_id ?? undefined });
294
+ return await fn(); // retry once with the now-cached approval
295
+ }
296
+ }
package/lib/sites.mjs CHANGED
@@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"
2
2
  import { tmpdir } from "os";
3
3
  import { dirname, join, resolve } from "path";
4
4
  import { fileSetFromDir, delegateTokenFromEnv } from "#sdk/node";
5
- import { allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
5
+ import { walletAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
6
6
  import { resolveFilePathsInManifest } from "./manifest.mjs";
7
7
  import { getSdk } from "./sdk.mjs";
8
8
  import { reportSdkError, fail } from "./sdk-errors.mjs";
@@ -64,7 +64,7 @@ Notes:
64
64
  - Progress events are emitted as JSON-line objects on stderr by default
65
65
  (one object per line). Final result envelope goes to stdout. Pass --quiet
66
66
  to silence stderr.
67
- - Free with active tier - requires allowance auth
67
+ - Free with active tier - requires SIWX auth
68
68
  `;
69
69
 
70
70
  const SUB_HELP = {
@@ -90,7 +90,7 @@ Manifest format (JSON):
90
90
 
91
91
  Notes:
92
92
  - Must include at least index.html in the files array
93
- - Free with active tier - requires allowance auth
93
+ - Free with active tier - requires SIWX auth
94
94
 
95
95
  Examples:
96
96
  run402 sites deploy --manifest site.json
@@ -139,7 +139,7 @@ Progress events:
139
139
  Notes:
140
140
  - Re-deploying an unchanged tree makes no S3 PUTs (returns immediately
141
141
  with bytes_uploaded: 0)
142
- - Free with active tier - requires allowance auth
142
+ - Free with active tier - requires SIWX auth
143
143
 
144
144
  Examples:
145
145
  run402 sites deploy-dir ./dist --project prj_abc
@@ -219,10 +219,10 @@ async function deploy(args) {
219
219
  const manifest = JSON.parse(raw);
220
220
  if (opts.manifest) resolveFilePathsInManifest(manifest, dirname(resolve(opts.manifest)));
221
221
 
222
- // Preserve the aggressive early exit when no allowance is configured.
222
+ // Preserve the aggressive early exit when no local wallet is configured.
223
223
  // A delegate 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()) allowanceAuthHeaders("/apply/v1/plans");
225
+ if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/plans");
226
226
 
227
227
  const stage = stageFilesToTempDir(manifest.files || []);
228
228
  try {
@@ -288,10 +288,10 @@ async function deployDir(args) {
288
288
  const projectId = resolveProjectId(opts.project);
289
289
  if (opts.target !== undefined) failUnsupportedTarget();
290
290
 
291
- // Preserve the aggressive early exit when no allowance is configured.
291
+ // Preserve the aggressive early exit when no local wallet is configured.
292
292
  // A delegate 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()) allowanceAuthHeaders("/apply/v1/plans");
294
+ if (!delegateTokenFromEnv()) walletAuthHeaders("/apply/v1/plans");
295
295
 
296
296
  let fileSet;
297
297
  try {
package/lib/stats.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * line per request to stderr, automatically, for every SDK caller (CLI, MCP,
9
9
  * `git-remote-run402`). This module is the thin CLI-edge reader of that
10
10
  * per-instance accumulator: it turns `sdk.stats()` into the `stats` envelope
11
- * field every `repos` verb's result and `deploy apply`'s final result carry
11
+ * field every `repos` verb's result and `deploy`'s final result carry
12
12
  * (always on), plus the `-v`/`--verbose` stderr summary line.
13
13
  *
14
14
  * `sdk.stats()` reflects only calls made through the ONE `Run402` instance
@@ -47,7 +47,7 @@ function writeVerboseStatsLine(sdk) {
47
47
  /**
48
48
  * One stderr summary line, only when verbose was requested. Coexists with
49
49
  * `--human`. `verbose` is either normalized argv (checked for `-v`/
50
- * `--verbose`) or a plain boolean, for callers (e.g. `deploy apply`) that
50
+ * `--verbose`) or a plain boolean, for callers (e.g. `deploy`) that
51
51
  * already parsed their own flags into an options object.
52
52
  */
53
53
  export function printVerboseStats(verbose, sdk) {
package/lib/status.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readRemoteStatus } from "#sdk/node";
2
2
  import {
3
- readAllowance,
3
+ readWallet,
4
4
  loadKeyStore,
5
5
  getActiveProjectId,
6
6
  apiBase,
@@ -21,8 +21,8 @@ Usage:
21
21
  Displays:
22
22
  - Wallet identity (local_label, server_label, address)
23
23
  - Payment rail (x402 | mpp)
24
- - Balances (on_chain_usd_micros + on_chain_token, prepaid_credit_usd_micros, held_usd_micros)
25
- - Tier subscription (name, status, expiry)
24
+ - Balances (on_chain_usd_micros + on_chain_token, allowance_usd_micros, held_usd_micros)
25
+ - Tier and lease (name, status, expiry)
26
26
  - Projects (from server, with fallback to local keystore)
27
27
  - Active project ID
28
28
  - Active API target
@@ -31,11 +31,11 @@ Output is JSON by default. --json is accepted as a compatibility no-op.
31
31
  --human renders a compact human summary instead (wallet, API target, tier,
32
32
  active project, balance, and the next action); it cannot be combined with
33
33
  --json.
34
- Run402 Cloud status requires an allowance; Core target status can still report
34
+ Run402 Cloud status requires a wallet; Core target status can still report
35
35
  local project state without one.
36
36
  `;
37
37
 
38
- // USDC / pathUSD constants (match allowance.mjs)
38
+ // USDC / pathUSD constants (match wallet.mjs)
39
39
  const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
40
40
  const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
41
41
  const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
@@ -105,13 +105,13 @@ export async function run(args = []) {
105
105
  hint: "JSON is the default; drop --json to keep it, or keep --human alone for the rendered view.",
106
106
  });
107
107
  }
108
- const allowance = readAllowance();
108
+ const localWallet = readWallet();
109
109
  const target = {
110
110
  api_base: apiBase(),
111
111
  api_base_source: apiBaseSource(),
112
112
  kind: apiTargetKind(),
113
113
  };
114
- if (!allowance) {
114
+ if (!localWallet) {
115
115
  const store = loadKeyStore();
116
116
  const bare = {
117
117
  wallet: null,
@@ -128,16 +128,16 @@ export async function run(args = []) {
128
128
  return;
129
129
  }
130
130
 
131
- const wallet = allowance.address.toLowerCase();
132
- const rail = allowance.rail || "x402";
131
+ const wallet = localWallet.address.toLowerCase();
132
+ const rail = localWallet.rail || "x402";
133
133
 
134
- // Parallel API calls: tier + billing balance + server-side projects + on-chain wallet balance.
134
+ // Parallel API calls: tier + allowance + server-side projects + on-chain wallet balance.
135
135
  // projects.list() is the membership-scoped named inventory (project-findability);
136
- // SIWX wallet auth is signed from the allowance. Best-effort — a missing
137
- // allowance yields null and we fall back to the local keystore below.
136
+ // SIWX wallet auth is signed from the wallet. Best-effort — a missing
137
+ // wallet yields null and we fall back to the local keystore below.
138
138
  const [status, walletBalance] = await Promise.all([
139
139
  readRemoteStatus(getSdk(), wallet),
140
- readWalletBalanceUsdMicros(rail, allowance.address),
140
+ readWalletBalanceUsdMicros(rail, localWallet.address),
141
141
  ]);
142
142
  const { tier, billing, remote } = status;
143
143
 
@@ -155,18 +155,18 @@ export async function run(args = []) {
155
155
  const walletName = getActiveProfile();
156
156
  const walletMeta = readMeta(walletName);
157
157
 
158
- // Balances are grouped under one object so the on-chain and
159
- // prepaid-credit numbers are unambiguous and rail-legible.
158
+ // Balances are grouped under one object so the on-chain (wallet) and
159
+ // allowance numbers are unambiguous and rail-legible.
160
160
  // - on_chain_usd_micros / on_chain_token: on-chain USDC (x402) or pathUSD
161
- // (mpp), null if the RPC read failed
162
- // - prepaid_credit_usd_micros / held_usd_micros: Run402-held credits,
163
- // rail-independent, null when no organization exists
161
+ // (mpp) in the wallet, null if the RPC read failed
162
+ // - allowance_usd_micros / held_usd_micros: the organization's Run402-held
163
+ // allowance, rail-independent, null when no organization exists
164
164
  const hasBilling = billing && billing.exists !== false;
165
165
  const result = {
166
166
  wallet: {
167
167
  local_label: walletName,
168
168
  server_label: walletMeta?.label ?? null,
169
- address: allowance.address,
169
+ address: localWallet.address,
170
170
  },
171
171
  rail,
172
172
  remote_status: status.availability,
@@ -175,7 +175,7 @@ export async function run(args = []) {
175
175
  balances: {
176
176
  on_chain_usd_micros: walletBalance,
177
177
  on_chain_token: rail === "mpp" ? "pathUSD" : "USDC",
178
- prepaid_credit_usd_micros: hasBilling ? billing.available_usd_micros : null,
178
+ allowance_usd_micros: hasBilling ? billing.allowance_usd_micros : null,
179
179
  held_usd_micros: hasBilling ? (billing.held_usd_micros ?? 0) : null,
180
180
  },
181
181
  tier: tier && tier.tier
@@ -205,7 +205,7 @@ function usdFromMicros(micros) {
205
205
 
206
206
  /**
207
207
  * The one next action for the human view, derived from the same state the
208
- * JSON carries. Mirrors `init`: `run402 up -y` subscribes the prototype tier
208
+ * JSON carries. Mirrors `init`: `run402 up -y` sets the prototype tier
209
209
  * itself as part of the first deploy, so a tier-less account gets ONE command.
210
210
  */
211
211
  function statusNextAction(result) {
@@ -214,7 +214,7 @@ function statusNextAction(result) {
214
214
  }
215
215
  if (result.remote_status?.tier?.state === "unavailable") return "Check connectivity/authentication and retry run402 status; tier state is unavailable.";
216
216
  if (!result.tier) {
217
- return "run402 up -y (subscribes the prototype tier, free on testnet, as part of the first deploy; or: run402 tier set prototype)";
217
+ return "run402 up -y (sets the prototype tier, free on testnet, as part of the first deploy; or: run402 tier set prototype)";
218
218
  }
219
219
  if (!result.active_project) {
220
220
  return "run402 up --name <name> -y (or select an existing project: run402 projects use <project_id>)";
@@ -230,7 +230,7 @@ function statusNextAction(result) {
230
230
  export function formatStatusHuman(result) {
231
231
  const lines = [];
232
232
  if (!result.wallet) {
233
- lines.push("Wallet: none (no allowance on this machine)");
233
+ lines.push("Wallet: none (no local wallet on this machine)");
234
234
  } else {
235
235
  const label = result.wallet.server_label
236
236
  ? `${result.wallet.local_label} (${result.wallet.server_label})`
@@ -262,8 +262,8 @@ export function formatStatusHuman(result) {
262
262
  const parts = [];
263
263
  const onChain = usdFromMicros(result.balances.on_chain_usd_micros);
264
264
  if (onChain) parts.push(`${onChain} ${result.balances.on_chain_token} on-chain`);
265
- const credit = usdFromMicros(result.balances.prepaid_credit_usd_micros);
266
- if (credit) parts.push(`${credit} prepaid credit`);
265
+ const allowance = usdFromMicros(result.balances.allowance_usd_micros);
266
+ if (allowance) parts.push(`${allowance} allowance`);
267
267
  if (parts.length) lines.push(`Balance: ${parts.join(", ")}`);
268
268
  }
269
269
  lines.push(`Next: ${statusNextAction(result)}`);
@@ -9,16 +9,16 @@ Usage:
9
9
  run402 subdomains <subcommand> [args...]
10
10
 
11
11
  Subcommands:
12
- claim <name> [--project <id>] [--release <id> | --deployment <id>] Claim a subdomain
12
+ add <name> [--project <project_id>] [--release <release_id> | --deployment <deployment_id>] Add a subdomain
13
13
  delete <name> --confirm [--project <id>] Release a subdomain. Requires --confirm.
14
14
  list [--project <id>] List subdomains for a project
15
15
 
16
16
  Options default to the active project. With neither --release nor
17
- --deployment, claim binds the project's live release.
17
+ --deployment, add binds the project's live release.
18
18
 
19
19
  Examples:
20
- run402 subdomains claim myapp
21
- run402 subdomains claim myapp --release rel_abc123 --project prj_abc123
20
+ run402 subdomains add myapp
21
+ run402 subdomains add myapp --release rel_abc123 --project prj_abc123
22
22
  run402 subdomains delete myapp --confirm
23
23
  run402 subdomains list
24
24
 
@@ -29,10 +29,10 @@ Notes:
29
29
  `;
30
30
 
31
31
  const SUB_HELP = {
32
- claim: `run402 subdomains claim — Claim a custom subdomain for a release
32
+ add: `run402 subdomains add — Add a subdomain for a release
33
33
 
34
34
  Usage:
35
- run402 subdomains claim <name> [--project <id>] [--release <id> | --deployment <id>]
35
+ run402 subdomains add <name> [--project <project_id>] [--release <release_id> | --deployment <deployment_id>]
36
36
 
37
37
  Arguments:
38
38
  <name> Subdomain name (3-63 chars, lowercase alphanumeric +
@@ -51,10 +51,10 @@ Notes:
51
51
  - A deploy manifest can declare the same thing: "subdomains": { "set": ["<name>"] }
52
52
 
53
53
  Examples:
54
- run402 subdomains claim myapp
55
- run402 subdomains claim myapp --release rel_abc123 --project prj_abc123
54
+ run402 subdomains add myapp
55
+ run402 subdomains add myapp --release rel_abc123 --project prj_abc123
56
56
  `,
57
- list: `run402 subdomains list — List subdomains claimed by a project
57
+ list: `run402 subdomains list — List a project's subdomains
58
58
 
59
59
  Usage:
60
60
  run402 subdomains list [--project <id>]
@@ -66,7 +66,7 @@ Examples:
66
66
  run402 subdomains list
67
67
  run402 subdomains list --project prj_abc123
68
68
  `,
69
- delete: `run402 subdomains delete — Release a claimed subdomain
69
+ delete: `run402 subdomains delete — Release a subdomain
70
70
 
71
71
  Usage:
72
72
  run402 subdomains delete <name> --confirm [--project <id>]
@@ -76,7 +76,7 @@ Arguments:
76
76
 
77
77
  Options:
78
78
  --confirm Required: releasing a subdomain is irreversible and
79
- makes it available for any other project to claim
79
+ makes it available for any other project to add
80
80
  --project <id> Project ID (defaults to the active project)
81
81
 
82
82
  Examples:
@@ -85,7 +85,7 @@ Examples:
85
85
  `,
86
86
  };
87
87
 
88
- async function claim(args) {
88
+ async function add(args) {
89
89
  const parsedArgs = normalizeArgv(args);
90
90
  const valueFlags = ["--project", "--deployment", "--release"];
91
91
  assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
@@ -99,8 +99,8 @@ async function claim(args) {
99
99
  if (positionals.length > 1) {
100
100
  fail({
101
101
  code: "BAD_USAGE",
102
- message: `Unexpected argument for subdomains claim: ${positionals[1]}`,
103
- hint: "Use `run402 subdomains claim <name> [--release <rel_id>]`.",
102
+ message: `Unexpected argument for subdomains add: ${positionals[1]}`,
103
+ hint: "Use `run402 subdomains add <name> [--release <rel_id>]`.",
104
104
  });
105
105
  }
106
106
  if (positionals.length === 1) {
@@ -110,14 +110,14 @@ async function claim(args) {
110
110
  fail({
111
111
  code: "BAD_USAGE",
112
112
  message: "Missing <name>.",
113
- hint: "run402 subdomains claim <name> [--project <id>] [--release <id> | --deployment <id>]",
113
+ hint: "run402 subdomains add <name> [--project <project_id>] [--release <release_id> | --deployment <deployment_id>]",
114
114
  });
115
115
  }
116
116
  if (opts.deployment && opts.release) {
117
117
  fail({
118
118
  code: "BAD_USAGE",
119
119
  message: "Pass either --release or --deployment, not both.",
120
- hint: "run402 subdomains claim <name> [--release <id> | --deployment <id>]",
120
+ hint: "run402 subdomains add <name> [--release <id> | --deployment <id>]",
121
121
  });
122
122
  }
123
123
  const projectId = resolveProjectId(opts.project);
@@ -129,7 +129,7 @@ async function claim(args) {
129
129
  const deploymentId = opts.deployment || (opts.release ? undefined : p.last_deployment_id) || undefined;
130
130
  const releaseId = opts.release || undefined;
131
131
  try {
132
- const data = await getSdk().subdomains.claim({ name, deploymentId, releaseId, projectId });
132
+ const data = await getSdk().subdomains.add({ name, deploymentId, releaseId, projectId });
133
133
  console.log(JSON.stringify(data, null, 2));
134
134
  } catch (err) {
135
135
  reportSdkError(err);
@@ -156,7 +156,7 @@ async function deleteSubdomain(allArgs) {
156
156
  if (!argList.includes("--confirm")) {
157
157
  fail({
158
158
  code: "CONFIRMATION_REQUIRED",
159
- message: `Destructive: releasing subdomain '${name}' makes it available for any other project to claim. This is irreversible. Re-run with --confirm to proceed.`,
159
+ message: `Destructive: releasing subdomain '${name}' makes it available for any other project to add. This is irreversible. Re-run with --confirm to proceed.`,
160
160
  details: { name },
161
161
  });
162
162
  }
@@ -202,7 +202,7 @@ export async function run(sub, args) {
202
202
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
203
203
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
204
204
  switch (sub) {
205
- case "claim": await claim(args); break;
205
+ case "add": await add(args); break;
206
206
  case "delete": await deleteSubdomain(args); break;
207
207
  case "list": await list(args); break;
208
208
  default: