@jaw.id/cli 0.1.25 → 0.2.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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +4 -0
  4. package/dist/base-command.js +3 -1
  5. package/dist/base-command.js.map +1 -1
  6. package/dist/commands/config/set.js +140 -12
  7. package/dist/commands/config/set.js.map +1 -1
  8. package/dist/commands/config/show.js +3 -1
  9. package/dist/commands/config/show.js.map +1 -1
  10. package/dist/commands/config/write.js +6 -4
  11. package/dist/commands/config/write.js.map +1 -1
  12. package/dist/commands/disconnect.js +24 -10
  13. package/dist/commands/disconnect.js.map +1 -1
  14. package/dist/commands/mcp/index.js +2426 -94
  15. package/dist/commands/mcp/index.js.map +1 -1
  16. package/dist/commands/rpc/call.js +197 -45
  17. package/dist/commands/rpc/call.js.map +1 -1
  18. package/dist/commands/session/add.js +1547 -0
  19. package/dist/commands/session/add.js.map +1 -0
  20. package/dist/commands/session/revoke.js +181 -54
  21. package/dist/commands/session/revoke.js.map +1 -1
  22. package/dist/commands/session/setup.js +516 -65
  23. package/dist/commands/session/setup.js.map +1 -1
  24. package/dist/commands/session/status.js +315 -6
  25. package/dist/commands/session/status.js.map +1 -1
  26. package/dist/commands/version.js +3 -1
  27. package/dist/commands/version.js.map +1 -1
  28. package/dist/commands/x402/log.js +344 -0
  29. package/dist/commands/x402/log.js.map +1 -0
  30. package/dist/commands/x402/pay.js +2122 -0
  31. package/dist/commands/x402/pay.js.map +1 -0
  32. package/dist/commands/x402/status.js +1047 -0
  33. package/dist/commands/x402/status.js.map +1 -0
  34. package/dist/index.js +41 -14
  35. package/dist/index.js.map +1 -1
  36. package/dist/lib/bridge-singleton.js +41 -14
  37. package/dist/lib/bridge-singleton.js.map +1 -1
  38. package/dist/lib/config.js +26 -3
  39. package/dist/lib/config.js.map +1 -1
  40. package/dist/lib/keystore.js +13 -2
  41. package/dist/lib/keystore.js.map +1 -1
  42. package/dist/lib/paths.js +3 -1
  43. package/dist/lib/paths.js.map +1 -1
  44. package/dist/lib/payment-lock.js +121 -0
  45. package/dist/lib/payment-lock.js.map +1 -0
  46. package/dist/lib/session-bridge.js +148 -24
  47. package/dist/lib/session-bridge.js.map +1 -1
  48. package/dist/lib/session-config.js +78 -11
  49. package/dist/lib/session-config.js.map +1 -1
  50. package/dist/lib/terminal.js +22 -0
  51. package/dist/lib/terminal.js.map +1 -0
  52. package/dist/lib/validation.js +3 -3
  53. package/dist/lib/validation.js.map +1 -1
  54. package/dist/lib/ws-bridge.js +22 -10
  55. package/dist/lib/ws-bridge.js.map +1 -1
  56. package/dist/mcp/handlers/config.js +73 -6
  57. package/dist/mcp/handlers/config.js.map +1 -1
  58. package/dist/mcp/handlers/daemon.js +43 -12
  59. package/dist/mcp/handlers/daemon.js.map +1 -1
  60. package/dist/mcp/handlers/resources.js +119 -0
  61. package/dist/mcp/handlers/resources.js.map +1 -1
  62. package/dist/mcp/handlers/rpc.js +269 -60
  63. package/dist/mcp/handlers/rpc.js.map +1 -1
  64. package/dist/mcp/helpers.js +50 -3
  65. package/dist/mcp/helpers.js.map +1 -1
  66. package/dist/mcp/server.js +2426 -94
  67. package/dist/mcp/server.js.map +1 -1
  68. package/dist/mcp/tools.js +43 -3
  69. package/dist/mcp/tools.js.map +1 -1
  70. package/dist/x402/log-view.js +160 -0
  71. package/dist/x402/log-view.js.map +1 -0
  72. package/dist/x402/status-report.js +90 -0
  73. package/dist/x402/status-report.js.map +1 -0
  74. package/oclif.manifest.json +398 -4
  75. package/package.json +8 -3
@@ -0,0 +1,1047 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import 'crypto';
6
+ import { privateKeyToAccount } from 'viem/accounts';
7
+ import { parseAbi, formatUnits, erc20Abi, createPublicClient, http, zeroAddress, BaseError, ContractFunctionRevertedError } from 'viem';
8
+ import 'viem/experimental/erc7739';
9
+ import { polygonAmoy, polygon, baseSepolia, base } from 'viem/chains';
10
+
11
+ // src/base-command.ts
12
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
13
+ var PATHS = {
14
+ root: JAW_DIR,
15
+ config: path.join(JAW_DIR, "config.json"),
16
+ session: path.join(JAW_DIR, "session.json"),
17
+ relay: path.join(JAW_DIR, "relay.json"),
18
+ keystore: path.join(JAW_DIR, "keystore.json"),
19
+ sessionConfig: path.join(JAW_DIR, "session-config.json"),
20
+ x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
21
+ paymentLock: path.join(JAW_DIR, "x402-payment.lock")
22
+ };
23
+
24
+ // src/lib/config.ts
25
+ function ensureDir(dir) {
26
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
27
+ fs.chmodSync(dir, 448);
28
+ }
29
+ function migrateConfig(config) {
30
+ if (config.paymasterUrl && !config.paymasters) {
31
+ const chainId = config.defaultChain ?? 1;
32
+ config.paymasters = { [chainId]: { url: config.paymasterUrl } };
33
+ delete config.paymasterUrl;
34
+ saveConfig(config);
35
+ }
36
+ return config;
37
+ }
38
+ function loadConfig() {
39
+ if (!fs.existsSync(PATHS.config)) {
40
+ return {};
41
+ }
42
+ const raw = fs.readFileSync(PATHS.config, "utf-8");
43
+ try {
44
+ const config = JSON.parse(raw);
45
+ return migrateConfig(config);
46
+ } catch {
47
+ throw new Error(
48
+ `Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
49
+ );
50
+ }
51
+ }
52
+ function saveConfig(config) {
53
+ ensureDir(PATHS.root);
54
+ fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
55
+ encoding: "utf-8",
56
+ mode: 384
57
+ });
58
+ }
59
+
60
+ // src/lib/output.ts
61
+ function formatOutput(data, format) {
62
+ if (format === "json") {
63
+ return JSON.stringify(data, replaceBigInt, 2);
64
+ }
65
+ return formatHuman(data);
66
+ }
67
+ function replaceBigInt(_key, value) {
68
+ if (typeof value === "bigint") {
69
+ return value.toString();
70
+ }
71
+ return value;
72
+ }
73
+ function formatHuman(data, indent = 0) {
74
+ if (data === null || data === void 0) {
75
+ return "null";
76
+ }
77
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean" || typeof data === "bigint") {
78
+ return String(data);
79
+ }
80
+ if (Array.isArray(data)) {
81
+ if (data.length === 0) return "(empty)";
82
+ return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join("\n");
83
+ }
84
+ if (typeof data === "object") {
85
+ const entries = Object.entries(data);
86
+ if (entries.length === 0) return "(empty)";
87
+ const pad = " ".repeat(indent);
88
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
89
+ return entries.map(([key, val]) => {
90
+ const paddedKey = key.padEnd(maxKeyLen);
91
+ const valStr = typeof val === "object" && val !== null ? "\n" + formatHuman(val, indent + 2) : String(val);
92
+ return `${pad}${paddedKey} ${valStr}`;
93
+ }).join("\n");
94
+ }
95
+ return String(data);
96
+ }
97
+
98
+ // src/base-command.ts
99
+ var BaseCommand = class extends Command {
100
+ static baseFlags = {
101
+ output: Flags.string({
102
+ char: "o",
103
+ description: "Output format",
104
+ options: ["json", "human"],
105
+ default: "human",
106
+ env: "JAW_OUTPUT"
107
+ }),
108
+ chain: Flags.integer({
109
+ char: "c",
110
+ description: "Chain ID",
111
+ env: "JAW_CHAIN_ID"
112
+ }),
113
+ "api-key": Flags.string({
114
+ description: "JAW API key",
115
+ env: "JAW_API_KEY"
116
+ }),
117
+ yes: Flags.boolean({
118
+ char: "y",
119
+ description: "Skip confirmations (for AI agents)",
120
+ default: false
121
+ }),
122
+ quiet: Flags.boolean({
123
+ char: "q",
124
+ description: "Suppress non-essential output",
125
+ default: false
126
+ })
127
+ };
128
+ resolveApiKey(flags) {
129
+ const apiKey = flags["api-key"] ?? loadConfig().apiKey;
130
+ if (!apiKey) {
131
+ this.error("API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`");
132
+ }
133
+ return apiKey;
134
+ }
135
+ resolveChainId(flags) {
136
+ const chainId = flags.chain ?? loadConfig().defaultChain;
137
+ if (!chainId) {
138
+ this.error("Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`");
139
+ }
140
+ return chainId;
141
+ }
142
+ outputResult(data, format) {
143
+ const output = formatOutput(data, format);
144
+ this.log(output);
145
+ }
146
+ };
147
+ function loadSessionKey() {
148
+ if (!fs.existsSync(PATHS.keystore)) {
149
+ throw new Error("No session configured. Run `jaw session setup` first.");
150
+ }
151
+ const contents = fs.readFileSync(PATHS.keystore, "utf-8");
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(contents);
155
+ } catch {
156
+ throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \`jaw session setup\` to recreate it.`);
157
+ }
158
+ return parsed.privateKey;
159
+ }
160
+ function keystoreExists() {
161
+ return fs.existsSync(PATHS.keystore);
162
+ }
163
+ var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
164
+ var SELECTOR_RE = /^0x[0-9a-fA-F]{8}$/;
165
+ var HEX_RE = /^0x[0-9a-fA-F]+$/;
166
+ var ALLOWANCE_RE = /^(0x[0-9a-fA-F]+|[0-9]+)$/;
167
+ var SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
168
+ function isPositiveInt(value) {
169
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
170
+ }
171
+ function parseGrantedPermission(raw) {
172
+ if (typeof raw !== "object" || raw === null) return void 0;
173
+ const r = raw;
174
+ const { account, spender, salt } = r;
175
+ if (typeof account !== "string" || !ADDRESS_RE.test(account)) return void 0;
176
+ if (typeof spender !== "string" || !ADDRESS_RE.test(spender)) return void 0;
177
+ if (typeof salt !== "string" || !HEX_RE.test(salt)) return void 0;
178
+ if (!isPositiveInt(r.start) || !isPositiveInt(r.end)) return void 0;
179
+ if (!Array.isArray(r.calls) || r.calls.length === 0) return void 0;
180
+ const calls = [];
181
+ for (const entry of r.calls) {
182
+ if (typeof entry !== "object" || entry === null) return void 0;
183
+ const { target, selector } = entry;
184
+ if (typeof target !== "string" || !ADDRESS_RE.test(target)) return void 0;
185
+ if (typeof selector !== "string" || !SELECTOR_RE.test(selector)) return void 0;
186
+ calls.push({ target, selector });
187
+ }
188
+ if (!Array.isArray(r.spends)) return void 0;
189
+ const spends = [];
190
+ for (const entry of r.spends) {
191
+ if (typeof entry !== "object" || entry === null) return void 0;
192
+ const { token, allowance, unit, multiplier } = entry;
193
+ if (typeof token !== "string" || !ADDRESS_RE.test(token)) return void 0;
194
+ if (typeof allowance !== "string" || !ALLOWANCE_RE.test(allowance)) return void 0;
195
+ if (typeof unit !== "string" || !SPEND_UNITS.has(unit)) return void 0;
196
+ if (!isPositiveInt(multiplier) || multiplier > 65535) return void 0;
197
+ spends.push({ token, allowance, unit, multiplier });
198
+ }
199
+ return { account, spender, start: r.start, end: r.end, salt, calls, spends };
200
+ }
201
+ function isLegacySession(config) {
202
+ return config.mode !== "eip7702";
203
+ }
204
+ function liveOrphans(orphans, now = Date.now() / 1e3) {
205
+ return (orphans ?? []).filter((orphan) => orphan.expiry > now);
206
+ }
207
+ function writeSessionConfig(config) {
208
+ ensureDir(PATHS.root);
209
+ const temp = `${PATHS.sessionConfig}.${process.pid}.tmp`;
210
+ fs.writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
211
+ fs.chmodSync(temp, 384);
212
+ fs.renameSync(temp, PATHS.sessionConfig);
213
+ }
214
+ function saveRecoveredPermission(config, permission) {
215
+ const current = tryLoadSessionConfig();
216
+ if (!current || current.permissionId !== config.permissionId) return false;
217
+ writeSessionConfig({ ...current, permission });
218
+ return true;
219
+ }
220
+ function loadSessionConfig() {
221
+ if (!fs.existsSync(PATHS.sessionConfig)) {
222
+ throw new Error("No session configured. Run `jaw session setup` first.");
223
+ }
224
+ const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
225
+ try {
226
+ return JSON.parse(raw);
227
+ } catch {
228
+ throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
229
+ }
230
+ }
231
+ function tryLoadSessionConfig() {
232
+ try {
233
+ return loadSessionConfig();
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ // src/x402/asset-registry.ts
240
+ var USDC_BY_NETWORK = {
241
+ "eip155:8453": {
242
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
243
+ chainId: 8453,
244
+ wireNetwork: "eip155:8453",
245
+ usdcName: "USD Coin",
246
+ usdcVersion: "2",
247
+ decimals: 6
248
+ },
249
+ "eip155:84532": {
250
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
251
+ chainId: 84532,
252
+ wireNetwork: "eip155:84532",
253
+ usdcName: "USDC",
254
+ usdcVersion: "2",
255
+ decimals: 6
256
+ },
257
+ "eip155:137": {
258
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
259
+ chainId: 137,
260
+ wireNetwork: "eip155:137",
261
+ usdcName: "USD Coin",
262
+ usdcVersion: "2",
263
+ decimals: 6
264
+ },
265
+ "eip155:80002": {
266
+ address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
267
+ chainId: 80002,
268
+ wireNetwork: "eip155:80002",
269
+ usdcName: "USDC",
270
+ usdcVersion: "2",
271
+ decimals: 6
272
+ }
273
+ };
274
+ function usdcForNetwork(network) {
275
+ return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
276
+ }
277
+ var JAW_RPC_URL = "https://api.justaname.id/proxy/v1/rpc";
278
+ var CHAINS = {
279
+ [base.id]: base,
280
+ [baseSepolia.id]: baseSepolia,
281
+ [polygon.id]: polygon,
282
+ [polygonAmoy.id]: polygonAmoy
283
+ };
284
+ for (const chainId of Object.values(USDC_BY_NETWORK).map((a) => a.chainId)) {
285
+ if (!CHAINS[chainId]) {
286
+ throw new Error(
287
+ `x402 balance: USDC registry has chain ${chainId} but no viem chain is mapped for it in balance.ts`
288
+ );
289
+ }
290
+ }
291
+ var clients = /* @__PURE__ */ new Map();
292
+ function rpcTransport(chainId, apiKey) {
293
+ if (!apiKey) return http();
294
+ return http(`${JAW_RPC_URL}?chainId=${chainId}&api-key=${apiKey}`);
295
+ }
296
+ function publicClientFor(chainId) {
297
+ const chain = CHAINS[chainId];
298
+ if (!chain) throw new Error(`x402: no viem chain configured for chainId ${chainId}`);
299
+ const apiKey = loadConfig().apiKey;
300
+ const key = `${chainId}:${apiKey ?? ""}`;
301
+ let client = clients.get(key);
302
+ if (!client) {
303
+ client = createPublicClient({ chain, transport: rpcTransport(chainId, apiKey) });
304
+ clients.set(key, client);
305
+ }
306
+ return client;
307
+ }
308
+ var readOnChain = (asset, owner) => publicClientFor(asset.chainId).readContract({
309
+ address: asset.address,
310
+ abi: erc20Abi,
311
+ functionName: "balanceOf",
312
+ args: [owner]
313
+ });
314
+ async function usdcBalance(network, owner, read = readOnChain) {
315
+ const asset = usdcForNetwork(network);
316
+ if (!asset) throw new Error(`Unsupported x402 network: ${network}`);
317
+ const raw = await read(asset, owner);
318
+ return { network, asset: asset.address, raw: raw.toString(), formatted: formatUnits(raw, asset.decimals) };
319
+ }
320
+
321
+ // src/x402/payer.ts
322
+ parseAbi(["function allowance(address owner, address spender) view returns (uint256)"]);
323
+ parseAbi([
324
+ "function eip712Domain() view returns (bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)"
325
+ ]);
326
+ function sessionPayerAddress() {
327
+ if (!keystoreExists()) {
328
+ throw new Error("No session key. Run `jaw session setup` first.");
329
+ }
330
+ return privateKeyToAccount(loadSessionKey()).address;
331
+ }
332
+ function readX402Log(limit) {
333
+ let raw;
334
+ try {
335
+ raw = fs.readFileSync(PATHS.x402Log, "utf-8");
336
+ } catch {
337
+ return [];
338
+ }
339
+ const entries = raw.split("\n").filter((line) => line.trim().length > 0).map((line) => {
340
+ try {
341
+ return JSON.parse(line);
342
+ } catch {
343
+ return null;
344
+ }
345
+ }).filter((e) => e !== null);
346
+ return limit && limit > 0 ? entries.slice(-limit) : entries;
347
+ }
348
+ function spendFigureOf(entry) {
349
+ if (entry.status !== "paid" && entry.status !== "failed") return 0n;
350
+ const parse = (value) => {
351
+ if (!value) return 0n;
352
+ try {
353
+ const parsed = BigInt(value);
354
+ return parsed > 0n ? parsed : 0n;
355
+ } catch {
356
+ return 0n;
357
+ }
358
+ };
359
+ if (entry.status === "paid") return parse(entry.amount);
360
+ const ceiling = parse(entry.authorized);
361
+ const charge = parse(entry.amount);
362
+ return ceiling > charge ? ceiling : charge;
363
+ }
364
+ function sumSpentSince(payerAddress, since) {
365
+ const payer = payerAddress.toLowerCase();
366
+ return readX402Log().reduce((total, entry) => {
367
+ if (entry.payer?.toLowerCase() !== payer) return total;
368
+ if (since && entry.at < since) return total;
369
+ return total + spendFigureOf(entry);
370
+ }, 0n);
371
+ }
372
+ function sumToppedUpSince(payerAddress, since) {
373
+ const payer = payerAddress.toLowerCase();
374
+ return readX402Log().reduce((total, entry) => {
375
+ if (!entry.topUpAmount) return total;
376
+ if (entry.payer?.toLowerCase() !== payer) return total;
377
+ if (since && entry.at < since) return total;
378
+ try {
379
+ return total + BigInt(entry.topUpAmount);
380
+ } catch {
381
+ return total;
382
+ }
383
+ }, 0n);
384
+ }
385
+
386
+ // src/x402/amount.ts
387
+ function parseBigInt(value) {
388
+ if (value === void 0 || value === null || value === "") return null;
389
+ try {
390
+ return BigInt(value);
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
395
+
396
+ // src/x402/period.ts
397
+ var PERIOD_UNITS = ["minute", "hour", "day", "week", "month", "forever"];
398
+ function isPeriodUnit(value) {
399
+ return typeof value === "string" && PERIOD_UNITS.includes(value);
400
+ }
401
+ function normalizePeriod(unit, multiplier) {
402
+ const m = Math.max(1, Math.floor(multiplier ?? 1));
403
+ if (unit === "year") return { unit: "month", multiplier: m * 12 };
404
+ if (isPeriodUnit(unit)) return { unit, multiplier: m };
405
+ return void 0;
406
+ }
407
+ var FIXED_UNIT_SECONDS = {
408
+ minute: 60,
409
+ hour: 3600,
410
+ day: 86400,
411
+ week: 604800
412
+ };
413
+ function addMonths(unixSeconds, months) {
414
+ const d = new Date(unixSeconds * 1e3);
415
+ const day = d.getUTCDate();
416
+ const target = new Date(
417
+ Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds())
418
+ );
419
+ const daysInTarget = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
420
+ target.setUTCDate(Math.min(day, daysInTarget));
421
+ return Math.floor(target.getTime() / 1e3);
422
+ }
423
+ function currentPeriodWindow(input) {
424
+ const { anchor, unit, now, permissionEnd } = input;
425
+ const multiplier = Math.max(1, Math.floor(input.multiplier ?? 1));
426
+ if (unit === "forever") {
427
+ return { start: anchor, end: permissionEnd };
428
+ }
429
+ let start;
430
+ let end;
431
+ if (unit === "month") {
432
+ let index = 0;
433
+ let cursor = anchor;
434
+ let next = addMonths(anchor, multiplier);
435
+ while (next <= now) {
436
+ index += 1;
437
+ cursor = next;
438
+ next = addMonths(anchor, (index + 1) * multiplier);
439
+ }
440
+ start = cursor;
441
+ end = next;
442
+ } else {
443
+ const duration = FIXED_UNIT_SECONDS[unit] * multiplier;
444
+ const elapsed = Math.max(0, now - anchor);
445
+ const index = Math.floor(elapsed / duration);
446
+ start = anchor + index * duration;
447
+ end = start + duration;
448
+ }
449
+ return { start, end: Math.min(end, permissionEnd) };
450
+ }
451
+ function describePeriod(unit, multiplier) {
452
+ if (unit === "forever") return "the whole permission";
453
+ return describeSpendPeriod(unit, multiplier);
454
+ }
455
+ function describeSpendPeriod(unit, multiplier) {
456
+ const n = Math.max(1, Math.floor(multiplier ?? 1));
457
+ return n === 1 ? unit : `${n} ${unit}s`;
458
+ }
459
+
460
+ // src/x402/policy.ts
461
+ var DEFAULT_X402_POLICY = {
462
+ maxAmountPerPayment: "1000000",
463
+ // 1 USDC per payment
464
+ maxTotalPerSession: "10000000",
465
+ // 10 USDC per process
466
+ allowedAssets: Object.values(USDC_BY_NETWORK).map((asset) => asset.address),
467
+ allowedNetworks: Object.keys(USDC_BY_NETWORK)
468
+ };
469
+ function policyFromPermission(permission, chainId) {
470
+ if (!permission) return {};
471
+ const usdc = Object.values(USDC_BY_NETWORK).find((asset) => asset.chainId === chainId);
472
+ if (!usdc) return {};
473
+ const forToken = permission.spends.filter((spend) => spend.token.toLowerCase() === usdc.address.toLowerCase());
474
+ if (forToken.length === 0) return {};
475
+ const start = new Date(permission.start * 1e3);
476
+ if (Number.isNaN(start.getTime())) return {};
477
+ const anchor = start.toISOString();
478
+ const perPeriod = [];
479
+ for (const spend of forToken) {
480
+ let allowance;
481
+ try {
482
+ const parsed = BigInt(spend.allowance);
483
+ if (parsed < 0n) continue;
484
+ allowance = parsed.toString();
485
+ } catch {
486
+ continue;
487
+ }
488
+ const period = normalizePeriod(spend.unit, spend.multiplier);
489
+ if (!period) continue;
490
+ perPeriod.push({ allowance, unit: period.unit, multiplier: period.multiplier, anchor });
491
+ }
492
+ if (perPeriod.length === 0) return {};
493
+ return {
494
+ // The registry's canonical address, not the permission's literal string:
495
+ // they match case-insensitively and this seeds an allowlist compared that
496
+ // way.
497
+ allowedAssets: [usdc.address],
498
+ allowedNetworks: [usdc.wireNetwork],
499
+ perPeriod
500
+ };
501
+ }
502
+ function resolveX402Policy(configPolicy, grantPolicy) {
503
+ const merged = { ...DEFAULT_X402_POLICY, ...grantPolicy ?? {}, ...configPolicy ?? {} };
504
+ if (grantPolicy?.perPeriod !== void 0 && configPolicy?.maxTotalPerSession === void 0) {
505
+ delete merged.maxTotalPerSession;
506
+ }
507
+ return merged;
508
+ }
509
+ function resolveSessionX402Policy(configPolicy, session) {
510
+ return resolveX402Policy(configPolicy, policyFromPermission(session?.permission, session?.chainId ?? 0));
511
+ }
512
+ function sameLimit(a, b) {
513
+ return a.unit === b.unit && a.multiplier === b.multiplier && a.allowance === b.allowance;
514
+ }
515
+ var PERMISSION_MANAGER_ABI = parseAbi([
516
+ "struct CallPermission { address target; bytes4 selector; address checker; }",
517
+ "struct SpendLimit { address token; uint160 allowance; uint8 unit; uint16 multiplier; }",
518
+ "struct Permission { address account; address spender; uint48 start; uint48 end; uint256 salt; CallPermission[] calls; SpendLimit[] spends; }",
519
+ "struct PeriodSpend { uint48 start; uint48 end; uint160 spend; }",
520
+ "function getHash(Permission permission) view returns (bytes32)",
521
+ "function isApproved(Permission permission) view returns (bool)",
522
+ "function isRevoked(Permission permission) view returns (bool)",
523
+ "function getCurrentPeriod(Permission permission, SpendLimit spendLimit) view returns (PeriodSpend)",
524
+ // Carried so the two time-bound reverts can be told apart from a node that
525
+ // did not answer. Everything else the manager can revert with decodes to an
526
+ // unnamed error, which is treated as unavailable rather than guessed at.
527
+ "error JustaPermissionManager_BeforePermissionStart(uint48 currentTimestamp, uint48 start)",
528
+ "error JustaPermissionManager_AfterPermissionEnd(uint48 currentTimestamp, uint48 end)"
529
+ ]);
530
+ var TIME_BOUND_ERRORS = /* @__PURE__ */ new Set([
531
+ "JustaPermissionManager_BeforePermissionStart",
532
+ "JustaPermissionManager_AfterPermissionEnd"
533
+ ]);
534
+ var PERIOD_UNIT_ENUM = {
535
+ minute: 0,
536
+ hour: 1,
537
+ day: 2,
538
+ week: 3,
539
+ month: 4,
540
+ forever: 5
541
+ };
542
+ function toContractSpendLimit(spend) {
543
+ const unit = spend.unit === "year" ? "month" : spend.unit;
544
+ const multiplier = spend.unit === "year" ? spend.multiplier * 12 : spend.multiplier;
545
+ if (!Object.hasOwn(PERIOD_UNIT_ENUM, unit)) return null;
546
+ return {
547
+ token: spend.token,
548
+ allowance: BigInt(spend.allowance),
549
+ unit: PERIOD_UNIT_ENUM[unit],
550
+ multiplier
551
+ };
552
+ }
553
+ function toContractPermission(permission) {
554
+ const spends = [];
555
+ for (const spend of permission.spends) {
556
+ const converted = toContractSpendLimit(spend);
557
+ if (!converted) return null;
558
+ spends.push(converted);
559
+ }
560
+ let salt;
561
+ try {
562
+ salt = BigInt(permission.salt);
563
+ } catch {
564
+ return null;
565
+ }
566
+ return {
567
+ account: permission.account,
568
+ spender: permission.spender,
569
+ start: permission.start,
570
+ end: permission.end,
571
+ salt,
572
+ calls: permission.calls.map((call) => ({
573
+ target: call.target,
574
+ selector: call.selector,
575
+ checker: zeroAddress
576
+ })),
577
+ spends
578
+ };
579
+ }
580
+ var DEFAULT_TIMEOUT_MS = 5e3;
581
+ async function within(work, timeoutMs = DEFAULT_TIMEOUT_MS) {
582
+ let timer;
583
+ try {
584
+ const expired = new Promise((_, reject) => {
585
+ timer = setTimeout(() => reject(new Error("timed out")), timeoutMs);
586
+ });
587
+ return await Promise.race([work, expired]);
588
+ } finally {
589
+ clearTimeout(timer);
590
+ }
591
+ }
592
+ async function managerAddress(override) {
593
+ if (override) return override;
594
+ const { PERMISSIONS_MANAGER_ADDRESS } = await import('@jaw.id/core');
595
+ return PERMISSIONS_MANAGER_ADDRESS;
596
+ }
597
+ function reader(chainId, deps) {
598
+ if (deps.readContract) return deps.readContract;
599
+ try {
600
+ const client = publicClientFor(chainId);
601
+ return (args) => client.readContract(args);
602
+ } catch {
603
+ return null;
604
+ }
605
+ }
606
+ async function readPermissionState(target, deps = {}) {
607
+ if (!target.permission) return { status: "unavailable" };
608
+ const permission = toContractPermission(target.permission);
609
+ if (!permission) return { status: "unavailable" };
610
+ const read = reader(target.chainId, deps);
611
+ if (!read) return { status: "unavailable" };
612
+ try {
613
+ const address = await managerAddress(deps.manager);
614
+ const [hash, approved, revoked] = await within(
615
+ Promise.all([
616
+ read({ address, abi: PERMISSION_MANAGER_ABI, functionName: "getHash", args: [permission] }),
617
+ read({ address, abi: PERMISSION_MANAGER_ABI, functionName: "isApproved", args: [permission] }),
618
+ read({ address, abi: PERMISSION_MANAGER_ABI, functionName: "isRevoked", args: [permission] })
619
+ ]),
620
+ deps.timeoutMs
621
+ );
622
+ if (typeof hash !== "string" || hash.toLowerCase() !== target.permissionId.toLowerCase()) {
623
+ return { status: "mismatch" };
624
+ }
625
+ return { status: "ok", approved: approved === true, revoked: revoked === true };
626
+ } catch {
627
+ return { status: "unavailable" };
628
+ }
629
+ }
630
+ async function readCurrentPeriods(target, deps = {}) {
631
+ if (!target.permission) return [];
632
+ const permission = toContractPermission(target.permission);
633
+ if (!permission) return [];
634
+ const granted = target.permission.spends;
635
+ const indexes = granted.map((spend, index) => ({ spend, index })).filter(({ spend }) => spend.token.toLowerCase() === target.token.toLowerCase());
636
+ if (indexes.length === 0) return [];
637
+ const unreadable = indexes.map(({ spend }) => ({ ...spend, period: { status: "unavailable" } }));
638
+ const read = reader(target.chainId, deps);
639
+ if (!read) return unreadable;
640
+ try {
641
+ const address = await managerAddress(deps.manager);
642
+ const settled = await within(
643
+ Promise.allSettled([
644
+ read({ address, abi: PERMISSION_MANAGER_ABI, functionName: "getHash", args: [permission] }),
645
+ ...indexes.map(
646
+ ({ index }) => read({
647
+ address,
648
+ abi: PERMISSION_MANAGER_ABI,
649
+ functionName: "getCurrentPeriod",
650
+ args: [permission, permission.spends[index]]
651
+ })
652
+ )
653
+ ]),
654
+ deps.timeoutMs
655
+ );
656
+ const [hashed, ...counters] = settled;
657
+ const hash = hashed.status === "fulfilled" ? hashed.value : null;
658
+ if (typeof hash !== "string" || hash.toLowerCase() !== target.permissionId.toLowerCase()) {
659
+ return unreadable;
660
+ }
661
+ return indexes.map(({ spend }, i) => {
662
+ const result = counters[i];
663
+ if (result.status === "rejected") {
664
+ return {
665
+ ...spend,
666
+ period: isTimeBoundRevert(result.reason) ? { status: "outside-window" } : { status: "unavailable" }
667
+ };
668
+ }
669
+ const period = result.value;
670
+ return {
671
+ ...spend,
672
+ period: period ? {
673
+ status: "ok",
674
+ start: Number(period.start),
675
+ end: Number(period.end),
676
+ spend: BigInt(period.spend)
677
+ } : { status: "unavailable" }
678
+ };
679
+ });
680
+ } catch {
681
+ return unreadable;
682
+ }
683
+ }
684
+ function isTimeBoundRevert(err) {
685
+ if (!(err instanceof BaseError)) return false;
686
+ const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
687
+ return revert instanceof ContractFunctionRevertedError && TIME_BOUND_ERRORS.has(revert.data?.errorName ?? "");
688
+ }
689
+ async function readLiveness(session, deps = {}) {
690
+ const state = await readPermissionState(session, deps);
691
+ if (state.status === "unavailable") return "unknown";
692
+ if (state.status === "mismatch") return "mismatch";
693
+ if (state.revoked) return "revoked";
694
+ return state.approved ? "active" : "unapproved";
695
+ }
696
+
697
+ // src/x402/spend-window.ts
698
+ function currentLimitUsage(policy, payerAddress, session, now = /* @__PURE__ */ new Date()) {
699
+ if (!session || !policy.perPeriod) return [];
700
+ const usage = [];
701
+ for (const limit of policy.perPeriod) {
702
+ const anchorMs = Date.parse(limit.anchor);
703
+ if (Number.isNaN(anchorMs)) continue;
704
+ const window = currentPeriodWindow({
705
+ anchor: Math.floor(anchorMs / 1e3),
706
+ unit: limit.unit,
707
+ multiplier: limit.multiplier,
708
+ now: Math.floor(now.getTime() / 1e3),
709
+ permissionEnd: session.expiry
710
+ });
711
+ const since = new Date(window.start * 1e3).toISOString();
712
+ usage.push({
713
+ ...limit,
714
+ spent: sumSpentSince(payerAddress, since),
715
+ toppedUp: sumToppedUpSince(payerAddress, since),
716
+ endsAt: new Date(window.end * 1e3),
717
+ source: "ledger"
718
+ });
719
+ }
720
+ return usage;
721
+ }
722
+ async function currentLimitUsageOnChain(policy, payerAddress, session, now = /* @__PURE__ */ new Date(), deps = {}) {
723
+ const local = currentLimitUsage(policy, payerAddress, session, now);
724
+ if (!session || local.length === 0) return local;
725
+ const asset = Object.values(USDC_BY_NETWORK).find((a) => a.chainId === session.chainId);
726
+ if (!asset) return local;
727
+ const onChain = await readCurrentPeriods(
728
+ {
729
+ chainId: session.chainId,
730
+ permissionId: session.permissionId,
731
+ permission: session.permission,
732
+ token: asset.address
733
+ },
734
+ deps
735
+ );
736
+ if (onChain.length === 0) return local;
737
+ return local.map((limit) => {
738
+ const match = onChain.find((candidate) => {
739
+ const normalized = normalizePeriod(candidate.unit, candidate.multiplier);
740
+ if (normalized?.unit !== limit.unit || normalized.multiplier !== limit.multiplier) return false;
741
+ const a = parseBigInt(candidate.allowance);
742
+ const b = parseBigInt(limit.allowance);
743
+ return a !== null && b !== null && a === b;
744
+ });
745
+ if (!match || match.period.status !== "ok") return limit;
746
+ const since = new Date(match.period.start * 1e3).toISOString();
747
+ const fromLedger = sumToppedUpSince(payerAddress, since);
748
+ const metered = match.period.spend >= fromLedger;
749
+ return {
750
+ ...limit,
751
+ spent: sumSpentSince(payerAddress, since),
752
+ toppedUp: metered ? match.period.spend : fromLedger,
753
+ endsAt: new Date(match.period.end * 1e3),
754
+ source: metered ? "chain" : "ledger"
755
+ };
756
+ });
757
+ }
758
+
759
+ // src/x402/gas-reserve.ts
760
+ function gasReserve(asset) {
761
+ return 10n ** BigInt(asset.decimals) / 10n;
762
+ }
763
+
764
+ // src/lib/terminal.ts
765
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
766
+ var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
767
+ var REPLACEMENT = "\uFFFD";
768
+ var DEFAULT_LINE_LENGTH = 200;
769
+ function bound(text, maxLength) {
770
+ if (text.length <= maxLength) return text;
771
+ return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
772
+ }
773
+ function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
774
+ const text = typeof value === "string" ? value : String(value);
775
+ return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
776
+ }
777
+
778
+ // src/x402/status-report.ts
779
+ function formatUsdc(base2, decimals) {
780
+ if (base2 === void 0) return "unlimited";
781
+ const value = parseBigInt(base2);
782
+ if (value === null) return `${sanitizeLine(base2, 32)} (invalid)`;
783
+ const scale = 10n ** BigInt(decimals);
784
+ const whole = value / scale;
785
+ const frac = (value % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
786
+ return `${whole}${frac ? `.${frac}` : ""} USDC`;
787
+ }
788
+ function formatRemaining(seconds) {
789
+ const days = Math.floor(seconds / 86400);
790
+ if (days > 0) return `${days} day${days === 1 ? "" : "s"} left`;
791
+ const hours = Math.max(0, Math.floor(seconds / 3600));
792
+ return `${hours}h left`;
793
+ }
794
+ function diagnose(facts) {
795
+ const problems = [];
796
+ if (facts.expired) {
797
+ problems.push("The session expired. Run `jaw session setup --x402`.");
798
+ }
799
+ if (facts.liveness === "revoked") {
800
+ problems.push(
801
+ "The permission was revoked on chain, so nothing can be pulled through it any more. Run `jaw session setup --x402` to grant a new one."
802
+ );
803
+ }
804
+ if (facts.liveness === "unapproved") {
805
+ problems.push(
806
+ "The chain has no record of this permission being approved. If the session was just created, the grant may not have been mined yet; otherwise run `jaw session setup --x402`."
807
+ );
808
+ }
809
+ if (facts.outdated) {
810
+ problems.push(
811
+ "This session was created by an older CLI and cannot pay: its permission belongs to an address separate from the session key. Run `jaw session setup --x402` to recreate it."
812
+ );
813
+ }
814
+ if (!facts.hasAsset) {
815
+ problems.push("This chain has no USDC configured, so x402 payments cannot be made on it.");
816
+ }
817
+ if (facts.hasAsset && facts.ownerBalance === null) {
818
+ problems.push(
819
+ facts.payerBalance === null ? "Could not read balances. Check the API key and network." : `Could not read the owner balance for ${facts.ownerAddress}. The address may be malformed.`
820
+ );
821
+ }
822
+ if (facts.ownerBalance !== null && Number(facts.ownerBalance) === 0) {
823
+ const payerHoldsMoreThanItsGas = facts.payerBalance !== null && Number(facts.payerBalance) > (facts.payerReserve ?? 0);
824
+ problems.push(
825
+ payerHoldsMoreThanItsGas ? "The owner account is empty but the payer holds USDC. Payments will work, but they bypass the permission, so the cap you granted is not applying. Move the funds to the owner." : "The owner account holds no USDC, so there is nothing to pay with."
826
+ );
827
+ }
828
+ if (facts.periodCap != null && facts.periodSpent != null && facts.periodSpent >= facts.periodCap) {
829
+ problems.push(
830
+ `The granted allowance for this ${facts.periodLabel ?? "period"} is used up. It resets at the end of the window, or grant a new permission with \`jaw session setup --x402\`.`
831
+ );
832
+ }
833
+ if (facts.sessionCap !== null && facts.spent >= facts.sessionCap) {
834
+ problems.push(
835
+ "The session cap is used up. Raise it with `jaw config set x402.maxTotalPerSession <base units>` or start a new session."
836
+ );
837
+ }
838
+ return problems;
839
+ }
840
+
841
+ // src/x402/permission-recovery.ts
842
+ var RECOVERY_TIMEOUT_MS = 5e3;
843
+ async function recoverPermission(session, apiKey, deps = {}) {
844
+ if (session.permission) return session.permission;
845
+ if (!apiKey) return void 0;
846
+ const fetchPermission = deps.fetchPermission ?? (async (id, key) => {
847
+ const { getPermissionFromRelay } = await import('@jaw.id/core');
848
+ return getPermissionFromRelay(id, key);
849
+ });
850
+ let timer;
851
+ try {
852
+ const expired = new Promise((_, reject) => {
853
+ timer = setTimeout(() => reject(new Error("timed out")), deps.timeoutMs ?? RECOVERY_TIMEOUT_MS);
854
+ });
855
+ const relayed = await Promise.race([fetchPermission(session.permissionId, apiKey), expired]);
856
+ const permission = parseGrantedPermission(relayed);
857
+ if (!permission) return void 0;
858
+ if (permission.account.toLowerCase() !== session.ownerAddress.toLowerCase() || permission.spender.toLowerCase() !== session.sessionAddress.toLowerCase() || permission.end !== session.expiry) {
859
+ return void 0;
860
+ }
861
+ return saveRecoveredPermission(session, permission) ? permission : void 0;
862
+ } catch {
863
+ return void 0;
864
+ } finally {
865
+ clearTimeout(timer);
866
+ }
867
+ }
868
+
869
+ // src/commands/x402/status.ts
870
+ var X402Status = class _X402Status extends BaseCommand {
871
+ static description = "Show x402 payment readiness: which account holds the funds, the resolved caps, and what has been spent. Reads only, never pays.";
872
+ static examples = ["<%= config.bin %> x402 status", "<%= config.bin %> x402 status --output json"];
873
+ static flags = {
874
+ ...BaseCommand.baseFlags
875
+ };
876
+ async run() {
877
+ const { flags } = await this.parse(_X402Status);
878
+ const format = flags.output;
879
+ const session = keystoreExists() ? tryLoadSessionConfig() : null;
880
+ if (!session) {
881
+ if (format === "json") {
882
+ this.outputResult({ ready: false, reason: "no session" }, format);
883
+ return;
884
+ }
885
+ this.log("No session. Run `jaw session setup --x402` to create one.");
886
+ return;
887
+ }
888
+ const config = loadConfig();
889
+ const now = Date.now() / 1e3;
890
+ const expired = session.expiry <= now;
891
+ const asset = Object.values(USDC_BY_NETWORK).find((a) => a.chainId === session.chainId);
892
+ const payer = sessionPayerAddress();
893
+ const stillLive = liveOrphans(session.orphanedPermissions, now);
894
+ const [balances, recovered] = await Promise.all([
895
+ Promise.all(
896
+ [session.ownerAddress, payer].map(async (address) => {
897
+ if (!asset) return null;
898
+ try {
899
+ return (await usdcBalance(asset.wireNetwork, address)).formatted;
900
+ } catch {
901
+ return null;
902
+ }
903
+ })
904
+ ),
905
+ // Recovered first for a session written before the struct was stored,
906
+ // which is otherwise stuck reporting "cannot tell" forever.
907
+ recoverPermission(session, config.apiKey)
908
+ ]);
909
+ const [ownerBalance, payerBalance] = balances;
910
+ const current = recovered ? { ...session, permission: recovered } : session;
911
+ const liveness = await readLiveness(current);
912
+ const policy = resolveSessionX402Policy(config.x402, current);
913
+ const spent = sumSpentSince(payer, session.createdAt);
914
+ const sessionCap = parseBigInt(policy.maxTotalPerSession);
915
+ const decimals = asset?.decimals ?? 6;
916
+ const usage = await currentLimitUsageOnChain(policy, payer, current);
917
+ const limits = (policy.perPeriod ?? []).map((limit) => {
918
+ const measured = usage.find((entry) => sameLimit(entry, limit));
919
+ return measured ?? {
920
+ ...limit,
921
+ spent: 0n,
922
+ toppedUp: 0n,
923
+ endsAt: null,
924
+ source: "unmeasured"
925
+ };
926
+ });
927
+ const remaining = (limit) => {
928
+ const cap = parseBigInt(limit.allowance);
929
+ if (cap === null) return null;
930
+ return cap > limit.toppedUp ? cap - limit.toppedUp : 0n;
931
+ };
932
+ const tightest = limits.reduce((a, b) => {
933
+ const left = remaining(b);
934
+ if (left === null) return a;
935
+ const best = a === null ? null : remaining(a);
936
+ return best === null || left < best ? b : a;
937
+ }, null);
938
+ const problems = diagnose({
939
+ expired,
940
+ liveness,
941
+ ownerAddress: session.ownerAddress,
942
+ ownerBalance,
943
+ payerBalance,
944
+ hasAsset: asset !== void 0,
945
+ spent,
946
+ sessionCap,
947
+ periodCap: tightest ? parseBigInt(tightest.allowance) : null,
948
+ // Top-ups, not payments: the period cap mirrors the on-chain allowance
949
+ // and the top-up is what draws it down, exactly as `topUpCeiling`
950
+ // measures it. Payments lag by whatever float the payer still holds,
951
+ // which kept this check quiet while the grant was already drained.
952
+ // Null, not the zero an unmeasured limit carries: `diagnose` compares it
953
+ // against the cap, and a figure nobody read is not a measurement of zero.
954
+ periodSpent: tightest && tightest.endsAt !== null ? tightest.toppedUp : null,
955
+ periodLabel: tightest ? describePeriod(tightest.unit, tightest.multiplier) : null,
956
+ outdated: isLegacySession(session),
957
+ // Same units as the formatted balances. Exact in a double: the reserve
958
+ // is a tenth of a token, six decimals at most.
959
+ payerReserve: asset ? Number(gasReserve(asset)) / 10 ** asset.decimals : 0
960
+ });
961
+ if (format === "json") {
962
+ this.outputResult(
963
+ {
964
+ ready: problems.length === 0,
965
+ problems,
966
+ chainId: session.chainId,
967
+ permission: { id: session.permissionId, onChain: liveness },
968
+ ...stillLive.length > 0 ? { stillLiveOnChain: stillLive } : {},
969
+ owner: { address: session.ownerAddress, usdc: ownerBalance },
970
+ payer: { address: payer, usdc: payerBalance },
971
+ policy: {
972
+ maxAmountPerPayment: policy.maxAmountPerPayment,
973
+ maxTotalPerSession: policy.maxTotalPerSession,
974
+ // Every one of them: the contract charges all, and naming one as
975
+ // the budget is what this set out to stop.
976
+ perPeriod: limits.map((limit) => ({
977
+ allowance: limit.allowance,
978
+ unit: limit.unit,
979
+ multiplier: limit.multiplier,
980
+ used: limit.endsAt === null ? null : limit.toppedUp.toString(),
981
+ usedFrom: limit.source,
982
+ resetsAt: limit.endsAt === null ? null : limit.endsAt.toISOString()
983
+ })),
984
+ topUpFloat: policy.topUpFloat
985
+ },
986
+ spentThisSession: spent.toString(),
987
+ expiry: session.expiry,
988
+ expired
989
+ },
990
+ format
991
+ );
992
+ return;
993
+ }
994
+ this.log(expired ? "Session expired.\n" : "Session active.\n");
995
+ this.log(` owner ${session.ownerAddress} ${fmt(ownerBalance)} <- funds go here`);
996
+ this.log(` payer ${payer} ${fmt(payerBalance)}`);
997
+ this.log("");
998
+ this.log(` chain ${session.chainId}${asset ? "" : " (no USDC configured for this chain)"}`);
999
+ if (liveness !== "unknown") {
1000
+ this.log(` perm ${session.permissionId} ${LIVENESS_LABEL[liveness]}`);
1001
+ }
1002
+ if (stillLive.length > 0) {
1003
+ this.log(
1004
+ ` ${stillLive.length} permission${stillLive.length === 1 ? "" : "s"} from earlier sessions still live on this account; \`jaw session revoke\` removes them too`
1005
+ );
1006
+ }
1007
+ this.log(` caps ${formatUsdc(policy.maxAmountPerPayment, decimals)} per payment`);
1008
+ for (const limit of limits) {
1009
+ const floor = limit.source === "chain" ? "" : "at least ";
1010
+ const window = limit.endsAt === null ? " (usage unknown)" : ` (resets ${limit.endsAt.toISOString()})`;
1011
+ const used = limit.endsAt === null ? "?" : `${floor}${formatUsdc(limit.toppedUp.toString(), decimals)}`;
1012
+ this.log(
1013
+ ` ${used} of ${formatUsdc(limit.allowance, decimals)} used this ${describePeriod(limit.unit, limit.multiplier)}${window}`
1014
+ );
1015
+ }
1016
+ if (limits.length > 1) {
1017
+ this.log(" all of them apply, so the tightest is what binds");
1018
+ }
1019
+ this.log(
1020
+ ` at least ${formatUsdc(spent.toString(), decimals)} of ${formatUsdc(policy.maxTotalPerSession, decimals)} spent this session`
1021
+ );
1022
+ this.log(" the session figure is counted from this CLI's ledger, which a direct jaw_rpc send bypasses");
1023
+ if (policy.topUpFloat) {
1024
+ this.log(` float tops the payer up to ${formatUsdc(policy.topUpFloat, decimals)} when it runs short`);
1025
+ }
1026
+ this.log(
1027
+ ` expires ${new Date(session.expiry * 1e3).toISOString()}${expired ? "" : ` (${formatRemaining(session.expiry - now)})`}`
1028
+ );
1029
+ if (problems.length > 0) {
1030
+ this.log("");
1031
+ for (const problem of problems) this.log(` ! ${problem}`);
1032
+ }
1033
+ }
1034
+ };
1035
+ function fmt(balance) {
1036
+ return balance === null ? " ? " : `${balance} USDC`;
1037
+ }
1038
+ var LIVENESS_LABEL = {
1039
+ active: "live on chain",
1040
+ revoked: "REVOKED on chain",
1041
+ unapproved: "not approved on chain",
1042
+ mismatch: "does not match the stored permission"
1043
+ };
1044
+
1045
+ export { X402Status as default };
1046
+ //# sourceMappingURL=status.js.map
1047
+ //# sourceMappingURL=status.js.map