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
package/lib/wallets.mjs CHANGED
@@ -17,6 +17,7 @@ import { writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
17
17
  import { failUnknownSubcommand } from "./argparse.mjs";
18
18
  import { join } from "node:path";
19
19
  import { fail, reportSdkError } from "./sdk-errors.mjs";
20
+ import { walletFile, readWallet as readActiveWallet, saveWallet as saveActiveWallet } from "./config.mjs";
20
21
  import { isValidProfileName, getActiveProfile } from "../core-dist/config.js";
21
22
  import {
22
23
  listProfileNames,
@@ -30,10 +31,11 @@ import {
30
31
  getDefaultWallet,
31
32
  setDefaultWallet,
32
33
  } from "../core-dist/profiles.js";
33
- import { readAllowance, saveAllowance } from "../core-dist/allowance.js";
34
+ import { readWallet, saveWallet } from "../core-dist/wallet.js";
34
35
  import { describeRejectedValue } from "../core-dist/redact.js";
35
36
  import { getSdk } from "./sdk.mjs";
36
37
  import { readBindingFile, updateBindingFile } from "./wallet-context.mjs";
38
+ import { initializeWalletAction } from "./next-actions.mjs";
37
39
 
38
40
  const DEFAULT = "default";
39
41
  const PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
@@ -42,16 +44,18 @@ const HELP = `run402 wallets — manage named wallets (profiles)
42
44
 
43
45
  Usage:
44
46
  run402 wallets list List all wallets (local_label, server_label, address, rail, active)
45
- run402 wallets current Show the resolved active wallet + how it was selected
46
- run402 wallets new <name> Create a new named wallet (key stays local)
47
+ run402 wallets current Show the active wallet: how it was selected, address, rail, file path, faucet use
48
+ run402 wallets fund Request testnet funds from the faucet into the active wallet (Base Sepolia or Tempo)
49
+ run402 wallets balance The active wallet's on-chain balances, plus its organization's allowance
50
+ run402 wallets new <name> Create a new wallet (key stays local); 'default' creates the root wallet
47
51
  run402 wallets use <name> Set the global default wallet
48
52
  run402 wallets rename <old> --to <new> Rename a wallet (legacy: rename <old> <new>)
49
53
  run402 wallets bind [<name>] Write ./.run402.json binding this directory to a wallet
50
54
  run402 wallets unbind Remove ./.run402.json
51
55
  run402 wallets import <name> --key <path|-> Adopt an existing private key as a named wallet
52
56
  run402 wallets rm <name> --yes Delete a wallet and its keys (requires --yes)
53
- run402 wallets lightning status The active wallet's Lightning allowance (balance, budget, custody)
54
- run402 wallets lightning mint Mint the Lightning allowance for the active wallet (same as init lightning)
57
+ run402 wallets lightning status The active wallet's Lightning wallet (balance, budget, custody)
58
+ run402 wallets lightning mint Mint the Lightning wallet for the active wallet (same as init lightning)
55
59
  run402 wallets lightning revoke Revoke it: the sub-wallet is deleted on Run402's Hub; the rail returns to x402
56
60
 
57
61
  Selection precedence for normal commands:
@@ -102,7 +106,7 @@ function walletInfo(name, active) {
102
106
  const label = meta?.label ?? null;
103
107
  if (!address) {
104
108
  try {
105
- const a = readAllowance(join(profileDir(name), "allowance.json"));
109
+ const a = readWallet(join(profileDir(name), "wallet.json"));
106
110
  address = a?.address ?? null;
107
111
  rail = rail ?? a?.rail ?? null;
108
112
  } catch {
@@ -127,7 +131,7 @@ function cmdList() {
127
131
  out(listProfileNames().map((n) => walletInfo(n, active)));
128
132
  }
129
133
 
130
- function cmdCurrent() {
134
+ async function cmdCurrent() {
131
135
  const ctx = activeContext();
132
136
  const name = ctx?.name ?? getActiveProfile();
133
137
  const info = walletInfo(name, name);
@@ -146,21 +150,33 @@ function cmdCurrent() {
146
150
  hint: "Run 'run402 wallets rename' to reconcile.",
147
151
  });
148
152
  }
153
+ // The active wallet's own file: when it was created, its persisted rail, and
154
+ // whether the faucet has been used on it. `faucet_used` tracks faucet
155
+ // invocation, not pay-readiness — `run402 wallets balance` reads real funds.
156
+ let local = null;
157
+ try {
158
+ local = await getSdk().wallets.status();
159
+ } catch (err) {
160
+ reportSdkError(err);
161
+ }
149
162
  out({
150
163
  local_label: name,
151
164
  source: ctx?.source ?? "unknown",
152
165
  source_detail: ctx?.sourceDetail ?? null,
153
- address: info.address,
166
+ address: local?.configured ? local.address : info.address,
154
167
  server_label: info.server_label,
168
+ configured: !!local?.configured,
169
+ created: local?.created ?? null,
170
+ rail: local?.configured ? local.rail ?? "x402" : info.rail,
171
+ faucet_used: !!local?.faucet_used,
172
+ path: local?.path ?? walletFile(),
173
+ ...(local?.configured ? {} : { next_actions: [initializeWalletAction()] }),
155
174
  warnings,
156
175
  });
157
176
  }
158
177
 
159
178
  async function cmdNew(args) {
160
179
  const name = requireName(args.find((a) => a && !a.startsWith("-")));
161
- if (name === DEFAULT) {
162
- fail({ code: "BAD_WALLET_NAME", message: "'default' is reserved. Use 'run402 init' for the default wallet.", details: { name } });
163
- }
164
180
  if (profileExists(name)) {
165
181
  fail({ code: "WALLET_EXISTS", message: `A wallet named '${name}' already exists.`, hint: "run402 wallets list", details: { name } });
166
182
  }
@@ -175,12 +191,23 @@ async function cmdNew(args) {
175
191
  const address = privateKeyToAccount(privateKey).address;
176
192
  const created = new Date().toISOString();
177
193
  ensureProfileDir(name);
178
- saveAllowance({ address, privateKey, created, funded: false, rail }, join(profileDir(name), "allowance.json"));
179
- writeMeta(name, { name, address, label: name, rail, created });
180
- await maybePushLabel(name, name, address);
194
+ saveWallet({ address, privateKey, created, funded: false, rail }, join(profileDir(name), "wallet.json"));
195
+ // The reserved `default` wallet lives at the config-dir root and carries no
196
+ // server label; a named wallet mirrors its name as the label.
197
+ if (name === DEFAULT) {
198
+ writeMeta(name, { name, address, rail, created });
199
+ } else {
200
+ writeMeta(name, { name, address, label: name, rail, created });
201
+ await maybePushLabel(name, name, address);
202
+ }
181
203
  // requireName constrains aliases to shell-safe lowercase identifiers.
182
- const command = rail === "lightning" ? `run402 --wallet ${name} init lightning` : `run402 wallets use ${name}`;
183
- out({ local_label: name, address, rail, created: true, next: command, next_actions: [{ type: "run_command", command, why: rail === "lightning" ? "Initialize this Lightning wallet." : "Select the wallet you just created." }] });
204
+ const command = rail === "lightning"
205
+ ? `run402 --wallet ${name} init lightning`
206
+ : name === DEFAULT ? "run402 wallets fund" : `run402 wallets use ${name}`;
207
+ const why = rail === "lightning"
208
+ ? "Initialize this Lightning wallet."
209
+ : name === DEFAULT ? "Fund the wallet you just created from the testnet faucet." : "Select the wallet you just created.";
210
+ out({ local_label: name, address, rail, created: true, next: command, next_actions: [{ type: "run_command", command, why }] });
184
211
  }
185
212
 
186
213
  function cmdUse(args) {
@@ -216,7 +243,7 @@ async function cmdRename(args) {
216
243
  let address = meta.address ?? null;
217
244
  if (!address) {
218
245
  try {
219
- address = readAllowance(join(profileDir(newName), "allowance.json"))?.address ?? null;
246
+ address = readWallet(join(profileDir(newName), "wallet.json"))?.address ?? null;
220
247
  } catch {
221
248
  /* best-effort */
222
249
  }
@@ -285,7 +312,7 @@ async function cmdImport(args) {
285
312
  }
286
313
  const created = new Date().toISOString();
287
314
  ensureProfileDir(name);
288
- saveAllowance({ address, privateKey, created, funded: false, rail: "x402" }, join(profileDir(name), "allowance.json"));
315
+ saveWallet({ address, privateKey, created, funded: false, rail: "x402" }, join(profileDir(name), "wallet.json"));
289
316
  writeMeta(name, { name, address, label: name, rail: "x402", created });
290
317
  await maybePushLabel(name, name, address);
291
318
  out({ local_label: name, address, imported: true });
@@ -323,11 +350,11 @@ function flagVal(args, flag) {
323
350
  }
324
351
 
325
352
  /**
326
- * Best-effort server-side label push. Signs with the TARGET wallet's allowance
353
+ * Best-effort server-side label push. Signs with the TARGET wallet's key
327
354
  * (not the active one) so a just-created/renamed wallet can set its own label.
328
355
  *
329
356
  * ON by default — the gateway label endpoint is live, and the display label is
330
- * what makes the wallet name show up cross-machine and in the operator console
357
+ * what makes the wallet name show up cross-machine and in the console
331
358
  * (WEB). Set `RUN402_WALLET_LABEL_SYNC=0` to disable (fully-offline wallet ops,
332
359
  * or hermetic tests). The local folder name is always the source of truth; this
333
360
  * only mirrors the display label to the server. Always best-effort — `setLabel`
@@ -339,7 +366,7 @@ async function maybePushLabel(name, label, address) {
339
366
  if (!address) return;
340
367
  try {
341
368
  const sdk = getSdk({
342
- allowancePath: join(profileDir(name), "allowance.json"),
369
+ walletPath: join(profileDir(name), "wallet.json"),
343
370
  keystorePath: join(profileDir(name), "projects.json"),
344
371
  });
345
372
  await sdk.wallet(address).setLabel(label);
@@ -364,6 +391,8 @@ export async function run(sub, args = []) {
364
391
  case "unbind": return cmdUnbind();
365
392
  case "import": return cmdImport(rest);
366
393
  case "rm": return cmdRm(rest);
394
+ case "fund": return cmdFund();
395
+ case "balance": return cmdBalance();
367
396
  case "lightning": return cmdLightning(rest);
368
397
  default:
369
398
  failUnknownSubcommand("wallets", sub);
@@ -371,30 +400,30 @@ export async function run(sub, args = []) {
371
400
  }
372
401
 
373
402
  // ── lightning ────────────────────────────────────────────────────────────────
374
- // The Lightning allowance of the ACTIVE wallet (mpp-lightning-over-nwc).
375
- // The pairing secret stays in allowance.json and is never printed.
403
+ // The Lightning wallet of the ACTIVE wallet (mpp-lightning-over-nwc).
404
+ // The pairing secret stays in wallet.json and is never printed.
376
405
  async function cmdLightning(args) {
377
406
  const action = args.find((a) => a && !a.startsWith("-")) ?? "status";
378
407
  const { ensureLightningWallet, revokeLightningWallet, describeLightning, readLightningBalance } = await import("./lightning-wallet.mjs");
379
- const { readAllowance } = await import("../core-dist/allowance.js");
408
+ const { readWallet } = await import("../core-dist/wallet.js");
380
409
  try {
381
410
  if (action === "status") {
382
- const allowance = readAllowance();
383
- if (!allowance) fail({ code: "NO_ALLOWANCE", message: "No allowance configured for this wallet.", hint: "run402 init lightning" });
411
+ const localWallet = readWallet();
412
+ if (!localWallet) fail({ code: "NO_WALLET", message: "No local wallet configured for this wallet.", hint: "run402 init lightning" });
384
413
  let wallet = null;
385
414
  try { wallet = await getSdk().agent.lightningWallet.get(); } catch (err) {
386
415
  const code = err?.body?.code ?? err?.code;
387
416
  if (code !== "LIGHTNING_WALLET_NOT_FOUND") throw err;
388
417
  }
389
- const balance = await readLightningBalance(allowance);
390
- const lightning = describeLightning(allowance, wallet, balance);
391
- out({ rail: allowance.rail ?? "x402", lightning, ...(lightning ? {} : { hint: "run402 init lightning" }) });
418
+ const balance = await readLightningBalance(localWallet);
419
+ const lightning = describeLightning(localWallet, wallet, balance);
420
+ out({ rail: localWallet.rail ?? "x402", lightning, ...(lightning ? {} : { hint: "run402 init lightning" }) });
392
421
  return;
393
422
  }
394
423
  if (action === "mint") {
395
424
  const result = await ensureLightningWallet();
396
- const balance = result.outcome === "stored" || result.outcome === "present" ? await readLightningBalance(result.allowance) : null;
397
- out({ outcome: result.outcome, rail: result.allowance.rail ?? "x402", lightning: describeLightning(result.allowance, result.wallet, balance) });
425
+ const balance = result.outcome === "stored" || result.outcome === "present" ? await readLightningBalance(result.localWallet) : null;
426
+ out({ outcome: result.outcome, rail: result.localWallet.rail ?? "x402", lightning: describeLightning(result.localWallet, result.wallet, balance) });
398
427
  return;
399
428
  }
400
429
  if (action === "revoke") {
@@ -407,3 +436,139 @@ async function cmdLightning(args) {
407
436
  reportSdkError(err);
408
437
  }
409
438
  }
439
+
440
+ // ── fund / balance (the ACTIVE wallet) ─────────────────────────────────────
441
+
442
+ const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
443
+ const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
444
+ const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
445
+ const PATH_USD = "0x20c0000000000000000000000000000000000000";
446
+ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
447
+
448
+ async function loadDeps() {
449
+ const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
450
+ const { createPublicClient, http, defineChain } = await import("viem");
451
+ const { base, baseSepolia } = await import("viem/chains");
452
+ const tempoModerato = defineChain({
453
+ id: 42431,
454
+ name: "Tempo Moderato",
455
+ nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
456
+ rpcUrls: { default: { http: [TEMPO_RPC] } },
457
+ });
458
+ return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia, tempoModerato };
459
+ }
460
+
461
+ async function cmdFund() {
462
+ const w = readActiveWallet();
463
+ if (!w) {
464
+ fail({
465
+ code: "NO_WALLET",
466
+ message: "No local wallet.",
467
+ hint: "Run: run402 init",
468
+ });
469
+ }
470
+
471
+ if (w.rail === "mpp") {
472
+ // Tempo Moderato faucet — instant, no polling needed
473
+ const { createPublicClient, http, tempoModerato } = await loadDeps();
474
+ const client = createPublicClient({ chain: tempoModerato, transport: http() });
475
+ const before = await readUsdcBalance(client, PATH_USD, w.address).catch(() => 0);
476
+
477
+ const res = await fetch(TEMPO_RPC, {
478
+ method: "POST",
479
+ headers: { "Content-Type": "application/json" },
480
+ body: JSON.stringify({ jsonrpc: "2.0", method: "tempo_fundAddress", params: [w.address], id: 1 }),
481
+ });
482
+ const data = await res.json();
483
+ if (data.error) {
484
+ fail({
485
+ code: "FAUCET_FAILED",
486
+ message: data.error.message || "Tempo faucet failed",
487
+ details: { rail: "mpp" },
488
+ });
489
+ }
490
+
491
+ // Re-read balance once (instant confirmation)
492
+ const now = await readUsdcBalance(client, PATH_USD, w.address).catch(() => before);
493
+ saveActiveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
494
+ console.log(JSON.stringify({
495
+ address: w.address,
496
+ rail: "mpp",
497
+ onchain: {
498
+ "tempo-moderato_pathusd_micros": now,
499
+ },
500
+ }, null, 2));
501
+ return;
502
+ }
503
+
504
+ // Default: Base Sepolia faucet (existing behavior)
505
+ const { createPublicClient, http, baseSepolia } = await loadDeps();
506
+ const client = createPublicClient({ chain: baseSepolia, transport: http() });
507
+ const before = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => 0);
508
+
509
+ let data;
510
+ try {
511
+ data = await getSdk().wallets.faucet(w.address);
512
+ } catch (err) {
513
+ reportSdkError(err);
514
+ }
515
+
516
+ const MAX_WAIT = 30;
517
+ for (let i = 0; i < MAX_WAIT; i++) {
518
+ await new Promise(r => setTimeout(r, 1000));
519
+ const now = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => before);
520
+ if (now > before) {
521
+ saveActiveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
522
+ console.log(JSON.stringify({
523
+ address: w.address,
524
+ rail: w.rail || "x402",
525
+ onchain: {
526
+ "base-sepolia_usd_micros": now,
527
+ },
528
+ }, null, 2));
529
+ return;
530
+ }
531
+ }
532
+
533
+ saveActiveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
534
+ console.log(JSON.stringify({ ...data, balance_confirmed: false, hint: "Faucet request sent but on-chain balance not yet confirmed" }));
535
+ }
536
+
537
+ async function readUsdcBalance(client, usdc, address) {
538
+ const raw = await client.readContract({ address: usdc, abi: USDC_ABI, functionName: "balanceOf", args: [address] });
539
+ return Number(raw);
540
+ }
541
+
542
+ async function cmdBalance() {
543
+ const w = readActiveWallet();
544
+ if (!w) {
545
+ fail({
546
+ code: "NO_WALLET",
547
+ message: "No local wallet.",
548
+ hint: "Run: run402 init",
549
+ });
550
+ }
551
+
552
+ const { createPublicClient, http, base, baseSepolia, tempoModerato } = await loadDeps();
553
+ const mainnetClient = createPublicClient({ chain: base, transport: http() });
554
+ const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
555
+ const tempoClient = createPublicClient({ chain: tempoModerato, transport: http() });
556
+
557
+ const [mainnetUsdc, sepoliaUsdc, tempoPathUsd, billingRes] = await Promise.all([
558
+ readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
559
+ readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
560
+ readUsdcBalance(tempoClient, PATH_USD, w.address).catch(() => null),
561
+ getSdk().billing.checkBalance(w.address).catch(() => null),
562
+ ]);
563
+
564
+ console.log(JSON.stringify({
565
+ address: w.address,
566
+ rail: w.rail || "x402",
567
+ onchain: {
568
+ "base-mainnet_usd_micros": mainnetUsdc,
569
+ "base-sepolia_usd_micros": sepoliaUsdc,
570
+ "tempo-moderato_pathusd_micros": tempoPathUsd,
571
+ },
572
+ allowance_usd_micros: billingRes ? billingRes.allowance_usd_micros : null,
573
+ }, null, 2));
574
+ }
@@ -1,18 +1,18 @@
1
1
  /**
2
- * run402 webhook-secret — Manage the operator webhook signing secret.
2
+ * run402 webhook-secret — Manage your webhook signing secret.
3
3
  *
4
4
  * Split out from notifications.mjs so the SURFACE inventory parser sees
5
5
  * `webhook-secret:rotate` as its own command, separate from the
6
6
  * `notifications:*` group.
7
7
  */
8
8
 
9
- import { allowanceAuthHeaders } from "./config.mjs";
10
- import { operatorProofs } from "./operator-proofs.mjs";
9
+ import { walletAuthHeaders } from "./config.mjs";
10
+ import { sessionProofs } from "./session-proofs.mjs";
11
11
  import { getSdk } from "./sdk.mjs";
12
12
  import { reportSdkError } from "./sdk-errors.mjs";
13
13
  import { assertKnownFlags, normalizeArgv, failUnknownSubcommand } from "./argparse.mjs";
14
14
 
15
- const HELP = `run402 webhook-secret — Manage the operator webhook signing secret
15
+ const HELP = `run402 webhook-secret — Manage your webhook signing secret
16
16
 
17
17
  Usage:
18
18
  run402 webhook-secret rotate
@@ -20,7 +20,7 @@ Usage:
20
20
  Notes:
21
21
  - Returns the new plaintext secret EXACTLY once. Store it immediately.
22
22
  - Previous secret remains valid for 24 hours after rotation.
23
- - Requires operator_passkey assurance level.
23
+ - Requires passkey assurance ('run402 login', or 'run402 agent passkey enroll').
24
24
  `;
25
25
 
26
26
  export async function run(sub, args = []) {
@@ -39,7 +39,7 @@ export async function run(sub, args = []) {
39
39
  }
40
40
  const parsedArgs = normalizeArgv(args);
41
41
  assertKnownFlags(parsedArgs, ["--help", "-h"]);
42
- const proofs = operatorProofs("/agent/v1/webhook-secret/rotate");
42
+ const proofs = sessionProofs("/agent/v1/webhook-secret/rotate");
43
43
  try {
44
44
  const data = await getSdk().admin.rotateWebhookSecret(proofs);
45
45
  console.log(JSON.stringify(data, null, 2));
package/lib/whoami.mjs ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `run402 whoami` — who this CLI acts as: the principal, its org memberships,
3
+ * and the sign-in session (`session.grade`: `browser` | `loopback` | `device`,
4
+ * or `null` for a wallet caller) from `GET /agent/v1/whoami`, plus the write
5
+ * approvals cached on this machine. See sign-in.mjs.
6
+ */
7
+
8
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
9
+ import { getSdk } from "./sdk.mjs";
10
+ import { normalizeArgv, hasHelp, assertKnownFlags, positionalArgs } from "./argparse.mjs";
11
+ import { loadLiveControlPlaneSession, clearControlPlaneSession } from "../core-dist/control-plane-session.js";
12
+ import { readApprovals } from "../core-dist/write-approvals.js";
13
+
14
+ const HELP = `run402 whoami — who this CLI acts as
15
+
16
+ Usage:
17
+ run402 whoami
18
+
19
+ Calls GET /agent/v1/whoami. With a live sign-in session ('run402 login') it
20
+ reads as that session and reports session.grade: loopback (full), browser, or
21
+ device (read-only). Without one it reads as the active wallet and session is
22
+ null. The output also lists the write approvals cached on this machine
23
+ ('run402 approve'), each with its action, target, and expiry.
24
+ `;
25
+
26
+ /** Live cached write approvals: action, target, expiry. Never the token. */
27
+ function cachedApprovals(nowMs = Date.now()) {
28
+ try {
29
+ return readApprovals()
30
+ .filter((a) => a.expires_at > nowMs)
31
+ .map((a) => ({
32
+ action: a.action,
33
+ org_id: a.org_id ?? null,
34
+ project_id: a.project_id ?? null,
35
+ expires_at: new Date(a.expires_at).toISOString(),
36
+ }));
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ export async function run(args = []) {
43
+ args = normalizeArgv(args);
44
+ if (hasHelp(args)) {
45
+ console.log(HELP);
46
+ process.exit(0);
47
+ }
48
+ assertKnownFlags(args, ["--help", "-h"]);
49
+ const extra = positionalArgs(args, []);
50
+ if (extra.length > 0) {
51
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for whoami: ${extra[0]}`, hint: "Use `run402 whoami`." });
52
+ }
53
+ const sdk = getSdk();
54
+ const session = loadLiveControlPlaneSession();
55
+ let who;
56
+ try {
57
+ who = session
58
+ ? await sdk.session.whoami({ token: session.control_plane_session_token })
59
+ : await sdk.orgs.whoami();
60
+ } catch (err) {
61
+ // A cached session the gateway no longer honours (revoked or expired
62
+ // server-side): drop it so the next command does not replay a dead token.
63
+ if (session && err && err.status === 401) {
64
+ clearControlPlaneSession();
65
+ return fail({
66
+ code: "SESSION_INVALID",
67
+ message: "The cached sign-in session is no longer valid (revoked or expired).",
68
+ hint: "Run 'run402 login' to sign in again.",
69
+ next_actions: [{ type: "authenticate", command: "run402 login" }],
70
+ });
71
+ }
72
+ return reportSdkError(err);
73
+ }
74
+ console.log(JSON.stringify({ ...who, write_approvals: cachedApprovals() }, null, 2));
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.93.1",
3
+ "version": "4.94.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": {
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
- import { existsSync, renameSync, mkdirSync, chmodSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, renameSync, mkdirSync, chmodSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { describeRejectedValue } from "./redact.js";
6
6
  export const DEFAULT_API_BASE = "https://api.run402.com";
@@ -118,7 +118,7 @@ export function getProfilesDir() {
118
118
  * The effective config directory for the *active* wallet. The `default` wallet
119
119
  * resolves to the base dir (zero migration for existing single-wallet
120
120
  * installs); any named wallet resolves to `{base}/profiles/<name>`. Because
121
- * keystore, allowance, and meta paths all derive from this, switching the
121
+ * keystore, wallet, and meta paths all derive from this, switching the
122
122
  * profile env var moves the whole wallet bundle atomically — and the SDK/MCP
123
123
  * inherit profile selection for free.
124
124
  */
@@ -232,27 +232,61 @@ export function isCoreApiTarget() {
232
232
  return true;
233
233
  return false;
234
234
  }
235
- export function getAllowancePath() {
236
- if (process.env.RUN402_ALLOWANCE_PATH)
237
- return process.env.RUN402_ALLOWANCE_PATH;
238
- const dir = getConfigDir();
239
- const newPath = join(dir, "allowance.json");
240
- const oldPath = join(dir, "wallet.json");
241
- // Auto-migrate from wallet.json → allowance.json. renameSync preserves the
242
- // source file's mode, so a legacy world-readable wallet.json (mode 0644)
243
- // would otherwise carry that mode forward and leave the private key
244
- // world-readable on a shared machine. Tighten to 0600 after the rename.
245
- if (!existsSync(newPath) && existsSync(oldPath)) {
246
- mkdirSync(dir, { recursive: true });
247
- renameSync(oldPath, newPath);
235
+ /**
236
+ * The file that holds a wallet's key, in the base config dir (the `default`
237
+ * wallet) and in each `profiles/<name>/` directory.
238
+ */
239
+ export const WALLET_FILE_NAME = "wallet.json";
240
+ /** The name the wallet key file had before it was `wallet.json`. */
241
+ const PREVIOUS_WALLET_FILE_NAME = "allowance.json";
242
+ const walletFilesMovedFor = new Set();
243
+ /**
244
+ * One-time local move of the wallet key file to its current name: in the base
245
+ * config dir and every `profiles/<name>/` directory, `allowance.json` is
246
+ * renamed to `wallet.json` (an atomic same-directory rename) when
247
+ * `wallet.json` is absent, then tightened to 0600. A directory that already
248
+ * holds `wallet.json` is left untouched. Runs once per base dir per process;
249
+ * nothing reads the previous name afterwards.
250
+ */
251
+ export function moveWalletFiles() {
252
+ const base = getConfigBaseDir();
253
+ if (walletFilesMovedFor.has(base))
254
+ return;
255
+ walletFilesMovedFor.add(base);
256
+ const dirs = [base];
257
+ try {
258
+ for (const entry of readdirSync(join(base, "profiles"), { withFileTypes: true })) {
259
+ if (entry.isDirectory())
260
+ dirs.push(join(base, "profiles", entry.name));
261
+ }
262
+ }
263
+ catch {
264
+ // No profiles directory yet.
265
+ }
266
+ for (const dir of dirs) {
267
+ const from = join(dir, PREVIOUS_WALLET_FILE_NAME);
268
+ const to = join(dir, WALLET_FILE_NAME);
269
+ if (existsSync(to) || !existsSync(from))
270
+ continue;
271
+ try {
272
+ renameSync(from, to);
273
+ }
274
+ catch {
275
+ continue;
276
+ }
248
277
  try {
249
- chmodSync(newPath, 0o600);
278
+ chmodSync(to, 0o600);
250
279
  }
251
280
  catch {
252
281
  // Best-effort (e.g. Windows / exotic filesystems). Read-time self-heal
253
- // in readAllowance() is the backstop.
282
+ // in readWallet() is the backstop.
254
283
  }
255
284
  }
256
- return newPath;
285
+ }
286
+ export function getWalletPath() {
287
+ if (process.env.RUN402_WALLET_PATH)
288
+ return process.env.RUN402_WALLET_PATH;
289
+ moveWalletFiles();
290
+ return join(getConfigDir(), WALLET_FILE_NAME);
257
291
  }
258
292
  //# sourceMappingURL=config.js.map
@@ -10,6 +10,14 @@ export function getControlPlaneSessionPath() {
10
10
  return (process.env.RUN402_CONTROL_PLANE_SESSION_PATH ||
11
11
  join(getConfigBaseDir(), "control-plane-session.json"));
12
12
  }
13
+ /**
14
+ * Read a wire grade. An unknown or absent value is treated as `device` (the
15
+ * read-only grade) so a cache the client cannot classify never claims more
16
+ * authority than the gateway will grant.
17
+ */
18
+ function toGrade(raw) {
19
+ return raw === "browser" || raw === "loopback" || raw === "device" ? raw : "device";
20
+ }
13
21
  /** Tighten 0600 if group/other-readable, warning once. Best-effort, POSIX-only. */
14
22
  function selfHealPermissions(p) {
15
23
  if (process.platform === "win32")
@@ -50,20 +58,20 @@ export function readControlPlaneSession(path) {
50
58
  return null;
51
59
  }
52
60
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
53
- throw new Error("control-plane-session.json must contain a JSON object. Delete it and run 'run402 operator login --loopback' to recreate it.");
61
+ throw new Error("control-plane-session.json must contain a JSON object. Delete it and run 'run402 login' to recreate it.");
54
62
  }
55
63
  const data = parsed;
56
64
  if (typeof data.control_plane_session_token !== "string" ||
57
65
  data.control_plane_session_token.length === 0) {
58
- throw new Error("control-plane-session.json missing valid 'control_plane_session_token'. Run 'run402 operator login --loopback' to refresh it.");
66
+ throw new Error("control-plane-session.json missing valid 'control_plane_session_token'. Run 'run402 login' to refresh it.");
59
67
  }
60
68
  if (typeof data.expires_at !== "number" || !Number.isFinite(data.expires_at)) {
61
- throw new Error("control-plane-session.json missing valid 'expires_at'. Run 'run402 operator login --loopback' to refresh it.");
69
+ throw new Error("control-plane-session.json missing valid 'expires_at'. Run 'run402 login' to refresh it.");
62
70
  }
63
71
  return {
64
72
  control_plane_session_token: data.control_plane_session_token,
65
73
  token_type: typeof data.token_type === "string" ? data.token_type : "Bearer",
66
- provenance: typeof data.provenance === "string" ? data.provenance : "loopback_pkce",
74
+ grade: toGrade(data.grade),
67
75
  principal_id: typeof data.principal_id === "string" ? data.principal_id : "",
68
76
  amr: Array.isArray(data.amr) ? data.amr.filter((a) => typeof a === "string") : [],
69
77
  expires_at: data.expires_at,
@@ -79,7 +87,7 @@ export function saveControlPlaneSession(data, path) {
79
87
  renameSync(tmp, p);
80
88
  chmodSync(p, 0o600);
81
89
  }
82
- /** Delete the cached control-plane session — local half of `operator logout`. Idempotent. */
90
+ /** Delete the cached sign-in session — local half of `run402 logout`. Idempotent. */
83
91
  export function clearControlPlaneSession(path) {
84
92
  const p = path ?? getControlPlaneSessionPath();
85
93
  try {
@@ -105,10 +113,27 @@ export function controlPlaneSessionFromTokenResponse(resp, nowMs = Date.now()) {
105
113
  return {
106
114
  control_plane_session_token: resp.control_plane_session_token,
107
115
  token_type: resp.token_type ?? "Bearer",
108
- provenance: resp.provenance ?? "loopback_pkce",
116
+ grade: toGrade(resp.grade),
109
117
  principal_id: resp.principal_id ?? "",
110
118
  amr: Array.isArray(resp.amr) ? resp.amr.filter((a) => typeof a === "string") : [],
111
119
  expires_at: nowMs + (typeof resp.expires_in === "number" ? resp.expires_in : 0) * 1000,
112
120
  };
113
121
  }
122
+ /**
123
+ * Delete the session caches of the retired email-scoped session
124
+ * (`{base}/operator-session.json`) and the retired approval cache file
125
+ * (`{base}/write-auth-session.json`). Neither is read any more; `run402 login`
126
+ * and `run402 logout` call this so a stale token does not linger on disk.
127
+ * Best-effort and idempotent.
128
+ */
129
+ export function clearRetiredSessionCaches() {
130
+ for (const name of ["operator-session.json", "write-auth-session.json"]) {
131
+ try {
132
+ rmSync(join(getConfigBaseDir(), name), { force: true });
133
+ }
134
+ catch {
135
+ // Best-effort: a failed unlink never blocks login or logout.
136
+ }
137
+ }
138
+ }
114
139
  //# sourceMappingURL=control-plane-session.js.map