@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
package/dist/mcp/tools.js CHANGED
@@ -8,16 +8,56 @@ var rpcMethodSchema = {
8
8
  params: z.any().optional().describe(
9
9
  "Method parameters \u2014 structure varies by method. Read the jaw://api-reference/{method} resource for the expected format."
10
10
  ),
11
- chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
11
+ chainId: z.number().int().positive().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
12
12
  session: z.boolean().optional().describe(
13
- "Sign with the local session key instead of opening the browser (requires `jaw session setup`; check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, wallet_sendCalls, wallet_getCallsStatus, personal_sign, eth_signTypedData_v4. Defaults to the JAW_SESSION env var."
13
+ "Sign with the local session key instead of opening the browser (requires `jaw session setup`; check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, wallet_sendCalls, wallet_getCallsStatus. personal_sign and eth_signTypedData_v4 are browser only: a signature made by the session key never passes the spend caps or the ledger. Defaults to the JAW_SESSION env var."
14
14
  )
15
15
  };
16
16
  var configSetSchema = {
17
17
  key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl", "sessionExpiry"]).describe("Config key"),
18
18
  value: z.string().describe("Config value")
19
19
  };
20
+ var httpUrl = z.string().url().refine(
21
+ (u) => {
22
+ try {
23
+ const p = new URL(u).protocol;
24
+ return p === "http:" || p === "https:";
25
+ } catch {
26
+ return false;
27
+ }
28
+ },
29
+ { message: "url must be http(s) \u2014 other schemes (file:, data:, javascript:, ftp:) are not fetched" }
30
+ );
31
+ var payAndFetchSchema = {
32
+ url: httpUrl.describe("Resource URL to fetch (http/https only). If it answers HTTP 402 (x402), pay and retry."),
33
+ method: z.string().optional().describe("HTTP method (default GET)."),
34
+ headers: z.record(z.string()).optional().describe("Extra request headers."),
35
+ body: z.string().optional().describe("Request body (for POST/PUT/etc.)."),
36
+ maxAmount: z.string().optional().describe(
37
+ "Hard ceiling for THIS call, in the asset base units (e.g. 6-decimals for USDC). If the 402 asks for more, the payment is refused, not made."
38
+ ),
39
+ asset: z.string().optional().describe("Require a specific asset contract address."),
40
+ network: z.string().optional().describe("Require a specific CAIP-2 network, e.g. eip155:8453 (Base).")
41
+ };
42
+ var discoverSchema = {
43
+ query: z.string().max(400).optional().describe(
44
+ 'Keyword or natural-language search over the x402 Bazaar catalog of paid services (e.g. "ens resolver", "weather api", "token price"). Required unless `payTo` is set.'
45
+ ),
46
+ network: z.string().optional().describe("CAIP-2 network to prefer when picking the price to show, e.g. eip155:8453 (Base, default)."),
47
+ maxUsdPrice: z.string().optional().describe("Only return services priced at or below this many USD per call."),
48
+ curatedOnly: z.boolean().optional().describe("Only return Coinbase-curated (health-probed) services."),
49
+ limit: z.number().int().min(1).max(20).optional().describe("Maximum results to return (1-20, default 10)."),
50
+ payTo: z.string().optional().describe(
51
+ "Instead of searching, list every service registered by this seller address (0x\u2026). Takes precedence over `query` if both are given."
52
+ )
53
+ };
54
+ var x402LogSchema = {
55
+ limit: z.number().optional().describe("Return only the most recent N ledger entries (default: all).")
56
+ };
57
+ var x402BalanceSchema = {
58
+ network: z.string().optional().describe("CAIP-2 network to check the USDC balance on, e.g. eip155:8453 (Base) or eip155:84532 (Base Sepolia).")
59
+ };
20
60
 
21
- export { configSetSchema, rpcMethodSchema };
61
+ export { configSetSchema, discoverSchema, payAndFetchSchema, rpcMethodSchema, x402BalanceSchema, x402LogSchema };
22
62
  //# sourceMappingURL=tools.js.map
23
63
  //# sourceMappingURL=tools.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mcp/tools.ts"],"names":[],"mappings":";;;AAMO,IAAM,eAAA,GAAkB;AAAA,EAC7B,MAAA,EAAQ,CAAA,CACL,MAAA,EAAO,CACP,QAAA;AAAA,IACC;AAAA,GAEF;AAAA,EACF,MAAA,EAAQ,CAAA,CACL,GAAA,EAAI,CACJ,UAAS,CACT,QAAA;AAAA,IACC;AAAA,GAEF;AAAA,EACF,SAAS,CAAA,CACN,MAAA,GACA,QAAA,EAAS,CACT,SAAS,kGAAkG,CAAA;AAAA,EAC9G,OAAA,EAAS,CAAA,CACN,OAAA,EAAQ,CACR,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAKN;AAEO,IAAM,eAAA,GAAkB;AAAA,EAC7B,GAAA,EAAK,CAAA,CAAE,IAAA,CAAK,CAAC,QAAA,EAAU,cAAA,EAAgB,SAAA,EAAW,KAAA,EAAO,UAAA,EAAY,eAAe,CAAC,CAAA,CAAE,SAAS,YAAY,CAAA;AAAA,EAC5G,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,SAAS,cAAc;AAC3C","file":"tools.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * Single generic RPC method schema.\n * Accepts any EIP-1193 RPC method and forwards to JAWProvider.\n */\nexport const rpcMethodSchema = {\n method: z\n .string()\n .describe(\n 'EIP-1193 RPC method name (e.g. wallet_connect, wallet_sendCalls, personal_sign). ' +\n 'Read the jaw://api-reference resource for the full list and jaw://api-reference/{method} for parameter details.'\n ),\n params: z\n .any()\n .optional()\n .describe(\n 'Method parameters — structure varies by method. ' +\n 'Read the jaw://api-reference/{method} resource for the expected format.'\n ),\n chainId: z\n .number()\n .optional()\n .describe('Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia'),\n session: z\n .boolean()\n .optional()\n .describe(\n 'Sign with the local session key instead of opening the browser (requires `jaw session setup`; ' +\n 'check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, ' +\n 'wallet_sendCalls, wallet_getCallsStatus, personal_sign, eth_signTypedData_v4. ' +\n 'Defaults to the JAW_SESSION env var.'\n ),\n};\n\nexport const configSetSchema = {\n key: z.enum(['apiKey', 'defaultChain', 'keysUrl', 'ens', 'relayUrl', 'sessionExpiry']).describe('Config key'),\n value: z.string().describe('Config value'),\n};\n"]}
1
+ {"version":3,"sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"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","file":"tools.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * Single generic RPC method schema.\n * Accepts any EIP-1193 RPC method and forwards to JAWProvider.\n */\nexport const rpcMethodSchema = {\n method: z\n .string()\n .describe(\n 'EIP-1193 RPC method name (e.g. wallet_connect, wallet_sendCalls, personal_sign). ' +\n 'Read the jaw://api-reference resource for the full list and jaw://api-reference/{method} for parameter details.'\n ),\n params: z\n .any()\n .optional()\n .describe(\n 'Method parameters — structure varies by method. ' +\n 'Read the jaw://api-reference/{method} resource for the expected format.'\n ),\n chainId: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia'),\n session: z\n .boolean()\n .optional()\n .describe(\n 'Sign with the local session key instead of opening the browser (requires `jaw session setup`; ' +\n 'check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, ' +\n 'wallet_sendCalls, wallet_getCallsStatus. personal_sign and eth_signTypedData_v4 are browser ' +\n 'only: a signature made by the session key never passes the spend caps or the ledger. ' +\n 'Defaults to the JAW_SESSION env var.'\n ),\n};\n\nexport const configSetSchema = {\n key: z.enum(['apiKey', 'defaultChain', 'keysUrl', 'ens', 'relayUrl', 'sessionExpiry']).describe('Config key'),\n value: z.string().describe('Config value'),\n};\n\nconst httpUrl = z\n .string()\n .url()\n .refine(\n (u) => {\n try {\n const p = new URL(u).protocol;\n return p === 'http:' || p === 'https:';\n } catch {\n return false;\n }\n },\n { message: 'url must be http(s) — other schemes (file:, data:, javascript:, ftp:) are not fetched' }\n );\n\nexport const payAndFetchSchema = {\n url: httpUrl.describe('Resource URL to fetch (http/https only). If it answers HTTP 402 (x402), pay and retry.'),\n method: z.string().optional().describe('HTTP method (default GET).'),\n headers: z.record(z.string()).optional().describe('Extra request headers.'),\n body: z.string().optional().describe('Request body (for POST/PUT/etc.).'),\n maxAmount: z\n .string()\n .optional()\n .describe(\n 'Hard ceiling for THIS call, in the asset base units (e.g. 6-decimals for USDC). ' +\n 'If the 402 asks for more, the payment is refused, not made.'\n ),\n asset: z.string().optional().describe('Require a specific asset contract address.'),\n network: z.string().optional().describe('Require a specific CAIP-2 network, e.g. eip155:8453 (Base).'),\n};\n\nexport const discoverSchema = {\n query: z\n .string()\n .max(400)\n .optional()\n .describe(\n 'Keyword or natural-language search over the x402 Bazaar catalog of paid services ' +\n '(e.g. \"ens resolver\", \"weather api\", \"token price\"). Required unless `payTo` is set.'\n ),\n network: z\n .string()\n .optional()\n .describe('CAIP-2 network to prefer when picking the price to show, e.g. eip155:8453 (Base, default).'),\n maxUsdPrice: z.string().optional().describe('Only return services priced at or below this many USD per call.'),\n curatedOnly: z.boolean().optional().describe('Only return Coinbase-curated (health-probed) services.'),\n limit: z.number().int().min(1).max(20).optional().describe('Maximum results to return (1-20, default 10).'),\n payTo: z\n .string()\n .optional()\n .describe(\n 'Instead of searching, list every service registered by this seller address (0x…). ' +\n 'Takes precedence over `query` if both are given.'\n ),\n};\n\nexport const x402LogSchema = {\n limit: z.number().optional().describe('Return only the most recent N ledger entries (default: all).'),\n};\n\nexport const x402BalanceSchema = {\n network: z\n .string()\n .optional()\n .describe('CAIP-2 network to check the USDC balance on, e.g. eip155:8453 (Base) or eip155:84532 (Base Sepolia).'),\n};\n"]}
@@ -0,0 +1,160 @@
1
+ import 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ // src/x402/amount.ts
6
+ function parseBigInt(value) {
7
+ if (value === void 0 || value === null || value === "") return null;
8
+ try {
9
+ return BigInt(value);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ // src/lib/terminal.ts
16
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
17
+ var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
18
+ var REPLACEMENT = "\uFFFD";
19
+ var DEFAULT_LINE_LENGTH = 200;
20
+ function bound(text, maxLength) {
21
+ if (text.length <= maxLength) return text;
22
+ return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
23
+ }
24
+ function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
25
+ const text = typeof value === "string" ? value : String(value);
26
+ return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
27
+ }
28
+
29
+ // src/x402/status-report.ts
30
+ function formatUsdc(base, decimals) {
31
+ if (base === void 0) return "unlimited";
32
+ const value = parseBigInt(base);
33
+ if (value === null) return `${sanitizeLine(base, 32)} (invalid)`;
34
+ const scale = 10n ** BigInt(decimals);
35
+ const whole = value / scale;
36
+ const frac = (value % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
37
+ return `${whole}${frac ? `.${frac}` : ""} USDC`;
38
+ }
39
+
40
+ // src/x402/asset-registry.ts
41
+ var USDC_BY_NETWORK = {
42
+ "eip155:8453": {
43
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
44
+ chainId: 8453,
45
+ wireNetwork: "eip155:8453",
46
+ usdcName: "USD Coin",
47
+ usdcVersion: "2",
48
+ decimals: 6
49
+ },
50
+ "eip155:84532": {
51
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
52
+ chainId: 84532,
53
+ wireNetwork: "eip155:84532",
54
+ usdcName: "USDC",
55
+ usdcVersion: "2",
56
+ decimals: 6
57
+ },
58
+ "eip155:137": {
59
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
60
+ chainId: 137,
61
+ wireNetwork: "eip155:137",
62
+ usdcName: "USD Coin",
63
+ usdcVersion: "2",
64
+ decimals: 6
65
+ },
66
+ "eip155:80002": {
67
+ address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
68
+ chainId: 80002,
69
+ wireNetwork: "eip155:80002",
70
+ usdcName: "USDC",
71
+ usdcVersion: "2",
72
+ decimals: 6
73
+ }
74
+ };
75
+ function usdcForNetwork(network) {
76
+ return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
77
+ }
78
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
79
+ ({
80
+ config: path.join(JAW_DIR, "config.json"),
81
+ session: path.join(JAW_DIR, "session.json"),
82
+ relay: path.join(JAW_DIR, "relay.json"),
83
+ keystore: path.join(JAW_DIR, "keystore.json"),
84
+ sessionConfig: path.join(JAW_DIR, "session-config.json"),
85
+ x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
86
+ paymentLock: path.join(JAW_DIR, "x402-payment.lock")
87
+ });
88
+
89
+ // src/x402/ledger.ts
90
+ function spendFigureOf(entry) {
91
+ if (entry.status !== "paid" && entry.status !== "failed") return 0n;
92
+ const parse = (value) => {
93
+ if (!value) return 0n;
94
+ try {
95
+ const parsed = BigInt(value);
96
+ return parsed > 0n ? parsed : 0n;
97
+ } catch {
98
+ return 0n;
99
+ }
100
+ };
101
+ if (entry.status === "paid") return parse(entry.amount);
102
+ const ceiling = parse(entry.authorized);
103
+ const charge = parse(entry.amount);
104
+ return ceiling > charge ? ceiling : charge;
105
+ }
106
+
107
+ // src/x402/log-view.ts
108
+ function decimalsOf(entry) {
109
+ return (entry.network ? usdcForNetwork(entry.network)?.decimals : void 0) ?? 6;
110
+ }
111
+ function hostOf(url) {
112
+ try {
113
+ return sanitizeLine(new URL(url).host, 80);
114
+ } catch {
115
+ return sanitizeLine(url, 80);
116
+ }
117
+ }
118
+ function renderEntry(entry) {
119
+ const when = sanitizeLine(String(entry.at).replace("T", " ").slice(0, 19), 19);
120
+ const counted = spendFigureOf(entry);
121
+ const amount = entry.amount || entry.authorized ? formatUsdc(counted.toString(), decimalsOf(entry)) : "";
122
+ const head = ` ${when} ${sanitizeLine(entry.status, 7).padEnd(7)} ${amount.padStart(12)} ${hostOf(entry.url)}`;
123
+ const detail = [];
124
+ if (entry.topUpAmount) {
125
+ detail.push(`topped up ${formatUsdc(entry.topUpAmount, decimalsOf(entry))}`);
126
+ }
127
+ if (entry.approvalBatchId) detail.push("granted Permit2 its allowance");
128
+ if (entry.txHash) detail.push(sanitizeLine(entry.txHash, 80));
129
+ if (entry.status === "failed" && entry.nonce) detail.push(`nonce ${sanitizeLine(entry.nonce, 80)}`);
130
+ if (entry.reason) detail.push(sanitizeLine(entry.reason, 200));
131
+ return detail.length > 0 ? `${head}
132
+ ${" ".repeat(24)}${detail.join(" ")}` : head;
133
+ }
134
+ function renderSummary(entries) {
135
+ const counts = { paid: 0, failed: 0, refused: 0 };
136
+ const spentByScale = /* @__PURE__ */ new Map();
137
+ let unknown = 0;
138
+ for (const entry of entries) {
139
+ if (Object.hasOwn(counts, entry.status)) counts[entry.status] += 1;
140
+ else unknown += 1;
141
+ const counted = spendFigureOf(entry);
142
+ if (counted > 0n) {
143
+ try {
144
+ const decimals = decimalsOf(entry);
145
+ spentByScale.set(decimals, (spentByScale.get(decimals) ?? 0n) + counted);
146
+ } catch {
147
+ }
148
+ }
149
+ }
150
+ const parts = [`${counts.paid} paid`];
151
+ if (counts.failed > 0) parts.push(`${counts.failed} failed`);
152
+ if (counts.refused > 0) parts.push(`${counts.refused} refused`);
153
+ if (unknown > 0) parts.push(`${unknown} unreadable`);
154
+ const totals = spentByScale.size === 0 ? formatUsdc("0", 6) : [...spentByScale.entries()].map(([decimals, spent]) => formatUsdc(spent.toString(), decimals)).join(" + ");
155
+ return ` ${parts.join(", ")}, ${totals} out`;
156
+ }
157
+
158
+ export { decimalsOf, hostOf, renderEntry, renderSummary };
159
+ //# sourceMappingURL=log-view.js.map
160
+ //# sourceMappingURL=log-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/x402/amount.ts","../../src/lib/terminal.ts","../../src/x402/status-report.ts","../../src/x402/asset-registry.ts","../../src/lib/paths.ts","../../src/x402/ledger.ts","../../src/x402/log-view.ts"],"names":[],"mappings":"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","file":"log-view.js","sourcesContent":["/**\n * Canonical base-unit amount parser. Every place that turns an amount string\n * (from an untrusted 402 challenge or from hand-edited config) into a bigint\n * goes through here, so the failure surface is uniform and a malformed value\n * can never crash a hot path. Sign policy is left to the caller: this returns\n * the parsed value (which may be negative) or null when it is not an integer.\n */\nexport function parseBigInt(value: string | undefined | null): bigint | null {\n // Empty string is \"unset\", not zero: BigInt('') is 0n, but an absent config\n // value must read as null so callers treat it as \"no bound\", not \"cap of 0\".\n if (value === undefined || value === null || value === '') return null;\n try {\n return BigInt(value);\n } catch {\n return null;\n }\n}\n\n/** Parse a non-negative base-unit amount; undefined when absent, invalid, or negative. */\nexport function parseNonNegativeBigInt(value: string | undefined | null): bigint | undefined {\n const parsed = parseBigInt(value);\n return parsed !== null && parsed >= 0n ? parsed : undefined;\n}\n","/**\n * Make server-controlled text safe to print.\n *\n * A paid endpoint controls its response body, and parts of its 402 challenge\n * reach our refusal messages, which are stored in the payment ledger and\n * reprinted by `jaw x402 log`. Printed raw, an escape sequence lets that server\n * erase the line the CLI just wrote and paint its own in place: a resource that\n * was never paid for can render a convincing green \"Paid. 5 USDC\" the CLI never\n * emitted, and once in the ledger it does so on every later read.\n *\n * Two shapes, because the danger differs. A newline inside a one-line field\n * forges an extra row that reads as a genuine record, while inside a response\n * body it is just a newline. Only the human renderers need any of this: JSON\n * output escapes control characters on its own.\n */\n\n/**\n * Characters that are invisible or reorder what follows them, in any context.\n *\n * Bidi overrides (U+202A-U+202E, U+2066-U+2069) are the Trojan Source trick:\n * they flip rendering direction, so an address can display as something other\n * than the bytes that were signed. The zero-width family hides text outright,\n * splitting an address to the eye while leaving it intact to a copy. Neither is\n * a control character in the C0 or C1 sense, so stripping those alone misses\n * both. U+2028 and U+2029 break lines the way a newline does.\n */\nconst INVISIBLE_AND_BIDI = /[\\u200B-\\u200F\\u2028\\u2029\\u202A-\\u202E\\u2066-\\u2069\\uFEFF]/g;\n\n/** C0 except tab and newline, DEL, and the C1 block. For multi-line text. */\n// eslint-disable-next-line no-control-regex\nconst BLOCK_CONTROLS = /[\\u0000-\\u0008\\u000B-\\u001F\\u007F-\\u009F]/g;\n\n/** Every C0 control, DEL and C1. For text that must stay on one line. */\n// eslint-disable-next-line no-control-regex\nconst LINE_CONTROLS = /[\\u0000-\\u001F\\u007F-\\u009F]/g;\n\n/** Left in place of a stripped character, so tampering shows rather than vanishing. */\nconst REPLACEMENT = '\\uFFFD';\n\nexport const DEFAULT_LINE_LENGTH = 200;\n\nfunction bound(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n // Say it was cut. A silently truncated error message reads as a complete one.\n return `${text.slice(0, maxLength)}\\u2026 (${text.length - maxLength} more characters)`;\n}\n\n/**\n * Disarm a value that has to render as a single line: a refusal reason, a\n * network id, a host, a transaction hash.\n *\n * Newlines go with everything else here. A reason carrying one would otherwise\n * open a second line under a log entry, indistinguishable from the real row\n * above it, which is the same forgery the escape sequences allow.\n */\nexport function sanitizeLine(value: unknown, maxLength: number = DEFAULT_LINE_LENGTH): string {\n const text = typeof value === 'string' ? value : String(value);\n return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);\n}\n\n/**\n * Disarm a response body, keeping its shape.\n *\n * Newlines and tabs survive, since a body is legitimately multi-line and\n * neither can move the cursor back over text already written. Nothing is\n * truncated: the caller asked for this resource and may well have paid for it,\n * and the read is already bounded upstream by MAX_BODY_BYTES.\n */\nexport function sanitizeBlock(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value);\n return text.replace(BLOCK_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT);\n}\n","import { parseBigInt } from './amount.js';\nimport { sanitizeLine } from '../lib/terminal.js';\nimport type { PermissionLiveness } from './permission-onchain.js';\n\n/**\n * Presentation and diagnosis for `jaw x402 status`, kept apart from the command\n * so the rules can be tested without a session, a keystore or a network.\n */\n\n/** Base units to a readable amount, trailing zeros trimmed. */\nexport function formatUsdc(base: string | undefined, decimals: number): string {\n if (base === undefined) return 'unlimited';\n const value = parseBigInt(base);\n // Echoing the raw value put an unvalidated string on screen: amounts reach\n // here from the ledger and from config, both files that can be edited.\n if (value === null) return `${sanitizeLine(base, 32)} (invalid)`;\n const scale = 10n ** BigInt(decimals);\n const whole = value / scale;\n const frac = (value % scale).toString().padStart(decimals, '0').replace(/0+$/, '');\n return `${whole}${frac ? `.${frac}` : ''} USDC`;\n}\n\nexport function formatRemaining(seconds: number): string {\n const days = Math.floor(seconds / 86400);\n if (days > 0) return `${days} day${days === 1 ? '' : 's'} left`;\n const hours = Math.max(0, Math.floor(seconds / 3600));\n return `${hours}h left`;\n}\n\nexport interface StatusFacts {\n expired: boolean;\n /**\n * Defaults to `unknown`, which reports exactly what every session reported\n * before this could be read: the local file, and nothing more.\n */\n liveness?: PermissionLiveness;\n /**\n * True for a session an older CLI created, whose permission was granted to an\n * address separate from the session key. Auto mode refuses those, so a report\n * that stayed quiet about it would call a setup ready that cannot pay.\n */\n outdated?: boolean;\n ownerAddress: string;\n /** Formatted balance, or null when the read failed. */\n ownerBalance: string | null;\n payerBalance: string | null;\n /** False when the session's chain has no USDC in the registry. */\n hasAsset: boolean;\n spent: bigint;\n sessionCap: bigint | null;\n /**\n * The granted per-period cap and what has gone against it in the current\n * window: top-ups pulled through the permission, not payments, because the\n * cap mirrors the on-chain allowance and payments lag it by whatever float\n * the payer holds. Null when no grant seeded one. Reported separately from\n * the session cap because a grant-seeded policy usually has no session cap\n * at all, and checking only that one stayed quiet while the cap that\n * actually binds was exhausted.\n */\n periodCap?: bigint | null;\n periodSpent?: bigint | null;\n /** How the window reads in a sentence, e.g. \"day\" or \"2 weeks\". */\n periodLabel?: string | null;\n /**\n * The gas reserve refills leave in the payer, in the same formatted units as\n * the balances. A payer holding no more than this is holding what the CLI put\n * there to pay userOp fees, so it is not the misdirected-funds case below.\n */\n payerReserve?: number;\n}\n\n/**\n * What is stopping a payment, most likely cause first. Empty when nothing is.\n *\n * The interesting case is an empty owner next to a funded payer: payments still\n * succeed, so nothing looks wrong, but they are spending the payer's own balance\n * instead of pulling through the permission, which means the cap the user\n * granted is not being applied to anything.\n */\nexport function diagnose(facts: StatusFacts): string[] {\n const problems: string[] = [];\n\n if (facts.expired) {\n problems.push('The session expired. Run `jaw session setup --x402`.');\n }\n\n // Only the chain knows this one. Expiry is the same number the local file\n // carries, so it needs no read; a revoke made from keys.jaw.id or from\n // another machine leaves that file saying the session is fine.\n if (facts.liveness === 'revoked') {\n problems.push(\n 'The permission was revoked on chain, so nothing can be pulled through it any more. ' +\n 'Run `jaw session setup --x402` to grant a new one.'\n );\n }\n\n if (facts.liveness === 'unapproved') {\n problems.push(\n 'The chain has no record of this permission being approved. If the session was just created, ' +\n 'the grant may not have been mined yet; otherwise run `jaw session setup --x402`.'\n );\n }\n\n // `mismatch` is deliberately not here. It says the struct on disk does not\n // hash to the granted id, so the chain cannot be asked about this permission,\n // and nothing about the permission itself is wrong: the caps still apply and\n // payments still go through. Putting it in `problems` flipped `ready` to\n // false, which stops a script or an agent paying against a healthy session\n // over a local serialisation problem. It is reported on the permission line\n // instead.\n\n if (facts.outdated) {\n problems.push(\n 'This session was created by an older CLI and cannot pay: its permission belongs to an address ' +\n 'separate from the session key. Run `jaw session setup --x402` to recreate it.'\n );\n }\n\n if (!facts.hasAsset) {\n problems.push('This chain has no USDC configured, so x402 payments cannot be made on it.');\n }\n\n if (facts.hasAsset && facts.ownerBalance === null) {\n // Blaming the connection while the other balance rendered fine reads as a\n // contradiction, so only do it when both reads failed.\n problems.push(\n facts.payerBalance === null\n ? 'Could not read balances. Check the API key and network.'\n : `Could not read the owner balance for ${facts.ownerAddress}. The address may be malformed.`\n );\n }\n\n if (facts.ownerBalance !== null && Number(facts.ownerBalance) === 0) {\n // Above the reserve, because refills deliberately leave that much in the\n // payer to pay userOp fees with. Reading it back as funds sent to the wrong\n // address would tell the user to move money the CLI put there on purpose.\n const payerHoldsMoreThanItsGas =\n facts.payerBalance !== null && Number(facts.payerBalance) > (facts.payerReserve ?? 0);\n problems.push(\n payerHoldsMoreThanItsGas\n ? 'The owner account is empty but the payer holds USDC. Payments will work, but they bypass the ' +\n 'permission, so the cap you granted is not applying. Move the funds to the owner.'\n : 'The owner account holds no USDC, so there is nothing to pay with.'\n );\n }\n\n // Before the session cap: this is the one that mirrors the permission, so when\n // both are exhausted it is the more useful thing to say, and it frees up on its\n // own rather than needing a config change.\n if (facts.periodCap != null && facts.periodSpent != null && facts.periodSpent >= facts.periodCap) {\n problems.push(\n `The granted allowance for this ${facts.periodLabel ?? 'period'} is used up. It resets at the end of ` +\n 'the window, or grant a new permission with `jaw session setup --x402`.'\n );\n }\n\n if (facts.sessionCap !== null && facts.spent >= facts.sessionCap) {\n problems.push(\n 'The session cap is used up. Raise it with `jaw config set x402.maxTotalPerSession <base units>` ' +\n 'or start a new session.'\n );\n }\n\n return problems;\n}\n","// USDC asset registry, mirrored from the backend's\n// `apps/ens/src/external/payment/asset-registry.ts`. Keep this in sync when the\n// server adds a chain. `wireNetwork` is the CAIP-2 id used on the x402 v2 wire.\n\nexport interface UsdcAsset {\n address: `0x${string}`;\n chainId: number;\n wireNetwork: string;\n /** EIP-712 domain `name` for this deployment's USDC. */\n usdcName: string;\n /** EIP-712 domain `version`. */\n usdcVersion: string;\n /**\n * Token decimals. Every USDC deployment here is 6, but carrying it on the\n * registry entry (rather than a literal `6` at the format site) keeps the\n * source of truth in one place and is ready for a non-6-decimal asset when\n * the registry grows past USDC. Reading it off-chain from the contract is\n * deliberately avoided: the registry is a controlled allowlist, so an extra\n * RPC round-trip and trusting a token's self-reported decimals buy nothing.\n */\n decimals: number;\n}\n\nexport const USDC_BY_NETWORK: Record<string, UsdcAsset> = {\n 'eip155:8453': {\n address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',\n chainId: 8453,\n wireNetwork: 'eip155:8453',\n usdcName: 'USD Coin',\n usdcVersion: '2',\n decimals: 6,\n },\n 'eip155:84532': {\n address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',\n chainId: 84532,\n wireNetwork: 'eip155:84532',\n usdcName: 'USDC',\n usdcVersion: '2',\n decimals: 6,\n },\n 'eip155:137': {\n address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',\n chainId: 137,\n wireNetwork: 'eip155:137',\n usdcName: 'USD Coin',\n usdcVersion: '2',\n decimals: 6,\n },\n 'eip155:80002': {\n address: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582',\n chainId: 80002,\n wireNetwork: 'eip155:80002',\n usdcName: 'USDC',\n usdcVersion: '2',\n decimals: 6,\n },\n};\n\n/**\n * Look up USDC metadata by CAIP-2 network id, or `undefined` if unsupported.\n *\n * Own keys only. The network reaches here from a 402 challenge and from the\n * Bazaar catalogue, both untrusted, and a plain index answers `constructor` or\n * `toString` with something off the prototype: callers then read `.address` off\n * a function and throw where they expected an unsupported network.\n */\nexport function usdcForNetwork(network: string): UsdcAsset | undefined {\n return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : undefined;\n}\n","import * as path from 'node:path';\nimport * as os from 'node:os';\n\nconst JAW_DIR = path.join(os.homedir(), '.jaw');\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, 'config.json'),\n session: path.join(JAW_DIR, 'session.json'),\n relay: path.join(JAW_DIR, 'relay.json'),\n keystore: path.join(JAW_DIR, 'keystore.json'),\n sessionConfig: path.join(JAW_DIR, 'session-config.json'),\n x402Log: path.join(JAW_DIR, 'x402-log.jsonl'),\n paymentLock: path.join(JAW_DIR, 'x402-payment.lock'),\n} as const;\n","import * as fs from 'node:fs';\nimport { PATHS } from '../lib/paths.js';\nimport { ensureDir } from '../lib/config.js';\nimport { errorMessage } from '../lib/errors.js';\n\n/**\n * One line of the append-only x402 payment ledger (`~/.jaw/x402-log.jsonl`).\n * Every payment attempt an agent makes is recorded so spend is auditable and an\n * ambiguous settlement can be reconciled by nonce/txHash after the fact.\n */\nexport interface X402LogEntry {\n /** ISO timestamp of the attempt. */\n at: string;\n url: string;\n /** The paying EOA. */\n payer: string;\n /** paid = settled; failed = signed+sent but settlement failed; refused = never signed. */\n status: 'paid' | 'failed' | 'refused';\n /**\n * What actually left the payer. Under `exact` that is the amount signed for.\n * Under `upto` the server chooses it at settlement, anywhere from zero up to\n * `authorized`, and the receipt is the only place it exists.\n */\n amount?: string;\n /**\n * The ceiling the signature authorized, which is what a live authorization is\n * worth to whoever holds it. Equal to `amount` under `exact`. Absent on\n * entries written before the field existed, where `amount` was both.\n */\n authorized?: string;\n /**\n * When the authorization expires. Recorded but not yet read: reconciling a\n * failed payment means proving its nonce was never consumed and its deadline\n * has passed, and that check cannot be written against entries that never\n * stored the deadline.\n */\n deadline?: string;\n asset?: string;\n network?: string;\n payTo?: string;\n nonce?: string;\n txHash?: string;\n /** Base units refilled into the payer through the permission, when a top-up ran. */\n topUpAmount?: string;\n /** wallet_sendCalls id of that top-up, for on-chain reconciliation. */\n topUpBatchId?: string;\n /**\n * wallet_sendCalls id of the Permit2 approval, when this payment granted one.\n * Never summed with `topUpAmount`: it moves no principal, only the gas the\n * payer was charged for it. Recorded so a userOp the user paid for is not\n * missing from the audit trail.\n */\n approvalBatchId?: string;\n /** Reason for a refused/failed attempt. */\n reason?: string;\n}\n\n/**\n * Append one entry. Never throws — logging must not break a payment.\n *\n * The newline is a PREFIX, not a suffix: a torn write (crash/ENOSPC mid-append)\n * then leaves an incomplete line that the NEXT append starts on a fresh line\n * instead of concatenating onto, so one bad write loses at most its own record,\n * never the following one too.\n *\n * A write failure is surfaced to stderr (not thrown): the caller's payment\n * still succeeds, but the operator needs to know the audit trail — and the\n * restart-time spend-cap seed that reads it — just lost an entry.\n */\nexport function appendX402Log(entry: X402LogEntry): void {\n try {\n ensureDir(PATHS.root);\n fs.appendFileSync(PATHS.x402Log, '\\n' + JSON.stringify(entry), { encoding: 'utf-8', mode: 0o600 });\n } catch (err) {\n const msg = errorMessage(err);\n process.stderr.write(`[jaw] warning: failed to write x402 ledger (${msg}); spend audit/cap may undercount\\n`);\n }\n}\n\n/**\n * Read the ledger, oldest first. `limit` returns only the most recent N entries.\n * Malformed lines are skipped; a missing file is an empty log.\n */\nexport function readX402Log(limit?: number): X402LogEntry[] {\n let raw: string;\n try {\n raw = fs.readFileSync(PATHS.x402Log, 'utf-8');\n } catch {\n return [];\n }\n const entries = raw\n .split('\\n')\n .filter((line) => line.trim().length > 0)\n .map((line) => {\n try {\n return JSON.parse(line) as X402LogEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is X402LogEntry => e !== null);\n return limit && limit > 0 ? entries.slice(-limit) : entries;\n}\n\n/**\n * What one row contributes to a spend cap.\n *\n * The single definition of the rule, exported because more than one place needs\n * it and the three copies that existed before this had already drifted apart by\n * hand. `jaw x402 log` reports against it, and the caps enforce against it, so\n * the number a user reads and the number that refuses their next payment are\n * the same number.\n *\n * A settled payment costs what settled. A failed one costs the ceiling it\n * authorized, because an authorization that was signed and sent stays spendable\n * up to that ceiling until its nonce is consumed or its deadline passes, and\n * nothing yet proves either. Under `exact` the two figures are equal and this\n * is the rule that has always applied.\n *\n * Every parse failure reads as zero and the failed case takes the larger of the\n * two, so one unparseable field cannot shrink an enforced cap: a torn write or a\n * hand edit can only ever leave the cap where it was or higher. Negatives clamp\n * for the same reason, since `BigInt('-5')` parses fine and would otherwise\n * subtract.\n */\nexport function spendFigureOf(entry: X402LogEntry): bigint {\n if (entry.status !== 'paid' && entry.status !== 'failed') return 0n;\n const parse = (value?: string): bigint => {\n if (!value) return 0n;\n try {\n const parsed = BigInt(value);\n return parsed > 0n ? parsed : 0n;\n } catch {\n return 0n;\n }\n };\n if (entry.status === 'paid') return parse(entry.amount);\n const ceiling = parse(entry.authorized);\n const charge = parse(entry.amount);\n return ceiling > charge ? ceiling : charge;\n}\n\n/**\n * Sum a payer's settled and attempted payments since an ISO instant (its whole\n * history when `since` is omitted).\n *\n * Reading it from the ledger rather than an in-memory counter is what makes a\n * cap survive a process restart, which an agent could otherwise relaunch its way\n * past. What each row costs is `spendFigureOf`.\n */\nexport function sumSpentSince(payerAddress: string, since?: string): bigint {\n const payer = payerAddress.toLowerCase();\n return readX402Log().reduce((total, entry) => {\n if (entry.payer?.toLowerCase() !== payer) return total;\n if (since && entry.at < since) return total;\n return total + spendFigureOf(entry);\n }, 0n);\n}\n\n/**\n * Sum what a payer pulled through the permission since an ISO instant (its whole\n * history when `since` is omitted).\n *\n * Distinct from `sumSpentSince` because the two meter different things: the\n * on-chain allowance is drawn down by the top-up, not by the payment it later\n * funds. With a `topUpFloat` the two run apart by whatever is still sitting in\n * the payer, so measuring the granted per-period cap by payments reads a\n * permission as having more left than it does.\n *\n * Every status counts, refusals included: the pull settled on-chain before the\n * payment it was for was ever attempted, so the allowance is gone either way.\n */\nexport function sumToppedUpSince(payerAddress: string, since?: string): bigint {\n const payer = payerAddress.toLowerCase();\n return readX402Log().reduce((total, entry) => {\n if (!entry.topUpAmount) return total;\n if (entry.payer?.toLowerCase() !== payer) return total;\n if (since && entry.at < since) return total;\n try {\n return total + BigInt(entry.topUpAmount);\n } catch {\n return total; // a hand-edited amount must not take the cap down\n }\n }, 0n);\n}\n","import { formatUsdc } from './status-report.js';\nimport { usdcForNetwork } from './asset-registry.js';\nimport { sanitizeLine } from '../lib/terminal.js';\nimport { spendFigureOf, type X402LogEntry } from './ledger.js';\n\n/**\n * Rendering for `jaw x402 log`, kept apart from the command so the accounting in\n * the summary can be tested without a ledger file on disk.\n */\n\n/** Scale by the network the entry was denominated in, not a global assumption. */\nexport function decimalsOf(entry: X402LogEntry): number {\n return (entry.network ? usdcForNetwork(entry.network)?.decimals : undefined) ?? 6;\n}\n\nexport function hostOf(url: string): string {\n try {\n return sanitizeLine(new URL(url).host, 80);\n } catch {\n // Not a URL, so nothing has been parsed away: bound and disarm it.\n return sanitizeLine(url, 80);\n }\n}\n\nexport function renderEntry(entry: X402LogEntry): string {\n // Everything here is read back from a file, so nothing is trusted for being\n // ours originally: a tampered ledger must not be able to paint a row either.\n const when = sanitizeLine(String(entry.at).replace('T', ' ').slice(0, 19), 19);\n // What the caps counted for this row, not what the server charged: on a\n // failed attempt those differ, and the figure a user needs to see is the one\n // that will refuse their next payment.\n const counted = spendFigureOf(entry);\n const amount = entry.amount || entry.authorized ? formatUsdc(counted.toString(), decimalsOf(entry)) : '';\n const head = ` ${when} ${sanitizeLine(entry.status, 7).padEnd(7)} ${amount.padStart(12)} ${hostOf(entry.url)}`;\n\n const detail: string[] = [];\n // A top-up moved user funds through the permission. Always visible, even on an\n // attempt that then failed, since that money left the account regardless.\n if (entry.topUpAmount) {\n detail.push(`topped up ${formatUsdc(entry.topUpAmount, decimalsOf(entry))}`);\n }\n // The Permit2 approval moved no principal, only the gas the payer was charged\n // for it, so it is named rather than totalled.\n if (entry.approvalBatchId) detail.push('granted Permit2 its allowance');\n if (entry.txHash) detail.push(sanitizeLine(entry.txHash, 80));\n // A failed settlement may still have been broadcast: the nonce is what makes\n // it reconcilable on chain, so surface it exactly where it is ambiguous.\n if (entry.status === 'failed' && entry.nonce) detail.push(`nonce ${sanitizeLine(entry.nonce, 80)}`);\n // Stored server text: an endpoint that got refused once would\n // otherwise repaint this line on every later `x402 log`.\n if (entry.reason) detail.push(sanitizeLine(entry.reason, 200));\n\n return detail.length > 0 ? `${head}\\n${' '.repeat(24)}${detail.join(' ')}` : head;\n}\n\nexport function renderSummary(entries: X402LogEntry[]): string {\n const counts = { paid: 0, failed: 0, refused: 0 };\n // Only settled and attempted payments count as money out; a refusal never\n // signed anything. Same rule the spend caps use.\n //\n // Totalled per decimals scale rather than as one number. Base units from\n // tokens with different decimals are not the same unit, so adding them and\n // formatting the result with whichever entry happened to come last would\n // print a confident, wrong figure. Every USDC in the registry is 6 decimals\n // today, so this is a single group in practice and the guard costs nothing.\n const spentByScale = new Map<number, bigint>();\n let unknown = 0;\n for (const entry of entries) {\n // An unrecognised status used to land on `counts` as a stray key and vanish\n // from the tally, so a malformed row silently shrank the reported total.\n // Own keys only: `in` walks the prototype, so a row saying `constructor`\n // took the counted branch, landed on a key nothing reads, and disappeared\n // from both tallies.\n if (Object.hasOwn(counts, entry.status)) counts[entry.status] += 1;\n else unknown += 1;\n const counted = spendFigureOf(entry);\n if (counted > 0n) {\n try {\n const decimals = decimalsOf(entry);\n spentByScale.set(decimals, (spentByScale.get(decimals) ?? 0n) + counted);\n } catch {\n /* a hand-edited asset must not break the summary */\n }\n }\n }\n\n const parts = [`${counts.paid} paid`];\n if (counts.failed > 0) parts.push(`${counts.failed} failed`);\n if (counts.refused > 0) parts.push(`${counts.refused} refused`);\n if (unknown > 0) parts.push(`${unknown} unreadable`);\n\n const totals =\n spentByScale.size === 0\n ? formatUsdc('0', 6)\n : [...spentByScale.entries()].map(([decimals, spent]) => formatUsdc(spent.toString(), decimals)).join(' + ');\n\n return ` ${parts.join(', ')}, ${totals} out`;\n}\n"]}
@@ -0,0 +1,90 @@
1
+ // src/x402/amount.ts
2
+ function parseBigInt(value) {
3
+ if (value === void 0 || value === null || value === "") return null;
4
+ try {
5
+ return BigInt(value);
6
+ } catch {
7
+ return null;
8
+ }
9
+ }
10
+
11
+ // src/lib/terminal.ts
12
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
13
+ var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
14
+ var REPLACEMENT = "\uFFFD";
15
+ var DEFAULT_LINE_LENGTH = 200;
16
+ function bound(text, maxLength) {
17
+ if (text.length <= maxLength) return text;
18
+ return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
19
+ }
20
+ function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
21
+ const text = typeof value === "string" ? value : String(value);
22
+ return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
23
+ }
24
+
25
+ // src/x402/status-report.ts
26
+ function formatUsdc(base, decimals) {
27
+ if (base === void 0) return "unlimited";
28
+ const value = parseBigInt(base);
29
+ if (value === null) return `${sanitizeLine(base, 32)} (invalid)`;
30
+ const scale = 10n ** BigInt(decimals);
31
+ const whole = value / scale;
32
+ const frac = (value % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
33
+ return `${whole}${frac ? `.${frac}` : ""} USDC`;
34
+ }
35
+ function formatRemaining(seconds) {
36
+ const days = Math.floor(seconds / 86400);
37
+ if (days > 0) return `${days} day${days === 1 ? "" : "s"} left`;
38
+ const hours = Math.max(0, Math.floor(seconds / 3600));
39
+ return `${hours}h left`;
40
+ }
41
+ function diagnose(facts) {
42
+ const problems = [];
43
+ if (facts.expired) {
44
+ problems.push("The session expired. Run `jaw session setup --x402`.");
45
+ }
46
+ if (facts.liveness === "revoked") {
47
+ problems.push(
48
+ "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."
49
+ );
50
+ }
51
+ if (facts.liveness === "unapproved") {
52
+ problems.push(
53
+ "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`."
54
+ );
55
+ }
56
+ if (facts.outdated) {
57
+ problems.push(
58
+ "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."
59
+ );
60
+ }
61
+ if (!facts.hasAsset) {
62
+ problems.push("This chain has no USDC configured, so x402 payments cannot be made on it.");
63
+ }
64
+ if (facts.hasAsset && facts.ownerBalance === null) {
65
+ problems.push(
66
+ 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.`
67
+ );
68
+ }
69
+ if (facts.ownerBalance !== null && Number(facts.ownerBalance) === 0) {
70
+ const payerHoldsMoreThanItsGas = facts.payerBalance !== null && Number(facts.payerBalance) > (facts.payerReserve ?? 0);
71
+ problems.push(
72
+ 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."
73
+ );
74
+ }
75
+ if (facts.periodCap != null && facts.periodSpent != null && facts.periodSpent >= facts.periodCap) {
76
+ problems.push(
77
+ `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\`.`
78
+ );
79
+ }
80
+ if (facts.sessionCap !== null && facts.spent >= facts.sessionCap) {
81
+ problems.push(
82
+ "The session cap is used up. Raise it with `jaw config set x402.maxTotalPerSession <base units>` or start a new session."
83
+ );
84
+ }
85
+ return problems;
86
+ }
87
+
88
+ export { diagnose, formatRemaining, formatUsdc };
89
+ //# sourceMappingURL=status-report.js.map
90
+ //# sourceMappingURL=status-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/x402/amount.ts","../../src/lib/terminal.ts","../../src/x402/status-report.ts"],"names":[],"mappings":"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","file":"status-report.js","sourcesContent":["/**\n * Canonical base-unit amount parser. Every place that turns an amount string\n * (from an untrusted 402 challenge or from hand-edited config) into a bigint\n * goes through here, so the failure surface is uniform and a malformed value\n * can never crash a hot path. Sign policy is left to the caller: this returns\n * the parsed value (which may be negative) or null when it is not an integer.\n */\nexport function parseBigInt(value: string | undefined | null): bigint | null {\n // Empty string is \"unset\", not zero: BigInt('') is 0n, but an absent config\n // value must read as null so callers treat it as \"no bound\", not \"cap of 0\".\n if (value === undefined || value === null || value === '') return null;\n try {\n return BigInt(value);\n } catch {\n return null;\n }\n}\n\n/** Parse a non-negative base-unit amount; undefined when absent, invalid, or negative. */\nexport function parseNonNegativeBigInt(value: string | undefined | null): bigint | undefined {\n const parsed = parseBigInt(value);\n return parsed !== null && parsed >= 0n ? parsed : undefined;\n}\n","/**\n * Make server-controlled text safe to print.\n *\n * A paid endpoint controls its response body, and parts of its 402 challenge\n * reach our refusal messages, which are stored in the payment ledger and\n * reprinted by `jaw x402 log`. Printed raw, an escape sequence lets that server\n * erase the line the CLI just wrote and paint its own in place: a resource that\n * was never paid for can render a convincing green \"Paid. 5 USDC\" the CLI never\n * emitted, and once in the ledger it does so on every later read.\n *\n * Two shapes, because the danger differs. A newline inside a one-line field\n * forges an extra row that reads as a genuine record, while inside a response\n * body it is just a newline. Only the human renderers need any of this: JSON\n * output escapes control characters on its own.\n */\n\n/**\n * Characters that are invisible or reorder what follows them, in any context.\n *\n * Bidi overrides (U+202A-U+202E, U+2066-U+2069) are the Trojan Source trick:\n * they flip rendering direction, so an address can display as something other\n * than the bytes that were signed. The zero-width family hides text outright,\n * splitting an address to the eye while leaving it intact to a copy. Neither is\n * a control character in the C0 or C1 sense, so stripping those alone misses\n * both. U+2028 and U+2029 break lines the way a newline does.\n */\nconst INVISIBLE_AND_BIDI = /[\\u200B-\\u200F\\u2028\\u2029\\u202A-\\u202E\\u2066-\\u2069\\uFEFF]/g;\n\n/** C0 except tab and newline, DEL, and the C1 block. For multi-line text. */\n// eslint-disable-next-line no-control-regex\nconst BLOCK_CONTROLS = /[\\u0000-\\u0008\\u000B-\\u001F\\u007F-\\u009F]/g;\n\n/** Every C0 control, DEL and C1. For text that must stay on one line. */\n// eslint-disable-next-line no-control-regex\nconst LINE_CONTROLS = /[\\u0000-\\u001F\\u007F-\\u009F]/g;\n\n/** Left in place of a stripped character, so tampering shows rather than vanishing. */\nconst REPLACEMENT = '\\uFFFD';\n\nexport const DEFAULT_LINE_LENGTH = 200;\n\nfunction bound(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n // Say it was cut. A silently truncated error message reads as a complete one.\n return `${text.slice(0, maxLength)}\\u2026 (${text.length - maxLength} more characters)`;\n}\n\n/**\n * Disarm a value that has to render as a single line: a refusal reason, a\n * network id, a host, a transaction hash.\n *\n * Newlines go with everything else here. A reason carrying one would otherwise\n * open a second line under a log entry, indistinguishable from the real row\n * above it, which is the same forgery the escape sequences allow.\n */\nexport function sanitizeLine(value: unknown, maxLength: number = DEFAULT_LINE_LENGTH): string {\n const text = typeof value === 'string' ? value : String(value);\n return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);\n}\n\n/**\n * Disarm a response body, keeping its shape.\n *\n * Newlines and tabs survive, since a body is legitimately multi-line and\n * neither can move the cursor back over text already written. Nothing is\n * truncated: the caller asked for this resource and may well have paid for it,\n * and the read is already bounded upstream by MAX_BODY_BYTES.\n */\nexport function sanitizeBlock(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value);\n return text.replace(BLOCK_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT);\n}\n","import { parseBigInt } from './amount.js';\nimport { sanitizeLine } from '../lib/terminal.js';\nimport type { PermissionLiveness } from './permission-onchain.js';\n\n/**\n * Presentation and diagnosis for `jaw x402 status`, kept apart from the command\n * so the rules can be tested without a session, a keystore or a network.\n */\n\n/** Base units to a readable amount, trailing zeros trimmed. */\nexport function formatUsdc(base: string | undefined, decimals: number): string {\n if (base === undefined) return 'unlimited';\n const value = parseBigInt(base);\n // Echoing the raw value put an unvalidated string on screen: amounts reach\n // here from the ledger and from config, both files that can be edited.\n if (value === null) return `${sanitizeLine(base, 32)} (invalid)`;\n const scale = 10n ** BigInt(decimals);\n const whole = value / scale;\n const frac = (value % scale).toString().padStart(decimals, '0').replace(/0+$/, '');\n return `${whole}${frac ? `.${frac}` : ''} USDC`;\n}\n\nexport function formatRemaining(seconds: number): string {\n const days = Math.floor(seconds / 86400);\n if (days > 0) return `${days} day${days === 1 ? '' : 's'} left`;\n const hours = Math.max(0, Math.floor(seconds / 3600));\n return `${hours}h left`;\n}\n\nexport interface StatusFacts {\n expired: boolean;\n /**\n * Defaults to `unknown`, which reports exactly what every session reported\n * before this could be read: the local file, and nothing more.\n */\n liveness?: PermissionLiveness;\n /**\n * True for a session an older CLI created, whose permission was granted to an\n * address separate from the session key. Auto mode refuses those, so a report\n * that stayed quiet about it would call a setup ready that cannot pay.\n */\n outdated?: boolean;\n ownerAddress: string;\n /** Formatted balance, or null when the read failed. */\n ownerBalance: string | null;\n payerBalance: string | null;\n /** False when the session's chain has no USDC in the registry. */\n hasAsset: boolean;\n spent: bigint;\n sessionCap: bigint | null;\n /**\n * The granted per-period cap and what has gone against it in the current\n * window: top-ups pulled through the permission, not payments, because the\n * cap mirrors the on-chain allowance and payments lag it by whatever float\n * the payer holds. Null when no grant seeded one. Reported separately from\n * the session cap because a grant-seeded policy usually has no session cap\n * at all, and checking only that one stayed quiet while the cap that\n * actually binds was exhausted.\n */\n periodCap?: bigint | null;\n periodSpent?: bigint | null;\n /** How the window reads in a sentence, e.g. \"day\" or \"2 weeks\". */\n periodLabel?: string | null;\n /**\n * The gas reserve refills leave in the payer, in the same formatted units as\n * the balances. A payer holding no more than this is holding what the CLI put\n * there to pay userOp fees, so it is not the misdirected-funds case below.\n */\n payerReserve?: number;\n}\n\n/**\n * What is stopping a payment, most likely cause first. Empty when nothing is.\n *\n * The interesting case is an empty owner next to a funded payer: payments still\n * succeed, so nothing looks wrong, but they are spending the payer's own balance\n * instead of pulling through the permission, which means the cap the user\n * granted is not being applied to anything.\n */\nexport function diagnose(facts: StatusFacts): string[] {\n const problems: string[] = [];\n\n if (facts.expired) {\n problems.push('The session expired. Run `jaw session setup --x402`.');\n }\n\n // Only the chain knows this one. Expiry is the same number the local file\n // carries, so it needs no read; a revoke made from keys.jaw.id or from\n // another machine leaves that file saying the session is fine.\n if (facts.liveness === 'revoked') {\n problems.push(\n 'The permission was revoked on chain, so nothing can be pulled through it any more. ' +\n 'Run `jaw session setup --x402` to grant a new one.'\n );\n }\n\n if (facts.liveness === 'unapproved') {\n problems.push(\n 'The chain has no record of this permission being approved. If the session was just created, ' +\n 'the grant may not have been mined yet; otherwise run `jaw session setup --x402`.'\n );\n }\n\n // `mismatch` is deliberately not here. It says the struct on disk does not\n // hash to the granted id, so the chain cannot be asked about this permission,\n // and nothing about the permission itself is wrong: the caps still apply and\n // payments still go through. Putting it in `problems` flipped `ready` to\n // false, which stops a script or an agent paying against a healthy session\n // over a local serialisation problem. It is reported on the permission line\n // instead.\n\n if (facts.outdated) {\n problems.push(\n 'This session was created by an older CLI and cannot pay: its permission belongs to an address ' +\n 'separate from the session key. Run `jaw session setup --x402` to recreate it.'\n );\n }\n\n if (!facts.hasAsset) {\n problems.push('This chain has no USDC configured, so x402 payments cannot be made on it.');\n }\n\n if (facts.hasAsset && facts.ownerBalance === null) {\n // Blaming the connection while the other balance rendered fine reads as a\n // contradiction, so only do it when both reads failed.\n problems.push(\n facts.payerBalance === null\n ? 'Could not read balances. Check the API key and network.'\n : `Could not read the owner balance for ${facts.ownerAddress}. The address may be malformed.`\n );\n }\n\n if (facts.ownerBalance !== null && Number(facts.ownerBalance) === 0) {\n // Above the reserve, because refills deliberately leave that much in the\n // payer to pay userOp fees with. Reading it back as funds sent to the wrong\n // address would tell the user to move money the CLI put there on purpose.\n const payerHoldsMoreThanItsGas =\n facts.payerBalance !== null && Number(facts.payerBalance) > (facts.payerReserve ?? 0);\n problems.push(\n payerHoldsMoreThanItsGas\n ? 'The owner account is empty but the payer holds USDC. Payments will work, but they bypass the ' +\n 'permission, so the cap you granted is not applying. Move the funds to the owner.'\n : 'The owner account holds no USDC, so there is nothing to pay with.'\n );\n }\n\n // Before the session cap: this is the one that mirrors the permission, so when\n // both are exhausted it is the more useful thing to say, and it frees up on its\n // own rather than needing a config change.\n if (facts.periodCap != null && facts.periodSpent != null && facts.periodSpent >= facts.periodCap) {\n problems.push(\n `The granted allowance for this ${facts.periodLabel ?? 'period'} is used up. It resets at the end of ` +\n 'the window, or grant a new permission with `jaw session setup --x402`.'\n );\n }\n\n if (facts.sessionCap !== null && facts.spent >= facts.sessionCap) {\n problems.push(\n 'The session cap is used up. Raise it with `jaw config set x402.maxTotalPerSession <base units>` ' +\n 'or start a new session.'\n );\n }\n\n return problems;\n}\n"]}