run402 4.93.2 → 4.95.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 (316) hide show
  1. package/README.md +61 -59
  2. package/cli.mjs +40 -47
  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 +2 -2
  11. package/lib/admin.mjs +6 -6
  12. package/lib/agent.mjs +20 -18
  13. package/lib/ai.mjs +5 -5
  14. package/lib/approve.mjs +81 -0
  15. package/lib/apps.mjs +12 -12
  16. package/lib/archives.mjs +371 -8
  17. package/lib/argparse.mjs +5 -5
  18. package/lib/assets.mjs +14 -14
  19. package/lib/auth.mjs +34 -34
  20. package/lib/billing.mjs +6 -6
  21. package/lib/branches.mjs +7 -7
  22. package/lib/buzz-doctor.mjs +2 -2
  23. package/lib/buzz-notifications.mjs +5 -5
  24. package/lib/buzz.mjs +3 -3
  25. package/lib/buzz.test.mjs +1 -1
  26. package/lib/cache.mjs +1 -1
  27. package/lib/cdn.mjs +2 -2
  28. package/lib/ci.mjs +12 -12
  29. package/lib/claims.mjs +2 -2
  30. package/lib/cold-start.mjs +21 -21
  31. package/lib/command-manifest.mjs +55 -70
  32. package/lib/config.mjs +28 -28
  33. package/lib/contacts.mjs +16 -16
  34. package/lib/contracts.mjs +12 -12
  35. package/lib/credentials.mjs +33 -33
  36. package/lib/daemon-path.mjs +1 -1
  37. package/lib/delegates.mjs +14 -14
  38. package/lib/deliveries.mjs +6 -6
  39. package/lib/deploy-rehearse-discovery.test.mjs +7 -7
  40. package/lib/deploy-v2.mjs +241 -193
  41. package/lib/deploy.mjs +21 -53
  42. package/lib/dev.mjs +3 -3
  43. package/lib/doctor-source-scan.mjs +1 -1
  44. package/lib/doctor.mjs +63 -63
  45. package/lib/domains.mjs +27 -27
  46. package/lib/email.mjs +38 -38
  47. package/lib/errors.mjs +40 -24
  48. package/lib/escalations.mjs +5 -5
  49. package/lib/events.mjs +32 -25
  50. package/lib/feedback.mjs +12 -12
  51. package/lib/functions.mjs +34 -34
  52. package/lib/gitvault-daemon.mjs +1 -1
  53. package/lib/gitvault-scaffold.mjs +2 -2
  54. package/lib/gitvault.mjs +2 -2
  55. package/lib/grants.mjs +7 -7
  56. package/lib/image.mjs +10 -10
  57. package/lib/init.mjs +69 -69
  58. package/lib/jobs.mjs +15 -15
  59. package/lib/lightning-wallet.mjs +28 -28
  60. package/lib/live.mjs +1 -1
  61. package/lib/login.mjs +207 -0
  62. package/lib/logout.mjs +53 -0
  63. package/lib/logs.mjs +8 -8
  64. package/lib/messages.mjs +5 -5
  65. package/lib/next-actions.mjs +16 -16
  66. package/lib/org-context.mjs +13 -13
  67. package/lib/{org.mjs → orgs.mjs} +215 -167
  68. package/lib/pay.mjs +1 -1
  69. package/lib/projects.mjs +70 -84
  70. package/lib/redeem.mjs +7 -8
  71. package/lib/remote-helper-session.mjs +12 -12
  72. package/lib/repos.mjs +83 -83
  73. package/lib/rooms.mjs +105 -69
  74. package/lib/sdk-errors.mjs +10 -5
  75. package/lib/sdk.mjs +3 -3
  76. package/lib/secrets.mjs +13 -13
  77. package/lib/service.mjs +5 -5
  78. package/lib/session-proofs.mjs +21 -0
  79. package/lib/sign-in.mjs +296 -0
  80. package/lib/sites.mjs +22 -22
  81. package/lib/snapshots.mjs +10 -10
  82. package/lib/stats.mjs +2 -2
  83. package/lib/status.mjs +25 -25
  84. package/lib/subdomains.mjs +30 -30
  85. package/lib/subscriptions.mjs +10 -10
  86. package/lib/tier.mjs +29 -22
  87. package/lib/transfer.mjs +59 -86
  88. package/lib/up.mjs +14 -14
  89. package/lib/wallet-context.mjs +4 -4
  90. package/lib/wallet-context.test.mjs +3 -3
  91. package/lib/wallets.mjs +197 -32
  92. package/lib/webhook-secret.mjs +6 -6
  93. package/lib/webhooks.mjs +8 -8
  94. package/lib/whoami.mjs +102 -0
  95. package/package.json +1 -1
  96. package/sdk/core-dist/config.js +52 -18
  97. package/sdk/core-dist/control-plane-session.js +31 -6
  98. package/sdk/core-dist/keystore.js +7 -7
  99. package/sdk/core-dist/profiles.js +9 -7
  100. package/sdk/core-dist/{allowance-auth.js → wallet-auth.js} +15 -15
  101. package/{core-dist/allowance.js → sdk/core-dist/wallet.js} +20 -20
  102. package/{core-dist/write-auth-session.js → sdk/core-dist/write-approvals.js} +19 -19
  103. package/sdk/dist/actions.d.ts +4 -4
  104. package/sdk/dist/actions.d.ts.map +1 -1
  105. package/sdk/dist/app-up.d.ts +2 -2
  106. package/sdk/dist/app-up.d.ts.map +1 -1
  107. package/sdk/dist/config.d.ts +1 -2
  108. package/sdk/dist/config.d.ts.map +1 -1
  109. package/sdk/dist/config.js.map +1 -1
  110. package/sdk/dist/control-plane-credentials.d.ts +8 -8
  111. package/sdk/dist/control-plane-credentials.js +7 -7
  112. package/sdk/dist/credentials.d.ts +20 -20
  113. package/sdk/dist/credentials.d.ts.map +1 -1
  114. package/sdk/dist/credentials.js +1 -1
  115. package/sdk/dist/errors.d.ts +26 -28
  116. package/sdk/dist/errors.d.ts.map +1 -1
  117. package/sdk/dist/errors.js +35 -38
  118. package/sdk/dist/errors.js.map +1 -1
  119. package/sdk/dist/index.d.ts +29 -16
  120. package/sdk/dist/index.d.ts.map +1 -1
  121. package/sdk/dist/index.js +30 -17
  122. package/sdk/dist/index.js.map +1 -1
  123. package/sdk/dist/kernel.d.ts +1 -1
  124. package/sdk/dist/kernel.d.ts.map +1 -1
  125. package/sdk/dist/kernel.js +10 -9
  126. package/sdk/dist/kernel.js.map +1 -1
  127. package/sdk/dist/namespaces/admin.d.ts +52 -121
  128. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  129. package/sdk/dist/namespaces/admin.js +41 -45
  130. package/sdk/dist/namespaces/admin.js.map +1 -1
  131. package/sdk/dist/namespaces/agent.d.ts +2 -2
  132. package/sdk/dist/namespaces/agent.d.ts.map +1 -1
  133. package/sdk/dist/namespaces/agent.js +2 -2
  134. package/sdk/dist/namespaces/agent.js.map +1 -1
  135. package/sdk/dist/namespaces/ai.d.ts +2 -2
  136. package/sdk/dist/namespaces/ai.js +3 -3
  137. package/sdk/dist/namespaces/ai.js.map +1 -1
  138. package/sdk/dist/namespaces/assets.js +1 -1
  139. package/sdk/dist/namespaces/assets.js.map +1 -1
  140. package/sdk/dist/namespaces/billing.d.ts +6 -3
  141. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  142. package/sdk/dist/namespaces/billing.js.map +1 -1
  143. package/sdk/dist/namespaces/buzz-notifications.types.d.ts +1 -1
  144. package/sdk/dist/namespaces/buzz.d.ts +2 -2
  145. package/sdk/dist/namespaces/buzz.d.ts.map +1 -1
  146. package/sdk/dist/namespaces/buzz.js +2 -2
  147. package/sdk/dist/namespaces/buzz.js.map +1 -1
  148. package/sdk/dist/namespaces/buzz.types.d.ts +2 -2
  149. package/sdk/dist/namespaces/buzz.types.d.ts.map +1 -1
  150. package/sdk/dist/namespaces/ci.js +3 -3
  151. package/sdk/dist/namespaces/ci.js.map +1 -1
  152. package/sdk/dist/namespaces/credentials.types.d.ts +1 -1
  153. package/sdk/dist/namespaces/deploy.d.ts +1 -1
  154. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  155. package/sdk/dist/namespaces/deploy.js +31 -32
  156. package/sdk/dist/namespaces/deploy.js.map +1 -1
  157. package/sdk/dist/namespaces/deploy.types.d.ts +6 -6
  158. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  159. package/sdk/dist/namespaces/domains.d.ts +10 -2
  160. package/sdk/dist/namespaces/domains.d.ts.map +1 -1
  161. package/sdk/dist/namespaces/domains.js +10 -5
  162. package/sdk/dist/namespaces/domains.js.map +1 -1
  163. package/sdk/dist/namespaces/errors.js +1 -1
  164. package/sdk/dist/namespaces/escalations.d.ts +1 -1
  165. package/sdk/dist/namespaces/escalations.js +1 -1
  166. package/sdk/dist/namespaces/escalations.types.d.ts +2 -2
  167. package/sdk/dist/namespaces/escalations.types.d.ts.map +1 -1
  168. package/sdk/dist/namespaces/events.types.d.ts +1 -1
  169. package/sdk/dist/namespaces/functions.d.ts +1 -1
  170. package/sdk/dist/namespaces/functions.js +2 -2
  171. package/sdk/dist/namespaces/functions.js.map +1 -1
  172. package/sdk/dist/namespaces/gitvault.crypto.d.ts +1 -1
  173. package/sdk/dist/namespaces/gitvault.d.ts +26 -26
  174. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  175. package/sdk/dist/namespaces/gitvault.js +113 -113
  176. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  177. package/sdk/dist/namespaces/me.d.ts +120 -0
  178. package/sdk/dist/namespaces/me.d.ts.map +1 -0
  179. package/sdk/dist/namespaces/me.js +34 -0
  180. package/sdk/dist/namespaces/me.js.map +1 -0
  181. package/sdk/dist/namespaces/org.d.ts +108 -6
  182. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  183. package/sdk/dist/namespaces/org.js +63 -7
  184. package/sdk/dist/namespaces/org.js.map +1 -1
  185. package/sdk/dist/namespaces/org.types.d.ts +29 -9
  186. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  187. package/sdk/dist/namespaces/pay.js +3 -3
  188. package/sdk/dist/namespaces/pay.js.map +1 -1
  189. package/sdk/dist/namespaces/projects.d.ts +9 -9
  190. package/sdk/dist/namespaces/projects.js +15 -15
  191. package/sdk/dist/namespaces/projects.js.map +1 -1
  192. package/sdk/dist/namespaces/projects.types.d.ts +28 -12
  193. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  194. package/sdk/dist/namespaces/rooms.d.ts +7 -7
  195. package/sdk/dist/namespaces/rooms.js +17 -17
  196. package/sdk/dist/namespaces/rooms.js.map +1 -1
  197. package/sdk/dist/namespaces/rooms.types.d.ts +9 -9
  198. package/sdk/dist/namespaces/rooms.types.d.ts.map +1 -1
  199. package/sdk/dist/namespaces/service.d.ts +1 -1
  200. package/sdk/dist/namespaces/service.js +1 -1
  201. package/sdk/dist/namespaces/{operator-session.d.ts → session.d.ts} +135 -51
  202. package/sdk/dist/namespaces/session.d.ts.map +1 -0
  203. package/sdk/dist/namespaces/{operator-session.js → session.js} +118 -30
  204. package/sdk/dist/namespaces/session.js.map +1 -0
  205. package/sdk/dist/namespaces/subdomains.d.ts +5 -5
  206. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  207. package/sdk/dist/namespaces/subdomains.js +6 -6
  208. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  209. package/sdk/dist/namespaces/tier.d.ts +44 -21
  210. package/sdk/dist/namespaces/tier.d.ts.map +1 -1
  211. package/sdk/dist/namespaces/tier.js +7 -7
  212. package/sdk/dist/namespaces/tier.js.map +1 -1
  213. package/sdk/dist/namespaces/transfers.d.ts +119 -85
  214. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  215. package/sdk/dist/namespaces/transfers.js +31 -38
  216. package/sdk/dist/namespaces/transfers.js.map +1 -1
  217. package/sdk/dist/namespaces/vouchers.d.ts +9 -10
  218. package/sdk/dist/namespaces/vouchers.d.ts.map +1 -1
  219. package/sdk/dist/namespaces/vouchers.js +4 -5
  220. package/sdk/dist/namespaces/vouchers.js.map +1 -1
  221. package/sdk/dist/namespaces/wallets.d.ts +72 -3
  222. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  223. package/sdk/dist/namespaces/wallets.js +141 -3
  224. package/sdk/dist/namespaces/wallets.js.map +1 -1
  225. package/sdk/dist/namespaces/write-approval.d.ts +101 -0
  226. package/sdk/dist/namespaces/write-approval.d.ts.map +1 -0
  227. package/sdk/dist/namespaces/write-approval.js +62 -0
  228. package/sdk/dist/namespaces/write-approval.js.map +1 -0
  229. package/sdk/dist/node/actions-node.d.ts +3 -3
  230. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  231. package/sdk/dist/node/actions-node.js +44 -44
  232. package/sdk/dist/node/actions-node.js.map +1 -1
  233. package/sdk/dist/node/assets-node.js +6 -6
  234. package/sdk/dist/node/assets-node.js.map +1 -1
  235. package/sdk/dist/node/{bearer-claim-key.d.ts → bearer-redeem-key.d.ts} +12 -12
  236. package/sdk/dist/node/bearer-redeem-key.d.ts.map +1 -0
  237. package/sdk/dist/node/{bearer-claim-key.js → bearer-redeem-key.js} +21 -21
  238. package/sdk/dist/node/bearer-redeem-key.js.map +1 -0
  239. package/sdk/dist/node/ci.d.ts +1 -1
  240. package/sdk/dist/node/ci.d.ts.map +1 -1
  241. package/sdk/dist/node/ci.js +6 -6
  242. package/sdk/dist/node/ci.js.map +1 -1
  243. package/sdk/dist/node/credentials.d.ts +18 -17
  244. package/sdk/dist/node/credentials.d.ts.map +1 -1
  245. package/sdk/dist/node/credentials.js +29 -28
  246. package/sdk/dist/node/credentials.js.map +1 -1
  247. package/sdk/dist/node/deploy-manifest.d.ts +3 -5
  248. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  249. package/sdk/dist/node/deploy-manifest.js +3 -12
  250. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  251. package/sdk/dist/node/funding-recovery.d.ts +1 -1
  252. package/sdk/dist/node/funding-recovery.d.ts.map +1 -1
  253. package/sdk/dist/node/funding-recovery.js +1 -1
  254. package/sdk/dist/node/funding-recovery.js.map +1 -1
  255. package/sdk/dist/node/gitvault-apply.d.ts +1 -1
  256. package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
  257. package/sdk/dist/node/gitvault-apply.js +1 -1
  258. package/sdk/dist/node/gitvault-apply.js.map +1 -1
  259. package/sdk/dist/node/gitvault-handoff.d.ts +22 -22
  260. package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
  261. package/sdk/dist/node/gitvault-handoff.js +45 -45
  262. package/sdk/dist/node/gitvault-handoff.js.map +1 -1
  263. package/sdk/dist/node/gitvault-prewarm.js +7 -7
  264. package/sdk/dist/node/gitvault-prewarm.js.map +1 -1
  265. package/sdk/dist/node/gitvault-restore.d.ts +1 -1
  266. package/sdk/dist/node/gitvault-restore.js +1 -1
  267. package/sdk/dist/node/index.d.ts +16 -16
  268. package/sdk/dist/node/index.d.ts.map +1 -1
  269. package/sdk/dist/node/index.js +15 -15
  270. package/sdk/dist/node/index.js.map +1 -1
  271. package/sdk/dist/node/manifest-export.js +2 -2
  272. package/sdk/dist/node/manifest-export.js.map +1 -1
  273. package/sdk/dist/node/org-adopt.d.ts +49 -0
  274. package/sdk/dist/node/org-adopt.d.ts.map +1 -0
  275. package/sdk/dist/node/org-adopt.js +77 -0
  276. package/sdk/dist/node/org-adopt.js.map +1 -0
  277. package/sdk/dist/node/paid-fetch.d.ts +7 -7
  278. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  279. package/sdk/dist/node/paid-fetch.js +54 -54
  280. package/sdk/dist/node/paid-fetch.js.map +1 -1
  281. package/sdk/dist/node/preflight.d.ts +1 -1
  282. package/sdk/dist/node/preflight.d.ts.map +1 -1
  283. package/sdk/dist/node/preflight.js +1 -1
  284. package/sdk/dist/node/preflight.js.map +1 -1
  285. package/sdk/dist/node/sites-node.js +1 -1
  286. package/sdk/dist/node/sites-node.js.map +1 -1
  287. package/sdk/dist/node/source-scan.d.ts +1 -1
  288. package/sdk/dist/node/source-scan.js +2 -2
  289. package/sdk/dist/node/source-scan.js.map +1 -1
  290. package/sdk/dist/scoped.d.ts +11 -11
  291. package/sdk/dist/scoped.d.ts.map +1 -1
  292. package/sdk/dist/scoped.js +5 -5
  293. package/sdk/dist/scoped.js.map +1 -1
  294. package/core-dist/operator-session.js +0 -154
  295. package/lib/allowance.mjs +0 -355
  296. package/lib/cloud.mjs +0 -256
  297. package/lib/core.mjs +0 -123
  298. package/lib/operator-proofs.mjs +0 -22
  299. package/lib/operator.mjs +0 -771
  300. package/sdk/core-dist/operator-session.js +0 -154
  301. package/sdk/dist/namespaces/allowance.d.ts +0 -79
  302. package/sdk/dist/namespaces/allowance.d.ts.map +0 -1
  303. package/sdk/dist/namespaces/allowance.js +0 -149
  304. package/sdk/dist/namespaces/allowance.js.map +0 -1
  305. package/sdk/dist/namespaces/operator-session.d.ts.map +0 -1
  306. package/sdk/dist/namespaces/operator-session.js.map +0 -1
  307. package/sdk/dist/namespaces/operator.d.ts +0 -359
  308. package/sdk/dist/namespaces/operator.d.ts.map +0 -1
  309. package/sdk/dist/namespaces/operator.js +0 -272
  310. package/sdk/dist/namespaces/operator.js.map +0 -1
  311. package/sdk/dist/node/bearer-claim-key.d.ts.map +0 -1
  312. package/sdk/dist/node/bearer-claim-key.js.map +0 -1
  313. package/sdk/dist/node/operator-claim.d.ts +0 -49
  314. package/sdk/dist/node/operator-claim.d.ts.map +0 -1
  315. package/sdk/dist/node/operator-claim.js +0 -77
  316. package/sdk/dist/node/operator-claim.js.map +0 -1
package/lib/operator.mjs DELETED
@@ -1,771 +0,0 @@
1
- /**
2
- * run402 operator — the operator (human / email) session.
3
- *
4
- * The operator is YOU, the human, identified by email — distinct from the
5
- * AGENT (your wallet / SIWX identity). One browser login spans every wallet
6
- * that verified your email, so `operator overview` returns the cross-wallet
7
- * union. For a single wallet's organization state, use `run402 status`.
8
- *
9
- * Auth: browser-delegated device-authorization grant (RFC 8628, the
10
- * `aws sso login` model). The CLI never performs WebAuthn — the browser does,
11
- * via the existing magic-link / passkey flows — and the CLI brokers the
12
- * resulting operator-session token, cached at the BASE config dir (shared
13
- * across named wallets, since the session is email-scoped).
14
- *
15
- * Agent-first: JSON to stdout. `login` additionally prints the verification URL
16
- * + user code to stderr (human-in-the-loop) and degrades gracefully when not a
17
- * TTY. Gated on the gateway device-auth bridge.
18
- */
19
-
20
- import { setTimeout as sleep } from "node:timers/promises";
21
- import { spawn } from "node:child_process";
22
- import { createServer } from "node:http";
23
- import { randomBytes, createHash } from "node:crypto";
24
- import { fail, reportSdkError } from "./sdk-errors.mjs";
25
- import { getSdk } from "./sdk.mjs";
26
- import { claimWalletOrg, isStepUpRequired, isOperatorApprovalRequired } from "#sdk/node";
27
- import { normalizeArgv, hasHelp, assertKnownFlags, flagValue, failUnknownSubcommand } from "./argparse.mjs";
28
- import {
29
- saveOperatorSession,
30
- clearOperatorSession,
31
- loadLiveOperatorSession,
32
- readOperatorSession,
33
- isOperatorSessionExpired,
34
- operatorSessionFromTokenResponse,
35
- } from "../core-dist/operator-session.js";
36
- import {
37
- saveControlPlaneSession,
38
- clearControlPlaneSession,
39
- readControlPlaneSession,
40
- isControlPlaneSessionExpired,
41
- loadLiveControlPlaneSession,
42
- controlPlaneSessionFromTokenResponse,
43
- } from "../core-dist/control-plane-session.js";
44
- import {
45
- saveApproval,
46
- clearApprovals,
47
- readApprovals,
48
- hashControlPlaneSession,
49
- approvalFromTokenResponse,
50
- } from "../core-dist/write-auth-session.js";
51
- import { getApiBase } from "../core-dist/config.js";
52
-
53
- /** Gateway write-auth capabilities → required target scope. */
54
- const APPROVAL_ACTIONS = {
55
- "org.project.create": "org",
56
- "project.deploy": "project",
57
- "project.secret.write": "project",
58
- };
59
-
60
- /** True when `url` is same-origin as the API base (confirm_url validation). */
61
- function sameApiOrigin(url) {
62
- try {
63
- return new URL(url).origin === new URL(getApiBase()).origin;
64
- } catch {
65
- return false;
66
- }
67
- }
68
-
69
- const CLIENT_NAME = "run402 CLI";
70
-
71
- const HELP = `run402 operator — operator (human / email) session
72
-
73
- The operator is YOU, the human, identified by email — distinct from the agent
74
- (your wallet). One browser login spans every wallet that verified your email.
75
- For a single wallet's organization state, use 'run402 status'.
76
-
77
- Usage:
78
- run402 operator login [--no-open] (read session, device-flow)
79
- run402 operator login --loopback [--no-open] (write session, loopback-PKCE)
80
- run402 operator login --step-up (fresh write session for high-stakes ops)
81
- run402 operator overview
82
- run402 operator whoami
83
- run402 operator logout
84
- run402 operator claim-wallet-org [--org <id>] [--name <label>]
85
- run402 operator approve --action <cap> (--org <id> | --project <id>) [--no-open]
86
- run402 operator status
87
-
88
- Subcommands:
89
- login Sign in via the browser. Default = device-flow READ session (powers
90
- 'overview'). --loopback = write-capable control-plane session
91
- (aws-sso-style, passkey-fresh). --step-up = re-mint a fresh write session.
92
- overview Account view across ALL wallets controlling your email (requires login)
93
- whoami Show the cached session(s) — local, no network
94
- logout Revoke the session server-side and clear the local cache(s)
95
- claim-wallet-org Claim an org owned by your wallet's agent into your human identity
96
- (ownership transfer). Dual proof: your write-capable session
97
- ('login --loopback' first) + a fresh signature from the active wallet.
98
- approve Mint a passkey operator-approval scoped to one (action, target) so a
99
- wallet-less human can provision/deploy. --action is one of
100
- org.project.create (--org), project.deploy (--project),
101
- project.secret.write (--project). Requires 'login --loopback' first.
102
- status Show the control-plane login state + each cached approval (action,
103
- target, expiry). Local, no network.
104
-
105
- Options:
106
- --no-open (login) Do not auto-open the browser; just print the URL.
107
- --loopback (login) Use the loopback-PKCE write login instead of the device flow.
108
- --step-up (login) Re-mint a fresh write session (implies --loopback).
109
- --org <id> (claim-wallet-org) Pick which org to claim (only when the wallet owns several).
110
- --name <s> (claim-wallet-org) Optional label to set on the claimed org.
111
-
112
- Notes:
113
- - The session is cached at the base config dir, shared across named wallets.
114
- - 'overview' requires 'login' and never falls back to a single wallet.
115
- - JSON to stdout; 'login' prints the URL + code to stderr (human-in-the-loop).
116
- `;
117
-
118
- /** Shared output shape for `whoami` and the `login` success result. */
119
- function sessionView(session, nowMs = Date.now()) {
120
- return {
121
- logged_in: true,
122
- email: session.email,
123
- wallets: session.wallets,
124
- wallet_count: session.wallets.length,
125
- expires_at: new Date(session.expires_at).toISOString(),
126
- absolute_expires_at: session.absolute_expires_at || null,
127
- expires_in_seconds: Math.max(0, Math.round((session.expires_at - nowMs) / 1000)),
128
- };
129
- }
130
-
131
- /** Best-effort, cross-platform browser open. Never throws. */
132
- function openBrowser(url) {
133
- try {
134
- let cmd;
135
- let cmdArgs;
136
- if (process.platform === "darwin") {
137
- cmd = "open";
138
- cmdArgs = [url];
139
- } else if (process.platform === "win32") {
140
- cmd = "cmd";
141
- cmdArgs = ["/c", "start", "", url];
142
- } else {
143
- cmd = "xdg-open";
144
- cmdArgs = [url];
145
- }
146
- const child = spawn(cmd, cmdArgs, { stdio: "ignore", detached: true });
147
- child.on("error", () => {}); // ignore: the URL is also printed to stderr
148
- child.unref();
149
- } catch {
150
- // Best-effort only — the human can always copy the printed URL.
151
- }
152
- }
153
-
154
- /** Output shape for the write (control-plane) session. NEVER includes the token. */
155
- function controlPlaneView(session, nowMs = Date.now()) {
156
- return {
157
- logged_in: true,
158
- kind: "control_plane_session",
159
- provenance: session.provenance,
160
- principal_id: session.principal_id || null,
161
- amr: session.amr,
162
- expires_at: new Date(session.expires_at).toISOString(),
163
- expires_in_seconds: Math.max(0, Math.round((session.expires_at - nowMs) / 1000)),
164
- write_capable: true,
165
- };
166
- }
167
-
168
- const base64url = (buf) => buf.toString("base64url");
169
-
170
- /** Generate PKCE (S256) + CSRF state + replay nonce for the loopback flow. */
171
- function pkce() {
172
- const codeVerifier = base64url(randomBytes(32));
173
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
174
- return { codeVerifier, codeChallenge, state: base64url(randomBytes(16)), nonce: base64url(randomBytes(16)) };
175
- }
176
-
177
- /**
178
- * Start a 127.0.0.1 loopback server (RFC 8252) that captures exactly one
179
- * redirect. Returns the bound port (via `ready`), a promise for the auth code,
180
- * and a `close()`. State is validated here to reject CSRF before exchange.
181
- */
182
- function startLoopbackServer({ expectedState, timeoutMs }) {
183
- let resolveCode;
184
- let rejectCode;
185
- const codePromise = new Promise((res, rej) => {
186
- resolveCode = res;
187
- rejectCode = rej;
188
- });
189
- let timer;
190
- // Track live sockets: server.close() alone stops NEW connections but leaves
191
- // the browser's keep-alive socket open, which keeps Node's event loop alive
192
- // and hangs the CLI after a successful login. close() must destroy them.
193
- const sockets = new Set();
194
- const server = createServer((req, res) => {
195
- let u;
196
- try {
197
- u = new URL(req.url, "http://127.0.0.1");
198
- } catch {
199
- res.writeHead(400).end("bad request");
200
- return;
201
- }
202
- if (u.pathname !== "/callback") {
203
- res.writeHead(404).end("not found");
204
- return;
205
- }
206
- const code = u.searchParams.get("code");
207
- const gotState = u.searchParams.get("state");
208
- const errParam = u.searchParams.get("error");
209
- // `connection: close` so the browser does not keep the socket alive.
210
- res.writeHead(200, { "content-type": "text/html", connection: "close" });
211
- res.end(
212
- "<!doctype html><html><body style=\"font-family:system-ui;padding:3rem\">" +
213
- "<h2>run402 - you're signed in.</h2><p>You can close this window and return to your terminal.</p></body></html>",
214
- );
215
- // Do NOT tear down here — let the response flush. The caller calls close()
216
- // once it has the code (or on any failure path).
217
- if (errParam) rejectCode(new Error(`authorization error: ${errParam}`));
218
- else if (!code) rejectCode(new Error("no authorization code on the loopback redirect"));
219
- else if (gotState !== expectedState) rejectCode(new Error("state mismatch on the loopback redirect (possible CSRF) - aborted"));
220
- else resolveCode(code);
221
- });
222
- server.on("connection", (s) => {
223
- sockets.add(s);
224
- s.once("close", () => sockets.delete(s));
225
- });
226
- function close() {
227
- clearTimeout(timer);
228
- for (const s of sockets) {
229
- try {
230
- s.destroy();
231
- } catch {
232
- // already gone
233
- }
234
- }
235
- try {
236
- server.close();
237
- } catch {
238
- // already closing
239
- }
240
- }
241
- timer = setTimeout(() => {
242
- close();
243
- rejectCode(new Error("timed out waiting for browser approval"));
244
- }, timeoutMs);
245
- server.on("error", (e) => {
246
- close();
247
- rejectCode(e);
248
- });
249
- const ready = new Promise((res, rej) => {
250
- server.once("error", rej);
251
- server.listen(0, "127.0.0.1", () => res(server.address().port));
252
- });
253
- return { ready, codePromise, close };
254
- }
255
-
256
- /**
257
- * Loopback-PKCE write-login (RFC 8252, the aws-sso-style flow). Mints a
258
- * write-capable control-plane session via the browser passkey ceremony, caches
259
- * it (mode 0600), and prints metadata only - never the token.
260
- */
261
- async function loopbackLogin(args, { stepUp }) {
262
- const noOpen = args.includes("--no-open");
263
- const sdk = getSdk();
264
- const { codeVerifier, codeChallenge, state, nonce } = pkce();
265
- const { ready, codePromise, close } = startLoopbackServer({ expectedState: state, timeoutMs: 300_000 });
266
-
267
- let port;
268
- try {
269
- port = await ready;
270
- } catch (err) {
271
- close();
272
- return fail({ code: "OPERATOR_LOOPBACK_FAILED", message: `Could not start the loopback server: ${err.message}` });
273
- }
274
- const redirectUri = `http://127.0.0.1:${port}/callback`;
275
- const authorizeUrl = sdk.operator.buildCliAuthorizeUrl({ redirectUri, codeChallenge, state, nonce });
276
-
277
- process.stderr.write(
278
- `\nTo ${stepUp ? "re-authenticate (step-up)" : "sign in (write-capable)"}, open:\n ${authorizeUrl}\n\n`,
279
- );
280
- if (!noOpen && process.stderr.isTTY) {
281
- openBrowser(authorizeUrl);
282
- process.stderr.write("(opening your browser…)\n\n");
283
- }
284
- process.stderr.write("Waiting for approval…\n");
285
-
286
- let code;
287
- try {
288
- code = await codePromise;
289
- } catch (err) {
290
- close();
291
- return fail({
292
- code: "OPERATOR_LOGIN_FAILED",
293
- message: err.message,
294
- hint: "Run 'run402 operator login --loopback' to try again.",
295
- });
296
- }
297
-
298
- let session;
299
- try {
300
- session = await sdk.operator.exchangeCliToken({ code, codeVerifier, redirectUri, state });
301
- } catch (err) {
302
- close();
303
- return reportSdkError(err);
304
- }
305
- // The loopback server has done its job. Tear it down (destroying any
306
- // keep-alive socket) so Node's event loop drains and the CLI exits instead
307
- // of hanging until Ctrl+C.
308
- close();
309
-
310
- const cached = controlPlaneSessionFromTokenResponse(session);
311
- // A fresh control-plane session invalidates any approvals bound to the old one.
312
- clearApprovals();
313
- saveControlPlaneSession(cached);
314
- process.stderr.write(`\nSigned in (write-capable, provenance=${cached.provenance}).\n`);
315
-
316
- // Surface org memberships — newly-active rows are invites auto-claimed at this
317
- // login (owner/admin invites only claim once a passkey is enrolled). Best-effort:
318
- // login already succeeded, so a whoami hiccup must not fail the command.
319
- const view = controlPlaneView(cached);
320
- try {
321
- const who = await sdk.operator.session.whoami({ token: session.control_plane_session_token });
322
- const memberships = Array.isArray(who?.memberships) ? who.memberships : [];
323
- view.memberships = memberships;
324
- if (memberships.length) {
325
- process.stderr.write(
326
- `Member of ${memberships.length} org(s):\n` +
327
- memberships.map((m) => ` - ${m.display_name || m.org_id || m.org_id || "unknown"} (${m.role}, ${m.status})`).join("\n") +
328
- "\n",
329
- );
330
- }
331
- } catch {
332
- /* best-effort — the session is valid regardless of the whoami result */
333
- }
334
- console.log(JSON.stringify(view));
335
- }
336
-
337
- async function login(args) {
338
- assertKnownFlags(args, ["--help", "-h", "--no-open", "--loopback", "--device", "--step-up"]);
339
- // Loopback-PKCE = the write-capable control-plane login (--loopback/--step-up).
340
- // The default stays the device-flow READ session (which powers 'overview');
341
- // --device forces it explicitly.
342
- if (args.includes("--loopback") || args.includes("--step-up")) {
343
- return loopbackLogin(args, { stepUp: args.includes("--step-up") });
344
- }
345
- const noOpen = args.includes("--no-open");
346
- const sdk = getSdk();
347
-
348
- let start;
349
- try {
350
- start = await sdk.operator.deviceStart({ clientName: CLIENT_NAME });
351
- } catch (err) {
352
- return reportSdkError(err);
353
- }
354
-
355
- // Human-in-the-loop prompt → stderr, so stdout stays clean for the final JSON.
356
- const target = start.verification_uri_complete || start.verification_uri;
357
- process.stderr.write(
358
- `\nTo authorize the ${CLIENT_NAME}, open:\n ${start.verification_uri}\n` +
359
- `and enter the code: ${start.user_code}\n\n`,
360
- );
361
- if (!noOpen && process.stderr.isTTY) {
362
- openBrowser(target);
363
- process.stderr.write("(opening your browser…)\n\n");
364
- }
365
- process.stderr.write("Waiting for approval…\n");
366
-
367
- // Poll loop — honor the server interval, back off on slow_down, and stop at
368
- // the device-code deadline. if/else (not switch) so the sync scanner doesn't
369
- // mistake the poll states for CLI subcommands.
370
- let intervalMs = Math.max(1, Number(start.interval) || 5) * 1000;
371
- const deadline = Date.now() + Math.max(1, Number(start.expires_in) || 600) * 1000;
372
-
373
- while (Date.now() < deadline) {
374
- await sleep(intervalMs);
375
- let result;
376
- try {
377
- result = await sdk.operator.devicePoll(start.device_code);
378
- } catch (err) {
379
- return reportSdkError(err);
380
- }
381
- if (result.kind === "approved") {
382
- const session = operatorSessionFromTokenResponse(result.session);
383
- saveOperatorSession(session);
384
- process.stderr.write(`\nSigned in as ${session.email}.\n`);
385
- console.log(JSON.stringify(sessionView(session)));
386
- return;
387
- }
388
- if (result.kind === "authorization_pending") continue;
389
- if (result.kind === "slow_down") {
390
- intervalMs += 5000;
391
- continue;
392
- }
393
- if (result.kind === "access_denied") {
394
- fail({
395
- code: "OPERATOR_LOGIN_DENIED",
396
- message: "Authorization was denied in the browser.",
397
- hint: "Run 'run402 operator login' to try again.",
398
- });
399
- }
400
- if (result.kind === "expired_token") {
401
- fail({
402
- code: "OPERATOR_LOGIN_EXPIRED",
403
- message: "The device code expired before approval.",
404
- hint: "Run 'run402 operator login' to get a fresh code.",
405
- });
406
- }
407
- fail({ code: "OPERATOR_LOGIN_FAILED", message: `Unexpected device poll result: ${result.kind}` });
408
- }
409
- fail({
410
- code: "OPERATOR_LOGIN_TIMEOUT",
411
- message: "Timed out waiting for browser approval.",
412
- hint: "Run 'run402 operator login' to try again.",
413
- });
414
- }
415
-
416
- async function logout(args) {
417
- assertKnownFlags(args, ["--help", "-h"]);
418
- const session = loadLiveOperatorSession();
419
- let revoked = false;
420
- if (session) {
421
- try {
422
- await getSdk().operator.revoke({ token: session.operator_session_token });
423
- revoked = true;
424
- } catch {
425
- // Best-effort: a failed server revoke (expired token, offline) must not
426
- // block clearing the local cache. The local token is removed regardless.
427
- revoked = false;
428
- }
429
- }
430
- clearOperatorSession();
431
- clearControlPlaneSession();
432
- clearApprovals();
433
- console.log(JSON.stringify({ revoked, cleared: true }));
434
- }
435
-
436
- async function overview(args) {
437
- assertKnownFlags(args, ["--help", "-h"]);
438
- const session = loadLiveOperatorSession();
439
- if (!session) {
440
- fail({
441
- code: "OPERATOR_LOGIN_REQUIRED",
442
- message: "No operator session. Run 'run402 operator login' to sign in.",
443
- hint: "operator overview shows the union across all wallets controlling your email; for a single wallet use 'run402 status'.",
444
- });
445
- }
446
- try {
447
- const result = await getSdk().operator.overview({ token: session.operator_session_token });
448
- console.log(JSON.stringify(result, null, 2));
449
- } catch (err) {
450
- // 401/403 means the session was revoked or expired server-side. Clear the
451
- // stale cache and point at re-login instead of leaving a dead token behind.
452
- if (err && (err.status === 401 || err.status === 403)) {
453
- clearOperatorSession();
454
- fail({
455
- code: "OPERATOR_SESSION_INVALID",
456
- message: "Operator session is no longer valid (revoked or expired).",
457
- hint: "Run 'run402 operator login' to sign in again.",
458
- });
459
- }
460
- reportSdkError(err);
461
- }
462
- }
463
-
464
- async function whoami(args) {
465
- assertKnownFlags(args, ["--help", "-h"]);
466
- const now = Date.now();
467
- const session = readOperatorSession();
468
- const cp = readControlPlaneSession();
469
- const liveCp = cp && !isControlPlaneSessionExpired(cp, now) ? cp : null;
470
-
471
- if (session && !isOperatorSessionExpired(session, now)) {
472
- const view = sessionView(session, now);
473
- if (liveCp) view.control_plane = controlPlaneView(liveCp, now);
474
- console.log(JSON.stringify(view));
475
- return;
476
- }
477
- // No live device-flow READ session — fall back to the write session if present.
478
- if (liveCp) {
479
- console.log(JSON.stringify(controlPlaneView(liveCp, now)));
480
- return;
481
- }
482
- if (!session) {
483
- console.log(JSON.stringify({ logged_in: false, reason: "no_session", hint: "Run 'run402 operator login' to sign in." }));
484
- process.exitCode = 1;
485
- return;
486
- }
487
- console.log(JSON.stringify({ logged_in: false, reason: "expired", email: session.email, hint: "Run 'run402 operator login' to sign in again." }));
488
- process.exitCode = 1;
489
- }
490
-
491
- /**
492
- * Claim a wallet-owned org into your human (console) identity — an ownership
493
- * transfer (v1.82). Dual proof: your write-capable control-plane session (run
494
- * 'operator login --loopback' first) PLUS a fresh SIWX signature from the active
495
- * wallet over a server nonce. The Node convenience runs the whole dance
496
- * (read session → challenge → sign → submit). Step-up failures and the
497
- * multi-org `select_org` round-trip are surfaced here, not thrown to the user.
498
- */
499
- async function claimWalletOrgCmd(args) {
500
- assertKnownFlags(args, ["--help", "-h", "--org", "--name"], ["--org", "--name"]);
501
- const orgId = flagValue(args, "--org");
502
- const name = flagValue(args, "--name");
503
- try {
504
- const result = await claimWalletOrg(getSdk(), {
505
- orgId: orgId ?? undefined,
506
- displayName: name ?? undefined,
507
- });
508
- // CLI output contract (cli-output-shape): no top-level `status` envelope.
509
- // The gateway's discriminator becomes an explicit `claimed` boolean.
510
- if (result && result.status === "select_org") {
511
- console.log(
512
- JSON.stringify(
513
- {
514
- claimed: false,
515
- selectable_orgs: result.selectable_orgs,
516
- hint: "Your wallet's agent owns more than one org. Re-run with --org <id> to claim one.",
517
- },
518
- null,
519
- 2,
520
- ),
521
- );
522
- return;
523
- }
524
- console.log(
525
- JSON.stringify(
526
- {
527
- claimed: true,
528
- org_id: result.org_id,
529
- display_name: result.display_name,
530
- role: result.role,
531
- already_owned: result.already_owned ?? false,
532
- },
533
- null,
534
- 2,
535
- ),
536
- );
537
- } catch (err) {
538
- if (isStepUpRequired(err)) {
539
- return fail({
540
- code: "STEP_UP_REQUIRED",
541
- message: "Claiming a wallet org needs a fresh passkey step-up.",
542
- hint: "Run 'run402 operator login --step-up', then re-run 'run402 operator claim-wallet-org'.",
543
- });
544
- }
545
- reportSdkError(err);
546
- }
547
- }
548
-
549
- /**
550
- * Run the scoped operator-approval ceremony (loopback + passkey) and cache the
551
- * minted approval. Requires a live control-plane session. Returns the cached
552
- * approval (or exits non-zero via fail/reportSdkError). Reused by the
553
- * `operator approve` verb AND the TTY auto-approve path in provision/deploy.
554
- */
555
- export async function mintApproval({ action, orgId, projectId, noOpen = false }) {
556
- const cp = loadLiveControlPlaneSession();
557
- if (!cp) {
558
- return fail({
559
- code: "OPERATOR_LOGIN_REQUIRED",
560
- message: "No control-plane session. Run 'run402 operator login --loopback' first, then approve.",
561
- hint: "operator approval is minted on top of your write-capable control-plane session.",
562
- });
563
- }
564
- const sdk = getSdk();
565
- const { codeVerifier, codeChallenge, state } = pkce();
566
- const { ready, codePromise, close } = startLoopbackServer({ expectedState: state, timeoutMs: 300_000 });
567
-
568
- let port;
569
- try {
570
- port = await ready;
571
- } catch (err) {
572
- close();
573
- return fail({ code: "OPERATOR_APPROVE_FAILED", message: `Could not start the loopback server: ${err.message}` });
574
- }
575
- const redirectUri = `http://127.0.0.1:${port}/callback`;
576
-
577
- let challenge;
578
- try {
579
- challenge = await sdk.operator.approval.requestChallenge({
580
- action,
581
- orgId: orgId ?? undefined,
582
- projectId: projectId ?? undefined,
583
- cliRedirectUri: redirectUri,
584
- codeChallenge,
585
- state,
586
- token: cp.control_plane_session_token,
587
- });
588
- } catch (err) {
589
- close();
590
- return reportSdkError(err);
591
- }
592
-
593
- const confirmUrl = challenge?.confirm_url;
594
- if (!confirmUrl || !sameApiOrigin(confirmUrl)) {
595
- close();
596
- return fail({
597
- code: "OPERATOR_APPROVE_BAD_CONFIRM_URL",
598
- message: "The gateway returned a confirm_url that is not same-origin as the API base; aborting.",
599
- });
600
- }
601
- const targetLabel = orgId ? `org ${orgId}` : projectId ? `project ${projectId}` : "(no target)";
602
- process.stderr.write(
603
- `\nTo approve '${action}' for ${targetLabel}, open and approve with your passkey:\n ${confirmUrl}\n\n`,
604
- );
605
- if (!noOpen && process.stderr.isTTY) {
606
- openBrowser(confirmUrl);
607
- process.stderr.write("(opening your browser…)\n\n");
608
- }
609
- process.stderr.write("Waiting for passkey approval…\n");
610
-
611
- let code;
612
- try {
613
- code = await codePromise;
614
- } catch (err) {
615
- close();
616
- return fail({ code: "OPERATOR_APPROVE_FAILED", message: err.message, hint: "Run 'run402 operator approve' to try again." });
617
- }
618
-
619
- let token;
620
- try {
621
- token = await sdk.operator.approval.exchangeClaimCode({ code, codeVerifier, state });
622
- } catch (err) {
623
- close();
624
- return reportSdkError(err);
625
- }
626
- close();
627
-
628
- const approval = approvalFromTokenResponse(token, {
629
- action,
630
- target: { org_id: orgId ?? undefined, project_id: projectId ?? undefined },
631
- apiOrigin: new URL(getApiBase()).origin,
632
- controlPlaneSessionHash: hashControlPlaneSession(cp.control_plane_session_token),
633
- controlPlanePrincipalId: cp.principal_id || "",
634
- });
635
- saveApproval(approval);
636
- process.stderr.write(`\nApproved '${action}' for ${targetLabel}.\n`);
637
- return approval;
638
- }
639
-
640
- /**
641
- * Run a write action; on `OperatorApprovalRequiredError` AND an interactive
642
- * TTY (CLI), run the scoped approval ceremony and retry once. In MCP / CI /
643
- * non-TTY the typed error is rethrown unchanged — the agent relays the resolved
644
- * `operator approve …` command rather than a browser opening unexpectedly.
645
- */
646
- export async function withAutoApprove(fn) {
647
- try {
648
- return await fn();
649
- } catch (err) {
650
- if (!isOperatorApprovalRequired(err) || !process.stderr.isTTY) throw err;
651
- const action = err.capability;
652
- const target = err.target || {};
653
- if (!action) throw err;
654
- process.stderr.write(`\nThis '${action}' needs a one-time operator approval. Opening the browser to approve…\n`);
655
- await mintApproval({ action, orgId: target.org_id ?? undefined, projectId: target.project_id ?? undefined });
656
- return await fn(); // retry once with the now-cached approval
657
- }
658
- }
659
-
660
- /**
661
- * `operator approve --action <cap> (--org <id> | --project <id>)` — mint a
662
- * passkey approval scoped to one (action, target). Hidden alias: `write-auth`.
663
- */
664
- async function approve(args) {
665
- assertKnownFlags(
666
- args,
667
- ["--help", "-h", "--no-open", "--action", "--org", "--project"],
668
- ["--action", "--org", "--project"],
669
- );
670
- const action = flagValue(args, "--action");
671
- const org = flagValue(args, "--org");
672
- const project = flagValue(args, "--project");
673
-
674
- if (!action || !APPROVAL_ACTIONS[action]) {
675
- fail({
676
- code: "BAD_FLAG",
677
- message: `--action must be one of: ${Object.keys(APPROVAL_ACTIONS).join(", ")}`,
678
- details: { flag: "--action", value: action, allowed: Object.keys(APPROVAL_ACTIONS) },
679
- });
680
- }
681
- const scope = APPROVAL_ACTIONS[action];
682
- if (scope === "org" && !org) fail({ code: "BAD_FLAG", message: `--org <id> is required for --action ${action}` });
683
- if (scope === "project" && !project) fail({ code: "BAD_FLAG", message: `--project <id> is required for --action ${action}` });
684
- if (scope === "org" && project) fail({ code: "BAD_FLAG", message: `--project is not valid for org-scoped --action ${action}; use --org` });
685
- if (scope === "project" && org) fail({ code: "BAD_FLAG", message: `--org is not valid for project-scoped --action ${action}; use --project` });
686
-
687
- const approval = await mintApproval({
688
- action,
689
- orgId: org ?? undefined,
690
- projectId: project ?? undefined,
691
- noOpen: args.includes("--no-open"),
692
- });
693
- console.log(
694
- JSON.stringify({
695
- approved: true,
696
- action,
697
- ...(org ? { org_id: org } : {}),
698
- ...(project ? { project_id: project } : {}),
699
- expires_at: new Date(approval.expires_at).toISOString(),
700
- }),
701
- );
702
- }
703
-
704
- /** `operator status` — operator-login state + each cached approval's (action, target, expiry). */
705
- async function status(args) {
706
- assertKnownFlags(args, ["--help", "-h"]);
707
- const now = Date.now();
708
- const cp = readControlPlaneSession();
709
- const liveCp = cp && !isControlPlaneSessionExpired(cp, now) ? cp : null;
710
- const approvals = readApprovals()
711
- .filter((a) => a.expires_at > now)
712
- .map((a) => ({
713
- action: a.action,
714
- org_id: a.org_id ?? null,
715
- project_id: a.project_id ?? null,
716
- expires_at: new Date(a.expires_at).toISOString(),
717
- }));
718
- const out = {
719
- operator_login: liveCp
720
- ? { active: true, provenance: liveCp.provenance, amr: liveCp.amr, expires_at: new Date(liveCp.expires_at).toISOString() }
721
- : { active: false },
722
- approvals,
723
- };
724
- if (liveCp) {
725
- process.stderr.write(`Operator login: active (${liveCp.provenance})\n`);
726
- process.stderr.write(
727
- approvals.length
728
- ? `Approvals (${approvals.length}):\n` +
729
- approvals.map((a) => ` - ${a.action} ${a.org_id || a.project_id || ""} (expires ${a.expires_at})`).join("\n") +
730
- "\n"
731
- : "Approvals: none. Run 'run402 operator approve --action <cap> --org|--project <id>'.\n",
732
- );
733
- } else {
734
- process.stderr.write("Operator login: none. Run 'run402 operator login --loopback'.\n");
735
- }
736
- console.log(JSON.stringify(out, null, 2));
737
- }
738
-
739
- export async function run(sub, args = []) {
740
- args = normalizeArgv(args);
741
- if (!sub || sub === "--help" || sub === "-h" || hasHelp(args)) {
742
- console.log(HELP);
743
- process.exit(0);
744
- }
745
- if (sub === "write-auth") sub = "approve"; // hidden alias (transport name)
746
- switch (sub) {
747
- case "login":
748
- await login(args);
749
- break;
750
- case "logout":
751
- await logout(args);
752
- break;
753
- case "overview":
754
- await overview(args);
755
- break;
756
- case "whoami":
757
- await whoami(args);
758
- break;
759
- case "claim-wallet-org":
760
- await claimWalletOrgCmd(args);
761
- break;
762
- case "approve":
763
- await approve(args);
764
- break;
765
- case "status":
766
- await status(args);
767
- break;
768
- default:
769
- failUnknownSubcommand("operator", sub, { extraSubcommands: ["status"] });
770
- }
771
- }