@jaw.id/cli 0.1.26 → 0.2.1
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.
- package/dist/base-command.js +3 -1
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +140 -12
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +3 -1
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/config/write.js +6 -4
- package/dist/commands/config/write.js.map +1 -1
- package/dist/commands/disconnect.js +24 -10
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +2426 -94
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +197 -45
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/session/add.js +1547 -0
- package/dist/commands/session/add.js.map +1 -0
- package/dist/commands/session/revoke.js +181 -54
- package/dist/commands/session/revoke.js.map +1 -1
- package/dist/commands/session/setup.js +516 -65
- package/dist/commands/session/setup.js.map +1 -1
- package/dist/commands/session/status.js +315 -6
- package/dist/commands/session/status.js.map +1 -1
- package/dist/commands/version.js +3 -1
- package/dist/commands/version.js.map +1 -1
- package/dist/commands/x402/log.js +344 -0
- package/dist/commands/x402/log.js.map +1 -0
- package/dist/commands/x402/pay.js +2122 -0
- package/dist/commands/x402/pay.js.map +1 -0
- package/dist/commands/x402/status.js +1047 -0
- package/dist/commands/x402/status.js.map +1 -0
- package/dist/index.js +41 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +41 -14
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +26 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/keystore.js +13 -2
- package/dist/lib/keystore.js.map +1 -1
- package/dist/lib/paths.js +3 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/payment-lock.js +121 -0
- package/dist/lib/payment-lock.js.map +1 -0
- package/dist/lib/session-bridge.js +148 -24
- package/dist/lib/session-bridge.js.map +1 -1
- package/dist/lib/session-config.js +78 -11
- package/dist/lib/session-config.js.map +1 -1
- package/dist/lib/terminal.js +22 -0
- package/dist/lib/terminal.js.map +1 -0
- package/dist/lib/validation.js +3 -3
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +22 -10
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +73 -6
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +43 -12
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/resources.js +119 -0
- package/dist/mcp/handlers/resources.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +269 -60
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/helpers.js +50 -3
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/server.js +2426 -94
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +43 -3
- package/dist/mcp/tools.js.map +1 -1
- package/dist/x402/log-view.js +160 -0
- package/dist/x402/log-view.js.map +1 -0
- package/dist/x402/status-report.js +90 -0
- package/dist/x402/status-report.js.map +1 -0
- package/oclif.manifest.json +405 -11
- package/package.json +5 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as fs2 from 'fs';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
// src/lib/payment-lock.ts
|
|
7
|
+
var JAW_DIR = path.join(os.homedir(), ".jaw");
|
|
8
|
+
var PATHS = {
|
|
9
|
+
root: JAW_DIR,
|
|
10
|
+
config: path.join(JAW_DIR, "config.json"),
|
|
11
|
+
session: path.join(JAW_DIR, "session.json"),
|
|
12
|
+
relay: path.join(JAW_DIR, "relay.json"),
|
|
13
|
+
keystore: path.join(JAW_DIR, "keystore.json"),
|
|
14
|
+
sessionConfig: path.join(JAW_DIR, "session-config.json"),
|
|
15
|
+
x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
|
|
16
|
+
paymentLock: path.join(JAW_DIR, "x402-payment.lock")
|
|
17
|
+
};
|
|
18
|
+
function ensureDir(dir) {
|
|
19
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
20
|
+
fs2.chmodSync(dir, 448);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/lib/payment-lock.ts
|
|
24
|
+
var STALE_AFTER_MS = 3e5;
|
|
25
|
+
var DEFAULT_ACQUIRE_TIMEOUT_MS = 12e4;
|
|
26
|
+
var POLL_INTERVAL_MS = 100;
|
|
27
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
function readLock() {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(fs2.readFileSync(PATHS.paymentLock, "utf-8"));
|
|
31
|
+
if (typeof parsed?.pid !== "number" || typeof parsed?.at !== "number") return null;
|
|
32
|
+
return parsed;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isAlive(pid) {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return err?.code === "EPERM";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var TORN_GRACE_MS = 2e3;
|
|
46
|
+
function unreadableLockIsTorn() {
|
|
47
|
+
try {
|
|
48
|
+
return Date.now() - fs2.statSync(PATHS.paymentLock).mtimeMs > TORN_GRACE_MS;
|
|
49
|
+
} catch {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isStale(lock, staleAfterMs) {
|
|
54
|
+
if (!lock) return unreadableLockIsTorn();
|
|
55
|
+
if (!isAlive(lock.pid)) return true;
|
|
56
|
+
return Date.now() - lock.at > staleAfterMs;
|
|
57
|
+
}
|
|
58
|
+
function breakLock(observed) {
|
|
59
|
+
const current = readLock();
|
|
60
|
+
const sameLock = observed === null && current === null || observed !== null && current !== null && current.token === observed.token && current.at === observed.at;
|
|
61
|
+
if (!sameLock && current !== null) return;
|
|
62
|
+
if (current === null && !unreadableLockIsTorn()) return;
|
|
63
|
+
try {
|
|
64
|
+
fs2.unlinkSync(PATHS.paymentLock);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function withPaymentLock(fn, options = {}) {
|
|
69
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
|
|
70
|
+
const staleAfterMs = options.staleAfterMs ?? STALE_AFTER_MS;
|
|
71
|
+
const token = crypto.randomBytes(16).toString("hex");
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
ensureDir(PATHS.root);
|
|
74
|
+
let notified = false;
|
|
75
|
+
for (; ; ) {
|
|
76
|
+
try {
|
|
77
|
+
const fd = fs2.openSync(PATHS.paymentLock, "wx", 384);
|
|
78
|
+
try {
|
|
79
|
+
fs2.writeFileSync(fd, JSON.stringify({ pid: process.pid, token, at: Date.now() }));
|
|
80
|
+
} finally {
|
|
81
|
+
fs2.closeSync(fd);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err?.code !== "EEXIST") throw err;
|
|
86
|
+
const holder = readLock();
|
|
87
|
+
if (isStale(holder, staleAfterMs)) {
|
|
88
|
+
breakLock(holder);
|
|
89
|
+
} else if (!notified && holder) {
|
|
90
|
+
notified = true;
|
|
91
|
+
options.onWait?.(holder.pid);
|
|
92
|
+
}
|
|
93
|
+
if (Date.now() >= deadline) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Another payment has been running for ${Math.round((Date.now() - (holder?.at ?? Date.now())) / 1e3)}s (pid ${holder?.pid ?? "unknown"}). Refusing rather than paying past the session cap. Retry once it finishes, or remove ${PATHS.paymentLock} if that process is gone.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
await sleep(POLL_INTERVAL_MS);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const releaseOnExit = () => release(token);
|
|
102
|
+
process.once("exit", releaseOnExit);
|
|
103
|
+
try {
|
|
104
|
+
return await fn();
|
|
105
|
+
} finally {
|
|
106
|
+
process.removeListener("exit", releaseOnExit);
|
|
107
|
+
release(token);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function release(token) {
|
|
111
|
+
const current = readLock();
|
|
112
|
+
if (current?.token !== token) return;
|
|
113
|
+
try {
|
|
114
|
+
fs2.unlinkSync(PATHS.paymentLock);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { DEFAULT_ACQUIRE_TIMEOUT_MS, STALE_AFTER_MS, withPaymentLock };
|
|
120
|
+
//# sourceMappingURL=payment-lock.js.map
|
|
121
|
+
//# sourceMappingURL=payment-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/config.ts","../../src/lib/payment-lock.ts"],"names":["fs"],"mappings":"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","file":"payment-lock.js","sourcesContent":["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 './paths.js';\nimport type { JawConfig, SettableConfigKey } from './types.js';\nimport type { X402Policy, X402PolicyKey } from '../x402/policy.js';\nimport { isValidKeysUrl, isValidRelayUrl } from './validation.js';\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nfunction migrateConfig(config: JawConfig): JawConfig {\n if (config.paymasterUrl && !config.paymasters) {\n const chainId = config.defaultChain ?? 1;\n config.paymasters = { [chainId]: { url: config.paymasterUrl } };\n delete config.paymasterUrl;\n saveConfig(config);\n }\n return config;\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, 'utf-8');\n try {\n const config = JSON.parse(raw) as JawConfig;\n return migrateConfig(config);\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n}\n\n/** Paymaster URLs embed provider API keys as query params (e.g. Pimlico's ?apikey=...). */\nfunction redactUrlSecrets(url: string): string {\n try {\n const parsed = new URL(url);\n for (const key of [...parsed.searchParams.keys()]) {\n parsed.searchParams.set(key, '***');\n }\n return parsed.toString();\n } catch {\n return '***';\n }\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n ...(config.paymasters && {\n paymasters: Object.fromEntries(\n Object.entries(config.paymasters).map(([chainId, pm]) => [\n chainId,\n // context is a free-form object (usually just a sponsorshipPolicyId) but a\n // provider could stash a token there, so mask it rather than hand it to the agent.\n { ...pm, url: redactUrlSecrets(pm.url), context: pm.context ? '***' : undefined },\n ])\n ),\n }),\n };\n}\n\n/**\n * Set one field of the `x402` payment policy. Array fields (allow-lists) are\n * comma-split. Intentionally NOT reachable from the MCP tool surface: an agent\n * must not be able to raise its own spending caps — only a human at the CLI.\n */\nexport function setX402PolicyValue(key: X402PolicyKey, value: string): void {\n const config = loadConfig();\n const x402: X402Policy = { ...(config.x402 ?? {}) };\n if (key === 'maxAmountPerPayment' || key === 'maxTotalPerSession' || key === 'topUpFloat') {\n // These are base-unit amounts read via BigInt() on the hot payment path;\n // reject anything that isn't a non-negative integer here so a bad value\n // can never turn into a per-payment failure later.\n if (!/^\\d+$/.test(value.trim())) {\n throw new Error(`${key} must be a non-negative integer (base units), got: ${value}`);\n }\n x402[key] = value.trim();\n } else {\n x402[key] = value\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean);\n }\n saveConfig({ ...config, x402 });\n}\n\nexport function setConfigValue(key: SettableConfigKey, value: string | number): void {\n if (key === 'keysUrl' && typeof value === 'string' && !isValidKeysUrl(value)) {\n throw new Error(`Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`);\n }\n if (key === 'relayUrl' && typeof value === 'string' && !isValidRelayUrl(value)) {\n throw new Error(`Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`);\n }\n // Numeric keys: coerce + validate here so EVERY caller is safe. The MCP tool\n // passes raw strings (its schema types value as a string), so without this a\n // `sessionExpiry: \"abc\"` would land as a string and turn `expiry * 86400`\n // into NaN, and a `defaultChain` string would ride into an RPC URL verbatim.\n let toStore: string | number = value;\n if (key === 'defaultChain' || key === 'sessionExpiry') {\n // Strict: reject anything parseInt would silently truncate ('1.5' -> 1,\n // '0x10' -> 0, '10abc' -> 10). Only a bare positive decimal integer passes.\n const n = typeof value === 'number' ? value : /^\\d+$/.test(value.trim()) ? parseInt(value.trim(), 10) : NaN;\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(`${key} must be a positive integer, got: ${JSON.stringify(value)}`);\n }\n toStore = n;\n }\n const config = loadConfig();\n const updated = { ...config, [key]: toStore };\n saveConfig(updated);\n}\n","import * as fs from 'node:fs';\nimport * as crypto from 'node:crypto';\nimport { PATHS } from './paths.js';\nimport { ensureDir } from './config.js';\n\n/**\n * Serialize payments across processes.\n *\n * A spend cap is enforced by reading the ledger, checking the total, paying, and\n * appending the result. Seconds of network I/O sit between the read and the\n * write, so two payers that overlap in that window both see the same total, both\n * pass the cap, and both pay. The MCP server serializes its own tool calls in\n * memory, which does nothing about a second process: `jaw x402 pay --pay` next to\n * a running agent, or two agents at once.\n *\n * With a pre-funded payer the local cap is the only cap, so that window is the\n * difference between spending what was configured and spending the balance.\n *\n * The lock is a file created with `wx`, which is atomic: whoever creates it wins.\n * Everything else here is about not leaving it behind.\n */\n\ninterface LockFile {\n pid: number;\n /** Distinguishes our lock from one that replaced it after we judged it stale. */\n token: string;\n /** Epoch ms, for the age check. */\n at: number;\n}\n\n/**\n * A payment can legitimately take a while: up to 90s waiting on a top-up to\n * confirm, plus two 30s HTTP timeouts. The threshold sits well past that, so a\n * slow payment is never mistaken for a crashed one.\n */\nexport const STALE_AFTER_MS = 300_000;\n\n/** How long to wait for the holder before refusing. Refusing is safe; overspending is not. */\nexport const DEFAULT_ACQUIRE_TIMEOUT_MS = 120_000;\n\nconst POLL_INTERVAL_MS = 100;\n\nexport interface LockOptions {\n timeoutMs?: number;\n staleAfterMs?: number;\n /** Called once when the wait becomes noticeable, so a blocked CLI explains itself. */\n onWait?: (holderPid: number) => void;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction readLock(): LockFile | null {\n try {\n const parsed = JSON.parse(fs.readFileSync(PATHS.paymentLock, 'utf-8')) as LockFile;\n if (typeof parsed?.pid !== 'number' || typeof parsed?.at !== 'number') return null;\n return parsed;\n } catch {\n return null; // missing, truncated, or half-written: treat as breakable\n }\n}\n\n/** Signal 0 tests for existence without delivering anything. */\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM means it exists under another user, which still counts as alive.\n return (err as NodeJS.ErrnoException)?.code === 'EPERM';\n }\n}\n\n/**\n * How long an unreadable lock file has to stay unreadable before it counts as\n * torn rather than newborn.\n */\nconst TORN_GRACE_MS = 2_000;\n\n/**\n * A file that does not parse is either torn by a crash mid-write, or newborn:\n * `withPaymentLock` creates it with `wx` and writes a tick later, so there is a\n * real window where the winner's own lock reads as `null`. Breaking it there\n * hands the same critical section to a second payer, which is the one thing\n * this module exists to prevent. A torn file stops advancing its mtime, a\n * newborn one is about to, so age separates them.\n */\nfunction unreadableLockIsTorn(): boolean {\n try {\n return Date.now() - fs.statSync(PATHS.paymentLock).mtimeMs > TORN_GRACE_MS;\n } catch {\n return true; // already gone: nothing left to protect\n }\n}\n\nfunction isStale(lock: LockFile | null, staleAfterMs: number): boolean {\n if (!lock) return unreadableLockIsTorn(); // torn by a crash, or still being written\n if (!isAlive(lock.pid)) return true; // holder died without releasing\n return Date.now() - lock.at > staleAfterMs; // alive but wedged past any real payment\n}\n\n/**\n * Remove a lock only if it still looks like the one judged stale.\n *\n * Between deciding and deleting, the holder may have released and someone else\n * acquired. Comparing first keeps this from deleting a live lock and letting two\n * payers through, which is the exact failure the lock exists to prevent.\n */\nfunction breakLock(observed: LockFile | null): void {\n const current = readLock();\n const sameLock =\n (observed === null && current === null) ||\n (observed !== null && current !== null && current.token === observed.token && current.at === observed.at);\n if (!sameLock && current !== null) return;\n // Same grace as `isStale`, for the door it does not cover: `current === null`\n // also happens when the holder released and a third payer is mid-`wx`, its\n // file created and not yet written. Unlinking there deletes a lock that payer\n // believes it holds, and both of us end up inside the critical section.\n if (current === null && !unreadableLockIsTorn()) return;\n try {\n fs.unlinkSync(PATHS.paymentLock);\n } catch {\n /* already gone: someone else broke it first, which is the same outcome */\n }\n}\n\n/**\n * Hold the payment lock for the duration of `fn`.\n *\n * Held across the network call on purpose. The cap is only safe if the read, the\n * payment and the write happen as one unit, so payments are serialized machine\n * wide. They are inherently sequential for that reason, which the in-memory\n * queue in the MCP handler already assumed.\n */\nexport async function withPaymentLock<T>(fn: () => Promise<T>, options: LockOptions = {}): Promise<T> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;\n const staleAfterMs = options.staleAfterMs ?? STALE_AFTER_MS;\n const token = crypto.randomBytes(16).toString('hex');\n const deadline = Date.now() + timeoutMs;\n\n ensureDir(PATHS.root);\n\n let notified = false;\n for (;;) {\n try {\n const fd = fs.openSync(PATHS.paymentLock, 'wx', 0o600);\n try {\n fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, token, at: Date.now() } satisfies LockFile));\n } finally {\n fs.closeSync(fd);\n }\n break;\n } catch (err) {\n if ((err as NodeJS.ErrnoException)?.code !== 'EEXIST') throw err;\n\n const holder = readLock();\n if (isStale(holder, staleAfterMs)) {\n // Break it, then fall through to the deadline check and the sleep below\n // rather than retrying straight away. If the unlink cannot succeed (an\n // immutable file, or a directory at that path) its error is swallowed\n // and `wx` keeps returning EEXIST, so looping without yielding spins at\n // 100% CPU forever and wedges the whole process, not just the payment.\n breakLock(holder);\n } else if (!notified && holder) {\n notified = true;\n options.onWait?.(holder.pid);\n }\n if (Date.now() >= deadline) {\n throw new Error(\n `Another payment has been running for ${Math.round((Date.now() - (holder?.at ?? Date.now())) / 1000)}s ` +\n `(pid ${holder?.pid ?? 'unknown'}). Refusing rather than paying past the session cap. ` +\n `Retry once it finishes, or remove ${PATHS.paymentLock} if that process is gone.`\n );\n }\n await sleep(POLL_INTERVAL_MS);\n }\n }\n\n // Registered for the duration: a kill between here and the finally would\n // otherwise leave a lock that every later payment has to wait out.\n const releaseOnExit = () => release(token);\n process.once('exit', releaseOnExit);\n\n try {\n return await fn();\n } finally {\n process.removeListener('exit', releaseOnExit);\n release(token);\n }\n}\n\n/** Release only our own lock: if ours was broken as stale, the file is someone else's now. */\nfunction release(token: string): void {\n const current = readLock();\n if (current?.token !== token) return;\n try {\n fs.unlinkSync(PATHS.paymentLock);\n } catch {\n /* already gone */\n }\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import 'crypto';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
+
import { encodeFunctionData, maxUint256, erc20Abi } from 'viem';
|
|
5
6
|
|
|
6
7
|
// src/lib/keystore.ts
|
|
7
8
|
var JAW_DIR = path.join(os.homedir(), ".jaw");
|
|
@@ -11,7 +12,9 @@ var PATHS = {
|
|
|
11
12
|
session: path.join(JAW_DIR, "session.json"),
|
|
12
13
|
relay: path.join(JAW_DIR, "relay.json"),
|
|
13
14
|
keystore: path.join(JAW_DIR, "keystore.json"),
|
|
14
|
-
sessionConfig: path.join(JAW_DIR, "session-config.json")
|
|
15
|
+
sessionConfig: path.join(JAW_DIR, "session-config.json"),
|
|
16
|
+
x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
|
|
17
|
+
paymentLock: path.join(JAW_DIR, "x402-payment.lock")
|
|
15
18
|
};
|
|
16
19
|
function ensureDir(dir) {
|
|
17
20
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -62,6 +65,9 @@ function loadSessionKey() {
|
|
|
62
65
|
}
|
|
63
66
|
return parsed.privateKey;
|
|
64
67
|
}
|
|
68
|
+
function isLegacySession(config) {
|
|
69
|
+
return config.mode !== "eip7702";
|
|
70
|
+
}
|
|
65
71
|
function loadSessionConfig() {
|
|
66
72
|
if (!fs.existsSync(PATHS.sessionConfig)) {
|
|
67
73
|
throw new Error("No session configured. Run `jaw session setup` first.");
|
|
@@ -74,20 +80,86 @@ function loadSessionConfig() {
|
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
// src/x402/asset-registry.ts
|
|
84
|
+
var USDC_BY_NETWORK = {
|
|
85
|
+
"eip155:8453": {
|
|
86
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
87
|
+
chainId: 8453,
|
|
88
|
+
wireNetwork: "eip155:8453",
|
|
89
|
+
usdcName: "USD Coin",
|
|
90
|
+
usdcVersion: "2",
|
|
91
|
+
decimals: 6
|
|
92
|
+
},
|
|
93
|
+
"eip155:84532": {
|
|
94
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
95
|
+
chainId: 84532,
|
|
96
|
+
wireNetwork: "eip155:84532",
|
|
97
|
+
usdcName: "USDC",
|
|
98
|
+
usdcVersion: "2",
|
|
99
|
+
decimals: 6
|
|
100
|
+
},
|
|
101
|
+
"eip155:137": {
|
|
102
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
103
|
+
chainId: 137,
|
|
104
|
+
wireNetwork: "eip155:137",
|
|
105
|
+
usdcName: "USD Coin",
|
|
106
|
+
usdcVersion: "2",
|
|
107
|
+
decimals: 6
|
|
108
|
+
},
|
|
109
|
+
"eip155:80002": {
|
|
110
|
+
address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
|
|
111
|
+
chainId: 80002,
|
|
112
|
+
wireNetwork: "eip155:80002",
|
|
113
|
+
usdcName: "USDC",
|
|
114
|
+
usdcVersion: "2",
|
|
115
|
+
decimals: 6
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function usdcForNetwork(network) {
|
|
119
|
+
return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/x402/permit2.ts
|
|
123
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
124
|
+
|
|
77
125
|
// src/lib/session-bridge.ts
|
|
126
|
+
var JAW_ERC20_PAYMASTER_URL = "https://api.justaname.id/proxy/v1/rpc/erc20-paymaster";
|
|
127
|
+
function resolvePaymaster(options) {
|
|
128
|
+
if (options.paymasterUrl) {
|
|
129
|
+
return { paymasterUrl: options.paymasterUrl, paymasterContext: options.paymasterContext };
|
|
130
|
+
}
|
|
131
|
+
const configured = loadConfig().paymasters?.[options.chainId];
|
|
132
|
+
if (configured) {
|
|
133
|
+
return { paymasterUrl: configured.url, paymasterContext: configured.context };
|
|
134
|
+
}
|
|
135
|
+
if (!options.apiKey) return {};
|
|
136
|
+
const asset = usdcForNetwork(`eip155:${options.chainId}`);
|
|
137
|
+
if (!asset) {
|
|
138
|
+
console.warn(
|
|
139
|
+
`[jaw] No USDC in the x402 asset registry for chain ${options.chainId}, so no ERC-20 paymaster can be engaged. Gas will come out of the account\u2019s native balance. Set \`paymasters\` in your config to sponsor this chain.`
|
|
140
|
+
);
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
const url = new URL(JAW_ERC20_PAYMASTER_URL);
|
|
144
|
+
url.searchParams.set("chainId", String(options.chainId));
|
|
145
|
+
url.searchParams.set("api-key", options.apiKey);
|
|
146
|
+
return { paymasterUrl: url.toString(), paymasterContext: { token: asset.address } };
|
|
147
|
+
}
|
|
148
|
+
function explainUnchargeableSender(err, sessionAddress) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
if (!message.includes("Could not size the ERC-20 paymaster approval")) return err;
|
|
151
|
+
return new Error(
|
|
152
|
+
`${message}
|
|
153
|
+
|
|
154
|
+
If ${sessionAddress} holds no USDC, that is why: it pays for its own gas and cannot be charged with an empty balance. Send it 0.1 USDC, or run \`jaw session setup\` again.`,
|
|
155
|
+
{ cause: err }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
78
158
|
var SessionBridge = class {
|
|
79
159
|
options;
|
|
80
160
|
session = null;
|
|
81
161
|
constructor(options) {
|
|
82
|
-
this.options = { ...options };
|
|
83
|
-
if (!this.options.paymasterUrl) {
|
|
84
|
-
const config = loadConfig();
|
|
85
|
-
const pm = config.paymasters?.[this.options.chainId];
|
|
86
|
-
if (pm) {
|
|
87
|
-
this.options.paymasterUrl = pm.url;
|
|
88
|
-
this.options.paymasterContext = pm.context;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
162
|
+
this.options = { ...options, ...resolvePaymaster(options) };
|
|
91
163
|
}
|
|
92
164
|
async getSession() {
|
|
93
165
|
if (this.session) {
|
|
@@ -96,6 +168,11 @@ var SessionBridge = class {
|
|
|
96
168
|
}
|
|
97
169
|
const config = loadSessionConfig();
|
|
98
170
|
this.checkExpiry(config);
|
|
171
|
+
if (isLegacySession(config)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"This session was created by an older CLI and uses a session address separate from the session key. Run `jaw session setup` to recreate it, which offers to revoke the old permission first. `jaw session status` still shows the old session, and `jaw session revoke` still revokes it."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
99
176
|
if (config.chainId !== this.options.chainId) {
|
|
100
177
|
throw new Error(
|
|
101
178
|
`Session was created for chain ${config.chainId}, but --chain ${this.options.chainId} was requested. Run \`jaw session setup --chain ${this.options.chainId}\` to create a session for that chain.`
|
|
@@ -113,8 +190,14 @@ var SessionBridge = class {
|
|
|
113
190
|
paymasterUrl: this.options.paymasterUrl,
|
|
114
191
|
paymasterContext: this.options.paymasterContext
|
|
115
192
|
},
|
|
116
|
-
localAccount
|
|
193
|
+
localAccount,
|
|
194
|
+
{ eip7702: true }
|
|
117
195
|
);
|
|
196
|
+
if (account.address.toLowerCase() !== config.sessionAddress.toLowerCase()) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Session key derives ${account.address}, but the stored session address is ${config.sessionAddress}. The keystore and session config are out of sync. Run \`jaw session setup\` to recreate the session.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
118
201
|
this.session = { account, config };
|
|
119
202
|
return this.session;
|
|
120
203
|
}
|
|
@@ -124,6 +207,46 @@ var SessionBridge = class {
|
|
|
124
207
|
throw new Error(`Session expired on ${expiryDate}. Run \`jaw session setup\` to create a new session.`);
|
|
125
208
|
}
|
|
126
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Approve Permit2 to move one of the payer's tokens, and return the batch id.
|
|
212
|
+
*
|
|
213
|
+
* The only call this session sends outside its permission, and the only one
|
|
214
|
+
* that can be: `JustaPermissionManager` checks every call's selector against
|
|
215
|
+
* the grant, and the x402 grant permits `transfer` alone, so an approval
|
|
216
|
+
* routed through the permission reverts before anything else happens. Sent by
|
|
217
|
+
* the session on its own balance it never reaches the manager at all, whose
|
|
218
|
+
* approval revocation and Permit2 lockdown act on the granting account and
|
|
219
|
+
* only within their own execution.
|
|
220
|
+
*
|
|
221
|
+
* Being outside the permission is exactly why it is not a general send. It
|
|
222
|
+
* takes a token and nothing else: the spender is Permit2 and the amount is
|
|
223
|
+
* the maximum, neither reachable by a caller, and the token has to be the
|
|
224
|
+
* registry's USDC for this session's chain. There is no shape of argument
|
|
225
|
+
* that turns this into an arbitrary transfer, which matters because an agent
|
|
226
|
+
* reaches the tools that reach this.
|
|
227
|
+
*/
|
|
228
|
+
async approvePermit2(token) {
|
|
229
|
+
const { account, config } = await this.getSession();
|
|
230
|
+
const usdc = usdcForNetwork(`eip155:${config.chainId}`);
|
|
231
|
+
if (!usdc || token.toLowerCase() !== usdc.address.toLowerCase()) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Refusing to approve Permit2 for ${token}: only the registry USDC on chain ${config.chainId} is allowed.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const data = encodeFunctionData({
|
|
237
|
+
abi: erc20Abi,
|
|
238
|
+
functionName: "approve",
|
|
239
|
+
args: [PERMIT2_ADDRESS, maxUint256]
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
const sent = await account.sendCalls([{ to: usdc.address, data }]);
|
|
243
|
+
const id = typeof sent === "string" ? sent : sent?.id;
|
|
244
|
+
if (!id) throw new Error("approval submitted but no call id was returned");
|
|
245
|
+
return id;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw explainUnchargeableSender(err, config.sessionAddress);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
127
250
|
async request(method, params) {
|
|
128
251
|
const { account, config } = await this.getSession();
|
|
129
252
|
switch (method) {
|
|
@@ -133,24 +256,25 @@ var SessionBridge = class {
|
|
|
133
256
|
case "wallet_sendCalls": {
|
|
134
257
|
const payload = Array.isArray(params) ? params[0] : params;
|
|
135
258
|
const { calls } = payload;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
259
|
+
const sendOptions = { permissionId: config.permissionId };
|
|
260
|
+
try {
|
|
261
|
+
return await account.sendCalls(calls, sendOptions);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw explainUnchargeableSender(err, config.sessionAddress);
|
|
264
|
+
}
|
|
139
265
|
}
|
|
140
266
|
case "wallet_getCallsStatus": {
|
|
141
267
|
const batchId = Array.isArray(params) ? params[0] : params;
|
|
142
268
|
return account.getCallStatus(batchId);
|
|
143
269
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
case "eth_signTypedData_v4":
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return account.signTypedData(typedData);
|
|
153
|
-
}
|
|
270
|
+
// Refused rather than absent, so the reason is on screen instead of a
|
|
271
|
+
// caller reading "not supported in auto mode" and looking for a flag. See
|
|
272
|
+
// `supportsSessionMode` in rpc-classifier.ts for why.
|
|
273
|
+
case "personal_sign":
|
|
274
|
+
case "eth_signTypedData_v4":
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${method} is not available in auto mode: a signature the session makes is not a call, so it never reaches the spend caps or the ledger. Run it through the browser instead.`
|
|
277
|
+
);
|
|
154
278
|
case "wallet_grantPermissions":
|
|
155
279
|
throw new Error("Requires browser \u2014 run `jaw session setup`.");
|
|
156
280
|
case "wallet_revokePermissions":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/config.ts","../../src/lib/keystore.ts","../../src/lib/session-config.ts","../../src/lib/session-bridge.ts"],"names":["fs2","fs3"],"mappings":"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","file":"session-bridge.js","sourcesContent":["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} as const;\n","import * as fs from 'node:fs';\nimport { PATHS } from './paths.js';\nimport type { JawConfig } from './types.js';\nimport { isValidKeysUrl, isValidRelayUrl } from './validation.js';\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nfunction migrateConfig(config: JawConfig): JawConfig {\n if (config.paymasterUrl && !config.paymasters) {\n const chainId = config.defaultChain ?? 1;\n config.paymasters = { [chainId]: { url: config.paymasterUrl } };\n delete config.paymasterUrl;\n saveConfig(config);\n }\n return config;\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, 'utf-8');\n try {\n const config = JSON.parse(raw) as JawConfig;\n return migrateConfig(config);\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n}\n\n/** Paymaster URLs embed provider API keys as query params (e.g. Pimlico's ?apikey=...). */\nfunction redactUrlSecrets(url: string): string {\n try {\n const parsed = new URL(url);\n for (const key of [...parsed.searchParams.keys()]) {\n parsed.searchParams.set(key, '***');\n }\n return parsed.toString();\n } catch {\n return '***';\n }\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n ...(config.paymasters && {\n paymasters: Object.fromEntries(\n Object.entries(config.paymasters).map(([chainId, pm]) => [\n chainId,\n // context is a free-form object (usually just a sponsorshipPolicyId) but a\n // provider could stash a token there, so mask it rather than hand it to the agent.\n { ...pm, url: redactUrlSecrets(pm.url), context: pm.context ? '***' : undefined },\n ])\n ),\n }),\n };\n}\n\nexport function setConfigValue(key: keyof JawConfig, value: string | number): void {\n if (key === 'keysUrl' && typeof value === 'string' && !isValidKeysUrl(value)) {\n throw new Error(`Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`);\n }\n if (key === 'relayUrl' && typeof value === 'string' && !isValidRelayUrl(value)) {\n throw new Error(`Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`);\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport { PATHS } from './paths.js';\nimport { ensureDir } from './config.js';\n\ninterface KeystoreFile {\n version: 2;\n privateKey: string;\n address: string;\n createdAt: string;\n}\n\n/**\n * Generate a random secp256k1 private key as 0x-prefixed hex.\n */\nexport function generateSessionKey(): `0x${string}` {\n const bytes = crypto.randomBytes(32);\n return `0x${bytes.toString('hex')}` as `0x${string}`;\n}\n\n/**\n * Save private key to keystore.json.\n * On-chain PermissionManager is the real security boundary — the session key\n * can only act within its granted permission scope regardless of local access.\n */\nexport function saveKeystore(privateKeyHex: string, address: string): void {\n const keystore: KeystoreFile = {\n version: 2,\n privateKey: privateKeyHex,\n address,\n createdAt: new Date().toISOString(),\n };\n\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.keystore, JSON.stringify(keystore, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n // `mode` is only honored on file creation, not overwrite — re-apply explicitly.\n fs.chmodSync(PATHS.keystore, 0o600);\n}\n\n/**\n * Load private key hex from keystore.json.\n */\nexport function loadSessionKey(): string {\n if (!fs.existsSync(PATHS.keystore)) {\n throw new Error('No session configured. Run `jaw session setup` first.');\n }\n\n const contents = fs.readFileSync(PATHS.keystore, 'utf-8');\n let parsed: KeystoreFile;\n try {\n parsed = JSON.parse(contents) as KeystoreFile;\n } catch {\n throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \\`jaw session setup\\` to recreate it.`);\n }\n return parsed.privateKey;\n}\n\n/**\n * Delete keystore.json.\n */\nexport function deleteKeystore(): void {\n if (fs.existsSync(PATHS.keystore)) {\n fs.unlinkSync(PATHS.keystore);\n }\n}\n\n/**\n * Check if keystore.json exists.\n */\nexport function keystoreExists(): boolean {\n return fs.existsSync(PATHS.keystore);\n}\n","import * as fs from 'node:fs';\nimport { PATHS } from './paths.js';\nimport { ensureDir } from './config.js';\n\nexport interface SessionConfig {\n ownerAddress: string;\n sessionAddress: string;\n permissionId: string;\n chainId: number;\n expiry: number;\n createdAt: string;\n}\n\nexport function saveSessionConfig(input: Omit<SessionConfig, 'createdAt'>): void {\n const config: SessionConfig = {\n ...input,\n createdAt: new Date().toISOString(),\n };\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.sessionConfig, JSON.stringify(config, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n fs.chmodSync(PATHS.sessionConfig, 0o600);\n}\n\nexport function loadSessionConfig(): SessionConfig {\n if (!fs.existsSync(PATHS.sessionConfig)) {\n throw new Error('No session configured. Run `jaw session setup` first.');\n }\n const raw = fs.readFileSync(PATHS.sessionConfig, 'utf-8');\n try {\n return JSON.parse(raw) as SessionConfig;\n } catch {\n throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \\`jaw session setup\\` to recreate it.`);\n }\n}\n\nexport function deleteSessionConfig(): void {\n if (fs.existsSync(PATHS.sessionConfig)) {\n fs.unlinkSync(PATHS.sessionConfig);\n }\n}\n","import { loadSessionKey } from './keystore.js';\nimport { loadSessionConfig, type SessionConfig } from './session-config.js';\nimport { loadConfig } from './config.js';\n\nexport interface SessionBridgeOptions {\n apiKey: string;\n chainId: number;\n paymasterUrl?: string;\n paymasterContext?: Record<string, unknown>;\n}\n\n/** Lazily resolved Account instance + session config */\ninterface InitializedSession {\n account: {\n address: string;\n sendCalls: (...args: unknown[]) => Promise<unknown>;\n getCallStatus: (batchId: `0x${string}`) => Promise<unknown>;\n signMessage: (message: string) => Promise<`0x${string}`>;\n signTypedData: (typedData: unknown) => Promise<`0x${string}`>;\n };\n config: SessionConfig;\n}\n\nexport class SessionBridge {\n private readonly options: SessionBridgeOptions;\n private session: InitializedSession | null = null;\n\n constructor(options: SessionBridgeOptions) {\n this.options = { ...options };\n\n if (!this.options.paymasterUrl) {\n const config = loadConfig();\n const pm = config.paymasters?.[this.options.chainId];\n if (pm) {\n this.options.paymasterUrl = pm.url;\n this.options.paymasterContext = pm.context;\n }\n }\n }\n\n private async getSession(): Promise<InitializedSession> {\n if (this.session) {\n this.checkExpiry(this.session.config);\n return this.session;\n }\n\n const config = loadSessionConfig();\n this.checkExpiry(config);\n\n if (config.chainId !== this.options.chainId) {\n throw new Error(\n `Session was created for chain ${config.chainId}, but --chain ${this.options.chainId} was requested. ` +\n `Run \\`jaw session setup --chain ${this.options.chainId}\\` to create a session for that chain.`\n );\n }\n\n let privateKeyHex: string | null = loadSessionKey();\n\n const { privateKeyToAccount } = await import('viem/accounts');\n const localAccount = privateKeyToAccount(privateKeyHex as `0x${string}`);\n privateKeyHex = null;\n\n const { Account } = await import('@jaw.id/core');\n const account = await Account.fromLocalAccount(\n {\n chainId: this.options.chainId,\n apiKey: this.options.apiKey,\n paymasterUrl: this.options.paymasterUrl,\n paymasterContext: this.options.paymasterContext,\n },\n localAccount\n );\n\n this.session = { account: account as InitializedSession['account'], config };\n return this.session;\n }\n\n private checkExpiry(config: SessionConfig): void {\n if (config.expiry <= Date.now() / 1000) {\n const expiryDate = new Date(config.expiry * 1000).toISOString();\n throw new Error(`Session expired on ${expiryDate}. Run \\`jaw session setup\\` to create a new session.`);\n }\n }\n\n async request(method: string, params?: unknown): Promise<unknown> {\n const { account, config } = await this.getSession();\n\n switch (method) {\n case 'eth_requestAccounts':\n case 'eth_accounts':\n return [config.sessionAddress];\n\n case 'wallet_sendCalls': {\n const payload = Array.isArray(params) ? params[0] : params;\n const { calls } = payload as {\n calls: Array<{ to: string; value?: string; data?: string }>;\n };\n return account.sendCalls(calls, {\n permissionId: config.permissionId as `0x${string}`,\n });\n }\n\n case 'wallet_getCallsStatus': {\n const batchId = Array.isArray(params) ? params[0] : params;\n return account.getCallStatus(batchId as `0x${string}`);\n }\n\n case 'personal_sign': {\n const message = Array.isArray(params) ? params[0] : params;\n return account.signMessage(message as string);\n }\n\n case 'eth_signTypedData_v4': {\n const asArray = Array.isArray(params) ? params : [params];\n const raw = asArray.length > 1 ? asArray[1] : asArray[0];\n const typedData = typeof raw === 'string' ? JSON.parse(raw) : raw;\n return account.signTypedData(typedData);\n }\n\n case 'wallet_grantPermissions':\n throw new Error('Requires browser — run `jaw session setup`.');\n\n case 'wallet_revokePermissions':\n throw new Error('Requires browser — run `jaw session revoke`.');\n\n default:\n throw new Error(`Method ${method} is not supported in auto mode.`);\n }\n }\n\n close(): void {\n // No-op — no WebSocket to close\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/config.ts","../../src/lib/keystore.ts","../../src/lib/session-config.ts","../../src/x402/asset-registry.ts","../../src/x402/permit2.ts","../../src/lib/session-bridge.ts"],"names":["fs2","fs3"],"mappings":";;;;;;;AAGA,IAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAQ,EAAA,CAAA,OAAA,EAAQ,EAAG,MAAM,CAAA;AAEvC,IAAM,KAAA,GAAQ;AAAA,EACnB,IAAA,EAAM,OAAA;AAAA,EACN,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,cAAc,CAAA;AAAA,EAC1C,KAAA,EAAY,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,YAAY,CAAA;AAAA,EACtC,QAAA,EAAe,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,eAAe,CAAA;AAAA,EAC5C,aAAA,EAAoB,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,qBAAqB,CAAA;AAAA,EACvD,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,gBAAgB,CAAA;AAAA,EAC5C,WAAA,EAAkB,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,mBAAmB;AACrD,CAAA;ACRO,SAAS,UAAU,GAAA,EAAmB;AAC3C,EAAG,aAAU,GAAA,EAAK,EAAE,WAAW,IAAA,EAAM,IAAA,EAAM,KAAO,CAAA;AAClD,EAAG,EAAA,CAAA,SAAA,CAAU,KAAK,GAAK,CAAA;AACzB;AAEA,SAAS,cAAc,MAAA,EAA8B;AACnD,EAAA,IAAI,MAAA,CAAO,YAAA,IAAgB,CAAC,MAAA,CAAO,UAAA,EAAY;AAC7C,IAAA,MAAM,OAAA,GAAU,OAAO,YAAA,IAAgB,CAAA;AACvC,IAAA,MAAA,CAAO,UAAA,GAAa,EAAE,CAAC,OAAO,GAAG,EAAE,GAAA,EAAK,MAAA,CAAO,YAAA,EAAa,EAAE;AAC9D,IAAA,OAAO,MAAA,CAAO,YAAA;AACd,IAAA,UAAA,CAAW,MAAM,CAAA;AAAA,EACnB;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,UAAA,GAAwB;AACtC,EAAA,IAAI,CAAI,EAAA,CAAA,UAAA,CAAW,KAAA,CAAM,MAAM,CAAA,EAAG;AAChC,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,MAAM,GAAA,GAAS,EAAA,CAAA,YAAA,CAAa,KAAA,CAAM,MAAA,EAAQ,OAAO,CAAA;AACjD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,OAAO,cAAc,MAAM,CAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,eAAA,EAAkB,MAAM,MAAM,CAAA,oEAAA;AAAA,KAChC;AAAA,EACF;AACF;AAEO,SAAS,WAAW,MAAA,EAAyB;AAClD,EAAA,SAAA,CAAU,MAAM,IAAI,CAAA;AACpB,EAAG,EAAA,CAAA,aAAA,CAAc,MAAM,MAAA,EAAQ,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAA,GAAI,IAAA,EAAM;AAAA,IACrE,QAAA,EAAU,OAAA;AAAA,IACV,IAAA,EAAM;AAAA,GACP,CAAA;AACH;;;ACGO,SAAS,cAAA,GAAyB;AACvC,EAAA,IAAI,CAAIA,EAAA,CAAA,UAAA,CAAW,KAAA,CAAM,QAAQ,CAAA,EAAG;AAClC,IAAA,MAAM,IAAI,MAAM,uDAAuD,CAAA;AAAA,EACzE;AAEA,EAAA,MAAM,QAAA,GAAcA,EAAA,CAAA,YAAA,CAAa,KAAA,CAAM,QAAA,EAAU,OAAO,CAAA;AACxD,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,QAAQ,CAAA;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,YAAA,EAAe,KAAA,CAAM,QAAQ,CAAA,wDAAA,CAA0D,CAAA;AAAA,EACzG;AACA,EAAA,OAAO,MAAA,CAAO,UAAA;AAChB;ACoEO,SAAS,gBAAgB,MAAA,EAA8C;AAC5E,EAAA,OAAO,OAAO,IAAA,KAAS,SAAA;AACzB;AAyKO,SAAS,iBAAA,GAAmC;AACjD,EAAA,IAAI,CAAIC,EAAA,CAAA,UAAA,CAAW,KAAA,CAAM,aAAa,CAAA,EAAG;AACvC,IAAA,MAAM,IAAI,MAAM,uDAAuD,CAAA;AAAA,EACzE;AACA,EAAA,MAAM,GAAA,GAASA,EAAA,CAAA,YAAA,CAAa,KAAA,CAAM,aAAA,EAAe,OAAO,CAAA;AACxD,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAA,CAAM,aAAa,CAAA,wDAAA,CAA0D,CAAA;AAAA,EACpH;AACF;;;AC5RO,IAAM,eAAA,GAA6C;AAAA,EACxD,aAAA,EAAe;AAAA,IACb,OAAA,EAAS,4CAAA;AAAA,IACT,OAAA,EAAS,IAAA;AAAA,IACT,WAAA,EAAa,aAAA;AAAA,IACb,QAAA,EAAU,UAAA;AAAA,IACV,WAAA,EAAa,GAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,OAAA,EAAS,4CAAA;AAAA,IACT,OAAA,EAAS,KAAA;AAAA,IACT,WAAA,EAAa,cAAA;AAAA,IACb,QAAA,EAAU,MAAA;AAAA,IACV,WAAA,EAAa,GAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,OAAA,EAAS,4CAAA;AAAA,IACT,OAAA,EAAS,GAAA;AAAA,IACT,WAAA,EAAa,YAAA;AAAA,IACb,QAAA,EAAU,UAAA;AAAA,IACV,WAAA,EAAa,GAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,OAAA,EAAS,4CAAA;AAAA,IACT,OAAA,EAAS,KAAA;AAAA,IACT,WAAA,EAAa,cAAA;AAAA,IACb,QAAA,EAAU,MAAA;AAAA,IACV,WAAA,EAAa,GAAA;AAAA,IACb,QAAA,EAAU;AAAA;AAEd,CAAA;AAUO,SAAS,eAAe,OAAA,EAAwC;AACrE,EAAA,OAAO,OAAO,MAAA,CAAO,eAAA,EAAiB,OAAO,CAAA,GAAI,eAAA,CAAgB,OAAO,CAAA,GAAI,MAAA;AAC9E;;;AChDO,IAAM,eAAA,GAAkB,4CAAA;;;ACR/B,IAAM,uBAAA,GAA0B,uDAAA;AAkBhC,SAAS,iBACP,OAAA,EACiE;AACjE,EAAA,IAAI,QAAQ,YAAA,EAAc;AACxB,IAAA,OAAO,EAAE,YAAA,EAAc,OAAA,CAAQ,YAAA,EAAc,gBAAA,EAAkB,QAAQ,gBAAA,EAAiB;AAAA,EAC1F;AAEA,EAAA,MAAM,UAAA,GAAa,UAAA,EAAW,CAAE,UAAA,GAAa,QAAQ,OAAO,CAAA;AAC5D,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,OAAO,EAAE,YAAA,EAAc,UAAA,CAAW,GAAA,EAAK,gBAAA,EAAkB,WAAW,OAAA,EAAQ;AAAA,EAC9E;AAIA,EAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAC;AAO7B,EAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,CAAA,OAAA,EAAU,OAAA,CAAQ,OAAO,CAAA,CAAE,CAAA;AACxD,EAAA,IAAI,CAAC,KAAA,EAAO;AAIV,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,CAAA,mDAAA,EAAsD,QAAQ,OAAO,CAAA,yJAAA;AAAA,KAGvE;AACA,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,uBAAuB,CAAA;AAC3C,EAAA,GAAA,CAAI,aAAa,GAAA,CAAI,SAAA,EAAW,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAC,CAAA;AACvD,EAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,SAAA,EAAW,OAAA,CAAQ,MAAM,CAAA;AAC9C,EAAA,OAAO,EAAE,YAAA,EAAc,GAAA,CAAI,QAAA,EAAS,EAAG,kBAAkB,EAAE,KAAA,EAAO,KAAA,CAAM,OAAA,EAAQ,EAAE;AACpF;AAcA,SAAS,yBAAA,CAA0B,KAAc,cAAA,EAAiC;AAChF,EAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,8CAA8C,GAAG,OAAO,GAAA;AAC9E,EAAA,OAAO,IAAI,KAAA;AAAA,IACT,GAAG,OAAO;;AAAA,GAAA,EAAU,cAAc,CAAA,uJAAA,CAAA;AAAA,IAElC,EAAE,OAAO,GAAA;AAAI,GACf;AACF;AAmBO,IAAM,gBAAN,MAAoB;AAAA,EACR,OAAA;AAAA,EACT,OAAA,GAAqC,IAAA;AAAA,EAE7C,YAAY,OAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,UAAU,EAAE,GAAG,SAAS,GAAG,gBAAA,CAAiB,OAAO,CAAA,EAAE;AAAA,EAC5D;AAAA,EAEA,MAAc,UAAA,GAA0C;AACtD,IAAA,IAAI,KAAK,OAAA,EAAS;AAChB,MAAA,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,OAAA,CAAQ,MAAM,CAAA;AACpC,MAAA,OAAO,IAAA,CAAK,OAAA;AAAA,IACd;AAEA,IAAA,MAAM,SAAS,iBAAA,EAAkB;AACjC,IAAA,IAAA,CAAK,YAAY,MAAM,CAAA;AAOvB,IAAA,IAAI,eAAA,CAAgB,MAAM,CAAA,EAAG;AAC3B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OAGF;AAAA,IACF;AAEA,IAAA,IAAI,MAAA,CAAO,OAAA,KAAY,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS;AAC3C,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,8BAAA,EAAiC,MAAA,CAAO,OAAO,CAAA,cAAA,EAAiB,IAAA,CAAK,QAAQ,OAAO,CAAA,gDAAA,EAC/C,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,sCAAA;AAAA,OAC3D;AAAA,IACF;AAEA,IAAA,IAAI,gBAA+B,cAAA,EAAe;AAElD,IAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,MAAM,OAAO,eAAe,CAAA;AAC5D,IAAA,MAAM,YAAA,GAAe,oBAAoB,aAA8B,CAAA;AACvE,IAAA,aAAA,GAAgB,IAAA;AAEhB,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAM,OAAO,cAAc,CAAA;AAI/C,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,gBAAA;AAAA,MAC5B;AAAA,QACE,OAAA,EAAS,KAAK,OAAA,CAAQ,OAAA;AAAA,QACtB,MAAA,EAAQ,KAAK,OAAA,CAAQ,MAAA;AAAA,QACrB,YAAA,EAAc,KAAK,OAAA,CAAQ,YAAA;AAAA,QAC3B,gBAAA,EAAkB,KAAK,OAAA,CAAQ;AAAA,OACjC;AAAA,MACA,YAAA;AAAA,MACA,EAAE,SAAS,IAAA;AAAK,KAClB;AAMA,IAAA,IAAI,QAAQ,OAAA,CAAQ,WAAA,OAAkB,MAAA,CAAO,cAAA,CAAe,aAAY,EAAG;AACzE,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,oBAAA,EAAuB,OAAA,CAAQ,OAAO,CAAA,oCAAA,EAAuC,OAAO,cAAc,CAAA,qGAAA;AAAA,OAEpG;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,OAAA,GAAU,EAAE,OAAA,EAAmD,MAAA,EAAO;AAC3E,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEQ,YAAY,MAAA,EAA6B;AAC/C,IAAA,IAAI,MAAA,CAAO,MAAA,IAAU,IAAA,CAAK,GAAA,KAAQ,GAAA,EAAM;AACtC,MAAA,MAAM,aAAa,IAAI,IAAA,CAAK,OAAO,MAAA,GAAS,GAAI,EAAE,WAAA,EAAY;AAC9D,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,UAAU,CAAA,oDAAA,CAAsD,CAAA;AAAA,IACxG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,eAAe,KAAA,EAAuC;AAC1D,IAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAO,GAAI,MAAM,KAAK,UAAA,EAAW;AAClD,IAAA,MAAM,IAAA,GAAO,cAAA,CAAe,CAAA,OAAA,EAAU,MAAA,CAAO,OAAO,CAAA,CAAE,CAAA;AACtD,IAAA,IAAI,CAAC,QAAQ,KAAA,CAAM,WAAA,OAAkB,IAAA,CAAK,OAAA,CAAQ,aAAY,EAAG;AAC/D,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,gCAAA,EAAmC,KAAK,CAAA,kCAAA,EAAqC,MAAA,CAAO,OAAO,CAAA,YAAA;AAAA,OAC7F;AAAA,IACF;AAEA,IAAA,MAAM,OAAO,kBAAA,CAAmB;AAAA,MAC9B,GAAA,EAAK,QAAA;AAAA,MACL,YAAA,EAAc,SAAA;AAAA,MACd,IAAA,EAAM,CAAC,eAAA,EAAiB,UAAU;AAAA,KACnC,CAAA;AAED,IAAA,IAAI;AAIF,MAAA,MAAM,IAAA,GAAgB,MAAM,OAAA,CAAQ,SAAA,CAAU,CAAC,EAAE,EAAA,EAAI,IAAA,CAAK,OAAA,EAAS,IAAA,EAAM,CAAC,CAAA;AAC1E,MAAA,MAAM,EAAA,GAAK,OAAO,IAAA,KAAS,QAAA,GAAW,OAAQ,IAAA,EAAiC,EAAA;AAC/E,MAAA,IAAI,CAAC,EAAA,EAAI,MAAM,IAAI,MAAM,gDAAgD,CAAA;AACzE,MAAA,OAAO,EAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,yBAAA,CAA0B,GAAA,EAAK,MAAA,CAAO,cAAc,CAAA;AAAA,IAC5D;AAAA,EACF;AAAA,EAEA,MAAM,OAAA,CAAQ,MAAA,EAAgB,MAAA,EAAoC;AAChE,IAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAO,GAAI,MAAM,KAAK,UAAA,EAAW;AAElD,IAAA,QAAQ,MAAA;AAAQ,MACd,KAAK,qBAAA;AAAA,MACL,KAAK,cAAA;AACH,QAAA,OAAO,CAAC,OAAO,cAAc,CAAA;AAAA,MAE/B,KAAK,kBAAA,EAAoB;AACvB,QAAA,MAAM,UAAU,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA,GAAI,MAAA;AACpD,QAAA,MAAM,EAAE,OAAM,GAAI,OAAA;AAGlB,QAAA,MAAM,WAAA,GAAc,EAAE,YAAA,EAAc,MAAA,CAAO,YAAA,EAA8B;AAKzE,QAAA,IAAI;AACF,UAAA,OAAO,MAAM,OAAA,CAAQ,SAAA,CAAU,KAAA,EAAO,WAAW,CAAA;AAAA,QACnD,SAAS,GAAA,EAAK;AACZ,UAAA,MAAM,yBAAA,CAA0B,GAAA,EAAK,MAAA,CAAO,cAAc,CAAA;AAAA,QAC5D;AAAA,MACF;AAAA,MAEA,KAAK,uBAAA,EAAyB;AAC5B,QAAA,MAAM,UAAU,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA,GAAI,MAAA;AACpD,QAAA,OAAO,OAAA,CAAQ,cAAc,OAAwB,CAAA;AAAA,MACvD;AAAA;AAAA;AAAA;AAAA,MAKA,KAAK,eAAA;AAAA,MACL,KAAK,sBAAA;AACH,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,GAAG,MAAM,CAAA,kKAAA;AAAA,SACX;AAAA,MAEF,KAAK,yBAAA;AACH,QAAA,MAAM,IAAI,MAAM,kDAA6C,CAAA;AAAA,MAE/D,KAAK,0BAAA;AACH,QAAA,MAAM,IAAI,MAAM,mDAA8C,CAAA;AAAA,MAEhE;AACE,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,OAAA,EAAU,MAAM,CAAA,+BAAA,CAAiC,CAAA;AAAA;AACrE,EACF;AAAA,EAEA,KAAA,GAAc;AAAA,EAEd;AACF","file":"session-bridge.js","sourcesContent":["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 './paths.js';\nimport type { JawConfig, SettableConfigKey } from './types.js';\nimport type { X402Policy, X402PolicyKey } from '../x402/policy.js';\nimport { isValidKeysUrl, isValidRelayUrl } from './validation.js';\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nfunction migrateConfig(config: JawConfig): JawConfig {\n if (config.paymasterUrl && !config.paymasters) {\n const chainId = config.defaultChain ?? 1;\n config.paymasters = { [chainId]: { url: config.paymasterUrl } };\n delete config.paymasterUrl;\n saveConfig(config);\n }\n return config;\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, 'utf-8');\n try {\n const config = JSON.parse(raw) as JawConfig;\n return migrateConfig(config);\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n}\n\n/** Paymaster URLs embed provider API keys as query params (e.g. Pimlico's ?apikey=...). */\nfunction redactUrlSecrets(url: string): string {\n try {\n const parsed = new URL(url);\n for (const key of [...parsed.searchParams.keys()]) {\n parsed.searchParams.set(key, '***');\n }\n return parsed.toString();\n } catch {\n return '***';\n }\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n ...(config.paymasters && {\n paymasters: Object.fromEntries(\n Object.entries(config.paymasters).map(([chainId, pm]) => [\n chainId,\n // context is a free-form object (usually just a sponsorshipPolicyId) but a\n // provider could stash a token there, so mask it rather than hand it to the agent.\n { ...pm, url: redactUrlSecrets(pm.url), context: pm.context ? '***' : undefined },\n ])\n ),\n }),\n };\n}\n\n/**\n * Set one field of the `x402` payment policy. Array fields (allow-lists) are\n * comma-split. Intentionally NOT reachable from the MCP tool surface: an agent\n * must not be able to raise its own spending caps — only a human at the CLI.\n */\nexport function setX402PolicyValue(key: X402PolicyKey, value: string): void {\n const config = loadConfig();\n const x402: X402Policy = { ...(config.x402 ?? {}) };\n if (key === 'maxAmountPerPayment' || key === 'maxTotalPerSession' || key === 'topUpFloat') {\n // These are base-unit amounts read via BigInt() on the hot payment path;\n // reject anything that isn't a non-negative integer here so a bad value\n // can never turn into a per-payment failure later.\n if (!/^\\d+$/.test(value.trim())) {\n throw new Error(`${key} must be a non-negative integer (base units), got: ${value}`);\n }\n x402[key] = value.trim();\n } else {\n x402[key] = value\n .split(',')\n .map((v) => v.trim())\n .filter(Boolean);\n }\n saveConfig({ ...config, x402 });\n}\n\nexport function setConfigValue(key: SettableConfigKey, value: string | number): void {\n if (key === 'keysUrl' && typeof value === 'string' && !isValidKeysUrl(value)) {\n throw new Error(`Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`);\n }\n if (key === 'relayUrl' && typeof value === 'string' && !isValidRelayUrl(value)) {\n throw new Error(`Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`);\n }\n // Numeric keys: coerce + validate here so EVERY caller is safe. The MCP tool\n // passes raw strings (its schema types value as a string), so without this a\n // `sessionExpiry: \"abc\"` would land as a string and turn `expiry * 86400`\n // into NaN, and a `defaultChain` string would ride into an RPC URL verbatim.\n let toStore: string | number = value;\n if (key === 'defaultChain' || key === 'sessionExpiry') {\n // Strict: reject anything parseInt would silently truncate ('1.5' -> 1,\n // '0x10' -> 0, '10abc' -> 10). Only a bare positive decimal integer passes.\n const n = typeof value === 'number' ? value : /^\\d+$/.test(value.trim()) ? parseInt(value.trim(), 10) : NaN;\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(`${key} must be a positive integer, got: ${JSON.stringify(value)}`);\n }\n toStore = n;\n }\n const config = loadConfig();\n const updated = { ...config, [key]: toStore };\n saveConfig(updated);\n}\n","import * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport { PATHS } from './paths.js';\nimport { ensureDir } from './config.js';\n\ninterface KeystoreFile {\n version: 2;\n privateKey: string;\n address: string;\n createdAt: string;\n}\n\n/**\n * Generate a random secp256k1 private key as 0x-prefixed hex.\n */\nexport function generateSessionKey(): `0x${string}` {\n const bytes = crypto.randomBytes(32);\n return `0x${bytes.toString('hex')}` as `0x${string}`;\n}\n\n/**\n * Save private key to keystore.json.\n * On-chain PermissionManager is the real security boundary — the session key\n * can only act within its granted permission scope regardless of local access.\n */\nexport function saveKeystore(privateKeyHex: string, address: string): void {\n const keystore: KeystoreFile = {\n version: 2,\n privateKey: privateKeyHex,\n address,\n createdAt: new Date().toISOString(),\n };\n\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.keystore, JSON.stringify(keystore, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n // `mode` is only honored on file creation, not overwrite — re-apply explicitly.\n fs.chmodSync(PATHS.keystore, 0o600);\n}\n\n/**\n * Load private key hex from keystore.json.\n */\nexport function loadSessionKey(): string {\n if (!fs.existsSync(PATHS.keystore)) {\n throw new Error('No session configured. Run `jaw session setup` first.');\n }\n\n const contents = fs.readFileSync(PATHS.keystore, 'utf-8');\n let parsed: KeystoreFile;\n try {\n parsed = JSON.parse(contents) as KeystoreFile;\n } catch {\n throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \\`jaw session setup\\` to recreate it.`);\n }\n return parsed.privateKey;\n}\n\n/**\n * Load the session address from keystore.json, or null if it cannot be read.\n * Used to name the key in messages, so a corrupt keystore degrades the message\n * rather than the command.\n */\nexport function tryLoadKeystoreAddress(): string | null {\n try {\n if (!fs.existsSync(PATHS.keystore)) return null;\n const parsed = JSON.parse(fs.readFileSync(PATHS.keystore, 'utf-8')) as KeystoreFile;\n return parsed.address ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Delete keystore.json.\n */\nexport function deleteKeystore(): void {\n if (fs.existsSync(PATHS.keystore)) {\n fs.unlinkSync(PATHS.keystore);\n }\n}\n\n/**\n * Check if keystore.json exists.\n */\nexport function keystoreExists(): boolean {\n return fs.existsSync(PATHS.keystore);\n}\n","import * as fs from 'node:fs';\nimport { PATHS } from './paths.js';\nimport { ensureDir } from './config.js';\n\n/**\n * How the session account address is derived, as it appears on disk.\n *\n * 'eip7702' is the only one written now: the session key EOA itself, upgraded\n * in place via a delegation attached to its first userOp, so the session\n * account and the x402 payer are one address.\n *\n * 'counterfactual' is what earlier versions wrote, and what a config from\n * before the field existed means: a CREATE2 prediction from the account\n * factory, a second address that holds nothing and so cannot be charged for\n * the gas of the ops it sends. Still in the union because those files exist and\n * `SessionBridge` has to recognise them; `SessionSetup` never writes it.\n */\nexport type SessionMode = 'counterfactual' | 'eip7702';\n\n/**\n * The granted permission as `JustaPermissionManager` stores it.\n *\n * Every view on the manager takes `Permission calldata` and hashes it inside:\n * `isApproved`, `isRevoked` and `getCurrentPeriod` all do, and none of them\n * accept an id. A session holding only `permissionId` therefore holds the one\n * field the chain will not answer about, which is why nothing local could tell\n * that a permission had been revoked from another device.\n *\n * The grant response already carries all of it, so this costs a wider write\n * rather than a network call. Optional on read for two reasons: configs written\n * before this field exist, and a wallet running an older core answers with the\n * id alone. Consumers fall back to the local file, which is what those sessions\n * already meant.\n */\nexport interface GrantedPermission {\n account: string;\n spender: string;\n /**\n * Unix seconds the permission starts at. Also the anchor the contract steps\n * its period windows from, and what the local policy anchors on. A summary\n * field used to hold an approximation of it taken from the local clock.\n */\n start: number;\n /** Unix seconds the permission ends at. */\n end: number;\n /** Hex, as the grant returned it. Widened to a bigint at encode time. */\n salt: string;\n calls: Array<{ target: string; selector: string }>;\n spends: Array<{ token: string; allowance: string; unit: string; multiplier: number }>;\n}\n\nconst ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\nconst SELECTOR_RE = /^0x[0-9a-fA-F]{8}$/;\nconst HEX_RE = /^0x[0-9a-fA-F]+$/;\n/** Decimal or hex, matching what the SDK hands to `BigInt()`. */\nconst ALLOWANCE_RE = /^(0x[0-9a-fA-F]+|[0-9]+)$/;\n/** The units a grant may carry. `year` has no on-chain enum and is rewritten before encoding. */\nconst SPEND_UNITS = new Set(['minute', 'hour', 'day', 'week', 'month', 'year', 'forever']);\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0;\n}\n\n/**\n * The permission struct out of a `wallet_grantPermissions` response, or\n * undefined when the response does not carry a usable one.\n *\n * Undefined rather than a throw, on every malformed field. By the time this\n * runs the grant is already on chain, and refusing to write the session over a\n * field that arrived in an unexpected shape would strand a live permission with\n * no local record of it. A session that skips this keeps behaving the way every\n * session behaved before the field existed.\n *\n * Strict about what it does accept: a struct that is wrong in any part hashes\n * to something other than the granted permission, so every on-chain read made\n * from it would quietly answer about a permission that does not exist.\n */\nexport function parseGrantedPermission(raw: unknown): GrantedPermission | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined;\n const r = raw as Record<string, unknown>;\n\n const { account, spender, salt } = r;\n if (typeof account !== 'string' || !ADDRESS_RE.test(account)) return undefined;\n if (typeof spender !== 'string' || !ADDRESS_RE.test(spender)) return undefined;\n if (typeof salt !== 'string' || !HEX_RE.test(salt)) return undefined;\n if (!isPositiveInt(r.start) || !isPositiveInt(r.end)) return undefined;\n\n if (!Array.isArray(r.calls) || r.calls.length === 0) return undefined;\n const calls: GrantedPermission['calls'] = [];\n for (const entry of r.calls) {\n if (typeof entry !== 'object' || entry === null) return undefined;\n const { target, selector } = entry as Record<string, unknown>;\n if (typeof target !== 'string' || !ADDRESS_RE.test(target)) return undefined;\n // The response builds the selector from the signature, so an entry without\n // one cannot be reconstructed: the signature it came from is not returned.\n if (typeof selector !== 'string' || !SELECTOR_RE.test(selector)) return undefined;\n calls.push({ target, selector });\n }\n\n if (!Array.isArray(r.spends)) return undefined;\n const spends: GrantedPermission['spends'] = [];\n for (const entry of r.spends) {\n if (typeof entry !== 'object' || entry === null) return undefined;\n const { token, allowance, unit, multiplier } = entry as Record<string, unknown>;\n if (typeof token !== 'string' || !ADDRESS_RE.test(token)) return undefined;\n if (typeof allowance !== 'string' || !ALLOWANCE_RE.test(allowance)) return undefined;\n if (typeof unit !== 'string' || !SPEND_UNITS.has(unit)) return undefined;\n // `multiplier` is a uint16 on chain and defaults to 1 in the grant, but the\n // encoded struct has to carry the number the permission was hashed with,\n // so an absent one is a struct we cannot rebuild rather than a 1.\n if (!isPositiveInt(multiplier) || multiplier > 65535) return undefined;\n spends.push({ token, allowance, unit, multiplier });\n }\n\n return { account, spender, start: r.start, end: r.end, salt, calls, spends };\n}\n\n/**\n * Whether this session predates the CLI settling on one account derivation, so\n * its permission belongs to an address separate from the session key and no op\n * it sends can be charged for its own gas.\n *\n * Named rather than compared inline: three callers ask this, and each one\n * spelled as `mode !== 'eip7702'` reads like a check for a variant among\n * several, when the only question is whether the session is still usable.\n */\nexport function isLegacySession(config: Pick<SessionConfig, 'mode'>): boolean {\n return config.mode !== 'eip7702';\n}\n\n/**\n * A permission this CLI granted and then stopped tracking.\n *\n * `session setup` replaces a session rather than adding to it, and it does not\n * always revoke what it replaces: the interactive path takes no for an answer,\n * and `--yes` never revokes at all. The session key is reused by default, so\n * the address the session signs with then holds two live grants while the\n * config names one. Its real authority is the sum of both, `x402 status`\n * reports only the new one, and `session revoke` could not reach the old one\n * at all, because the id lived in the file that was overwritten.\n *\n * Keeping the id is what makes it reachable again. Nothing here revokes on its\n * own: setup already asked.\n */\nexport interface OrphanedPermission {\n id: string;\n chainId: number;\n /** Unix seconds. */\n expiry: number;\n}\n\nexport interface SessionConfig {\n ownerAddress: string;\n sessionAddress: string;\n permissionId: string;\n chainId: number;\n expiry: number;\n createdAt: string;\n mode?: SessionMode;\n /** The struct the on-chain reads need. Absent on older sessions; see the type. */\n permission?: GrantedPermission;\n /** Permissions this key still holds that the session no longer names. */\n orphanedPermissions?: OrphanedPermission[];\n /**\n * Whether the permission this session names has already been revoked, while\n * something else it holds has not.\n *\n * Its own field rather than a rewritten `expiry`, which is what this was\n * first. `expiry` means when the permission ends, and the recovered-struct\n * check compares it against the permission's own `end` to know the relay\n * answered about this session: overwriting it made that comparison fail\n * forever, so a partly revoked session could never recover its struct and\n * went back to reporting \"cannot tell\", which is the state recovery exists to\n * end. A field that says one thing cannot be borrowed to say another.\n */\n permissionRevoked?: boolean;\n}\n\n/**\n * The orphans still worth carrying, newest first.\n *\n * An expired permission authorises nothing, so it is dropped rather than\n * accumulating in the file for the life of the machine. Dropping it is the same\n * decision `session revoke` makes when it skips the browser for an expired\n * session.\n */\nexport function liveOrphans(\n orphans: OrphanedPermission[] | undefined,\n now: number = Date.now() / 1000\n): OrphanedPermission[] {\n return (orphans ?? []).filter((orphan) => orphan.expiry > now);\n}\n\n/**\n * `mode` is required and pinned here, unlike on the read side where it stays\n * optional to describe files earlier versions wrote. Nothing but `SessionSetup`\n * writes a session, and a session written without the mode would be refused by\n * `SessionBridge` as if an old CLI had made it, so the compiler holds the\n * invariant rather than a test having to.\n */\nexport function saveSessionConfig(\n input: Omit<SessionConfig, 'createdAt' | 'mode'> & {\n mode: 'eip7702';\n /**\n * Carried over when a session is being replaced in place rather than\n * started, which `session add` does. `createdAt` is what the session total\n * is counted from (`sumSpentSince(payer, session.createdAt)`), so stamping a\n * fresh one there would hand the session cap a clean slate as a side effect\n * of adding a capability.\n */\n createdAt?: string;\n }\n): void {\n writeSessionConfig({ ...input, createdAt: input.createdAt ?? new Date().toISOString() });\n}\n\n/**\n * Written to a temporary file and renamed over the real one, which is atomic on\n * the same filesystem, so a reader never sees a half-written config.\n *\n * It matters more than it did: recovering the permission struct made two\n * commands that only ever read (`x402 status`, `session status`) into writers,\n * and the MCP server runs alongside a terminal, so two processes writing at\n * once is ordinary rather than exotic. A torn file loses the permission id,\n * which is exactly the stranding the orphan list exists to prevent.\n */\nfunction writeSessionConfig(config: SessionConfig): void {\n ensureDir(PATHS.root);\n const temp = `${PATHS.sessionConfig}.${process.pid}.tmp`;\n fs.writeFileSync(temp, JSON.stringify(config, null, 2) + '\\n', { encoding: 'utf-8', mode: 0o600 });\n fs.chmodSync(temp, 0o600);\n fs.renameSync(temp, PATHS.sessionConfig);\n}\n\n/**\n * Store a permission struct recovered for a session that was written without\n * one, leaving the rest of the file alone. Same reason as below for not going\n * through `saveSessionConfig`: it stamps a fresh `createdAt`.\n */\nexport function saveRecoveredPermission(config: SessionConfig, permission: GrantedPermission): boolean {\n // Merged into the file as it is now, not into the snapshot the caller loaded.\n // Recovery holds its copy across a relay round trip, and in that time a\n // `session revoke` running beside it writes progress between browser\n // approvals: renaming the old copy back over it would restore the expiry and\n // the orphan list that revoke had just cleared, and the next revoke would\n // re-attempt ids that are already gone. Reading immediately before the write\n // does not make this a transaction, it makes the window microseconds instead\n // of the length of a network call.\n //\n // A session that disappeared in the meantime stays gone: the only thing being\n // added here is a cache of something the relay can produce again.\n const current = tryLoadSessionConfig();\n if (!current || current.permissionId !== config.permissionId) return false;\n writeSessionConfig({ ...current, permission });\n return true;\n}\n\n/**\n * Record what a revoke has already done, so the rest of it can be retried.\n *\n * Revoking is not idempotent: core reads the permission from the relay before\n * sending and deletes it from there afterwards, so a second attempt at an id\n * already revoked fails before it sends anything. A session left naming an id\n * that is gone therefore costs a browser round trip that can only fail, which\n * is why what succeeded has to come out of the file as it succeeds.\n *\n * `ownPermissionRevoked` is recorded on its own field. Borrowing `expiry` for\n * it, which is what this did first, made that field mean two things at once and\n * broke the recovered-struct check that compares it against the permission's\n * own `end`. What it is for is unchanged: stopping the next revoke from\n * attempting an id the relay no longer has.\n *\n * Separate from `saveSessionConfig` because that one stamps a fresh\n * `createdAt`, and `createdAt` is what the session total is counted from\n * (`sumSpentSince(payer, session.createdAt)`). Editing a session through it\n * would hand the session cap a clean slate as a side effect of revoking one\n * permission.\n */\nexport function saveRevokeProgress(\n config: SessionConfig,\n progress: { orphans: OrphanedPermission[]; ownPermissionRevoked: boolean }\n): void {\n // Merged into the file as it stands, for the same reason\n // `saveRecoveredPermission` does: this runs between browser round trips, and\n // a recovery finishing beside it would otherwise be dropped by the next\n // progress write.\n const next: SessionConfig = { ...(tryLoadSessionConfig() ?? config) };\n if (progress.orphans.length > 0) next.orphanedPermissions = progress.orphans;\n else delete next.orphanedPermissions;\n if (progress.ownPermissionRevoked) next.permissionRevoked = true;\n writeSessionConfig(next);\n}\n\nexport function sessionConfigExists(): boolean {\n return fs.existsSync(PATHS.sessionConfig);\n}\n\nexport function loadSessionConfig(): SessionConfig {\n if (!fs.existsSync(PATHS.sessionConfig)) {\n throw new Error('No session configured. Run `jaw session setup` first.');\n }\n const raw = fs.readFileSync(PATHS.sessionConfig, 'utf-8');\n try {\n return JSON.parse(raw) as SessionConfig;\n } catch {\n throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \\`jaw session setup\\` to recreate it.`);\n }\n}\n\n/**\n * Like `loadSessionConfig`, but returns null instead of throwing when the file\n * is missing or unreadable. For callers that can recover from a keystore whose\n * session-config is gone (interrupted setup, manual deletion, partial restore)\n * rather than callers that need an existing session to do their job.\n */\nexport function tryLoadSessionConfig(): SessionConfig | null {\n try {\n return loadSessionConfig();\n } catch {\n return null;\n }\n}\n\nexport function deleteSessionConfig(): void {\n if (fs.existsSync(PATHS.sessionConfig)) {\n fs.unlinkSync(PATHS.sessionConfig);\n }\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","/**\n * Permit2 declarations for the x402 `upto` scheme.\n *\n * `upto` does not settle through EIP-3009 the way `exact` does. The client signs\n * a Permit2 `permitWitnessTransferFrom` authorizing a ceiling, and the\n * facilitator later calls the x402 proxy with the amount the run actually\n * consumed, which the proxy refuses if it exceeds the ceiling. The witness binds\n * both the recipient and the facilitator, so a signature is not useful to anyone\n * else.\n *\n * Everything here is a declaration: the addresses, the structs, and the domain.\n * Hashing them is viem's job. Choosing a nonce, a deadline, a spender or an\n * amount, and deciding whether a challenge may be paid at all, belongs to the\n * scheme module and the policy, not to this file.\n */\n\n/**\n * Canonical Permit2, the same address `JustaPermissionManager` pins as its\n * `PERMIT2` constant. Deployed deterministically, so it does not vary per chain.\n */\nexport const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;\n\n/**\n * `x402UptoPermit2Proxy`, the spender the payer authorizes and the only contract\n * allowed to settle the permit.\n *\n * Pinned rather than read from the challenge. The spender is what a Permit2\n * signature hands the ability to move funds to, so accepting a server-supplied\n * one would authorize a stranger to pull up to the ceiling. This is the same\n * rule the `exact` scheme already applies to the token address.\n *\n * Verified on chain on 2026-08-28, not just read from a repo: deployed at this\n * address on both Base Mainnet and Base Sepolia with an identical codehash\n * (`0x4662dc27...`), which is what a deterministic CREATE2 deployment should\n * look like. The runtime bytecode contains the witness type string and the\n * typehash below as literals, so the transcription is checked against the\n * contract that will actually run and not only against its source.\n *\n * Two traps live near this address. The x402 README still lists Base Mainnet as\n * having no `upto` deployment, which is stale. And Base Sepolia carries a\n * second, legacy proxy at `0x402039b3d6E6BEC5A02c2C9fd937ac17A6940002` with\n * different bytecode, predating the deterministic build. Nothing refuses a\n * challenge that names it: the spender is not read off the wire at all, this\n * constant is written into every permit, and a challenge advertising the legacy\n * proxy is signed for this one instead. Substituting silently is the safe\n * direction, since the permit stays worth its ceiling only to the address\n * pinned here, but it is a substitution and not a refusal.\n */\nexport const X402_UPTO_PROXY_ADDRESS = '0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002' as const;\n\n/**\n * The chains the proxy above was verified on, and therefore the only ones an\n * `upto` payment may be signed for.\n *\n * The asset registry is wider than this: it also carries USDC on Polygon and\n * Amoy, and nothing about a deterministic address makes a contract exist on a\n * chain nobody deployed it to. Signing a permit whose spender has no code\n * produces an authorization that can never settle, and by the ledger's own rule\n * a failed attempt reserves its whole ceiling against the cap, so the cost of\n * guessing lands on the user. Allow what was checked, refuse the rest, and widen\n * this when a deployment is confirmed rather than assumed.\n */\nexport const UPTO_VERIFIED_CHAIN_IDS: readonly number[] = [8453, 84532];\n\n/**\n * `WITNESS_TYPE_STRING` from `x402UptoPermit2Proxy.sol`, reproduced verbatim.\n * Permit2 concatenates it onto its own stub to form the full type, so the two\n * halves below must stay exactly as the contracts spell them: the struct order\n * (TokenPermissions before Witness) is the alphabetical order EIP-712 requires,\n * and a single byte out of place produces a signature that no verifier accepts\n * and no error explains.\n */\nconst UPTO_WITNESS_TYPE_STRING =\n 'Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)';\n\n/** Permit2's `_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB`. */\nconst PERMIT_TRANSFER_FROM_WITNESS_STUB =\n 'PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,';\n\n/** The canonical EIP-712 type string for what the payer signs under `upto`. */\nexport const PERMIT2_UPTO_CONTENTS_TYPE = PERMIT_TRANSFER_FROM_WITNESS_STUB + UPTO_WITNESS_TYPE_STRING;\n\n/**\n * The same structs as a viem types object. This is what actually gets signed, on\n * both validation paths: viem derives the canonical type from it, which is what\n * makes the string above a check on this rather than a second copy of it.\n */\nexport const PERMIT_WITNESS_TRANSFER_FROM_TYPES = {\n PermitWitnessTransferFrom: [\n { name: 'permitted', type: 'TokenPermissions' },\n { name: 'spender', type: 'address' },\n { name: 'nonce', type: 'uint256' },\n { name: 'deadline', type: 'uint256' },\n { name: 'witness', type: 'Witness' },\n ],\n TokenPermissions: [\n { name: 'token', type: 'address' },\n { name: 'amount', type: 'uint256' },\n ],\n Witness: [\n { name: 'to', type: 'address' },\n { name: 'facilitator', type: 'address' },\n { name: 'validAfter', type: 'uint256' },\n ],\n} as const;\n\n/** What the payer authorizes: a ceiling, a spender, and the witness binding. */\nexport interface UptoPermitMessage {\n permitted: { token: `0x${string}`; amount: bigint };\n spender: `0x${string}`;\n nonce: bigint;\n deadline: bigint;\n witness: { to: `0x${string}`; facilitator: `0x${string}`; validAfter: bigint };\n}\n\n/**\n * Permit2's EIP-712 domain. It carries no `version`, so passing an empty one\n * would produce a domain separator the contract never computes and a signature\n * it never accepts.\n */\nexport function permit2Domain(chainId: number): { name: string; chainId: number; verifyingContract: `0x${string}` } {\n return { name: 'Permit2', chainId, verifyingContract: PERMIT2_ADDRESS };\n}\n","import { loadSessionKey } from './keystore.js';\nimport { isLegacySession, loadSessionConfig, type SessionConfig } from './session-config.js';\nimport { loadConfig } from './config.js';\nimport { encodeFunctionData, erc20Abi, maxUint256 } from 'viem';\nimport { usdcForNetwork } from '../x402/asset-registry.js';\nimport { PERMIT2_ADDRESS } from '../x402/permit2.js';\n\n// JAW's ERC-20 paymaster, mirrored from core's JAW_PAYMASTER_URL. Kept as a\n// local literal rather than an import because `@jaw.id/core` is lazy-loaded in\n// the CLI (a static import would pull it into startup); keep in sync if core's\n// URL moves. The core SDK recognises this exact base URL and adds the USDC\n// approval the paymaster needs, so the path must match byte for byte.\nconst JAW_ERC20_PAYMASTER_URL = 'https://api.justaname.id/proxy/v1/rpc/erc20-paymaster';\n\n/**\n * The paymaster an auto-mode userOp goes out with, in precedence order: an\n * explicit url, then `config.paymasters` for the chain, then JAW's own.\n *\n * Falling through to JAW's is what makes the default path need no configuration.\n * Without it a fresh setup could not top up at all: the account has to prefund\n * the EntryPoint for the worst-case gas, which fails on an account holding only\n * USDC, and the error names none of this. Asking the user for a paymaster url\n * put the ERC-7677 app-developer role on someone who is just using the wallet,\n * and it asked them to sign up with the provider we already proxy and pay for.\n *\n * It takes its fee in USDC rather than sponsoring anything, so the account never\n * needs a native token, which is the point: gas comes out of the same balance\n * the payments do. `config.paymasters` still wins, so anyone bringing their own\n * keeps it.\n */\nfunction resolvePaymaster(\n options: SessionBridgeOptions\n): Pick<SessionBridgeOptions, 'paymasterUrl' | 'paymasterContext'> {\n if (options.paymasterUrl) {\n return { paymasterUrl: options.paymasterUrl, paymasterContext: options.paymasterContext };\n }\n\n const configured = loadConfig().paymasters?.[options.chainId];\n if (configured) {\n return { paymasterUrl: configured.url, paymasterContext: configured.context };\n }\n\n // No api key means nothing to authenticate the proxy with, so there is no\n // paymaster to engage and the account pays its gas in the native token.\n if (!options.apiKey) return {};\n\n // The ERC-20 paymaster has to be told which token it is being paid in: the SDK\n // sizes and emits the `approve` it needs from this address, and without it the\n // userOp reaches the paymaster with no allowance behind it and cannot settle.\n // A chain the registry does not cover has no token to name, so fall back to\n // sending with no paymaster rather than engaging one that must fail.\n const asset = usdcForNetwork(`eip155:${options.chainId}`);\n if (!asset) {\n // Say so rather than falling through quietly: the userOp goes out with no\n // paymaster, and the failure the user eventually sees is about native funds\n // and mentions none of this. stderr, so stdio MCP framing is untouched.\n console.warn(\n `[jaw] No USDC in the x402 asset registry for chain ${options.chainId}, so no ERC-20 paymaster ` +\n 'can be engaged. Gas will come out of the account\\u2019s native balance. ' +\n 'Set `paymasters` in your config to sponsor this chain.'\n );\n return {};\n }\n\n const url = new URL(JAW_ERC20_PAYMASTER_URL);\n url.searchParams.set('chainId', String(options.chainId));\n url.searchParams.set('api-key', options.apiKey);\n return { paymasterUrl: url.toString(), paymasterContext: { token: asset.address } };\n}\n\n/**\n * The one way the send still breaks once nothing is sponsored: the ERC-20\n * paymaster charges the account the userOp is sent from, and an account with no\n * USDC cannot be charged, so sizing its approval fails. Core's error names the\n * token and the chain and nothing about the account, which is what made this\n * hard to read the first time it happened.\n *\n * A session normally receives its gas in the grant, so an empty one means that\n * transfer did not happen: the wallet that approved the permission does not\n * carry it yet. Say the address and the amount rather than leaving the user to\n * work backwards from a paymaster error.\n */\nfunction explainUnchargeableSender(err: unknown, sessionAddress: string): unknown {\n const message = err instanceof Error ? err.message : String(err);\n if (!message.includes('Could not size the ERC-20 paymaster approval')) return err;\n return new Error(\n `${message}\\n\\nIf ${sessionAddress} holds no USDC, that is why: it pays for its own gas and ` +\n 'cannot be charged with an empty balance. Send it 0.1 USDC, or run `jaw session setup` again.',\n { cause: err }\n );\n}\n\nexport interface SessionBridgeOptions {\n apiKey: string;\n chainId: number;\n paymasterUrl?: string;\n paymasterContext?: Record<string, unknown>;\n}\n\n/** Lazily resolved Account instance + session config */\ninterface InitializedSession {\n account: {\n address: string;\n sendCalls: (...args: unknown[]) => Promise<unknown>;\n getCallStatus: (batchId: `0x${string}`) => Promise<unknown>;\n };\n config: SessionConfig;\n}\n\nexport class SessionBridge {\n private readonly options: SessionBridgeOptions;\n private session: InitializedSession | null = null;\n\n constructor(options: SessionBridgeOptions) {\n this.options = { ...options, ...resolvePaymaster(options) };\n }\n\n private async getSession(): Promise<InitializedSession> {\n if (this.session) {\n this.checkExpiry(this.session.config);\n return this.session;\n }\n\n const config = loadSessionConfig();\n this.checkExpiry(config);\n\n // Sessions from before the CLI settled on EIP-7702 granted their permission\n // to a counterfactual second address, which holds nothing and so cannot be\n // charged the gas of the ops it sends. Re-deriving one of those here would\n // produce a different address and fail at the mismatch guard below, which\n // blames the keystore. Say what it actually is, and how to fix it.\n if (isLegacySession(config)) {\n throw new Error(\n 'This session was created by an older CLI and uses a session address separate from the session key. ' +\n 'Run `jaw session setup` to recreate it, which offers to revoke the old permission first. ' +\n '`jaw session status` still shows the old session, and `jaw session revoke` still revokes it.'\n );\n }\n\n if (config.chainId !== this.options.chainId) {\n throw new Error(\n `Session was created for chain ${config.chainId}, but --chain ${this.options.chainId} was requested. ` +\n `Run \\`jaw session setup --chain ${this.options.chainId}\\` to create a session for that chain.`\n );\n }\n\n let privateKeyHex: string | null = loadSessionKey();\n\n const { privateKeyToAccount } = await import('viem/accounts');\n const localAccount = privateKeyToAccount(privateKeyHex as `0x${string}`);\n privateKeyHex = null;\n\n const { Account } = await import('@jaw.id/core');\n // Every session is EIP-7702, so the account re-derives to the session key\n // EOA and the delegation rides its userOps. Deriving any other way would\n // produce an address the permission was never granted to.\n const account = await Account.fromLocalAccount(\n {\n chainId: this.options.chainId,\n apiKey: this.options.apiKey,\n paymasterUrl: this.options.paymasterUrl,\n paymasterContext: this.options.paymasterContext,\n },\n localAccount,\n { eip7702: true }\n );\n\n // The stored sessionAddress is the on-chain permission's spender. If the\n // key or mode drifted since setup (hand-edited keystore, config from\n // another machine), signing would come from an account the permission was\n // never granted to — fail clearly instead of sending doomed userOps.\n if (account.address.toLowerCase() !== config.sessionAddress.toLowerCase()) {\n throw new Error(\n `Session key derives ${account.address}, but the stored session address is ${config.sessionAddress}. ` +\n 'The keystore and session config are out of sync. Run `jaw session setup` to recreate the session.'\n );\n }\n\n this.session = { account: account as InitializedSession['account'], config };\n return this.session;\n }\n\n private checkExpiry(config: SessionConfig): void {\n if (config.expiry <= Date.now() / 1000) {\n const expiryDate = new Date(config.expiry * 1000).toISOString();\n throw new Error(`Session expired on ${expiryDate}. Run \\`jaw session setup\\` to create a new session.`);\n }\n }\n\n /**\n * Approve Permit2 to move one of the payer's tokens, and return the batch id.\n *\n * The only call this session sends outside its permission, and the only one\n * that can be: `JustaPermissionManager` checks every call's selector against\n * the grant, and the x402 grant permits `transfer` alone, so an approval\n * routed through the permission reverts before anything else happens. Sent by\n * the session on its own balance it never reaches the manager at all, whose\n * approval revocation and Permit2 lockdown act on the granting account and\n * only within their own execution.\n *\n * Being outside the permission is exactly why it is not a general send. It\n * takes a token and nothing else: the spender is Permit2 and the amount is\n * the maximum, neither reachable by a caller, and the token has to be the\n * registry's USDC for this session's chain. There is no shape of argument\n * that turns this into an arbitrary transfer, which matters because an agent\n * reaches the tools that reach this.\n */\n async approvePermit2(token: `0x${string}`): Promise<string> {\n const { account, config } = await this.getSession();\n const usdc = usdcForNetwork(`eip155:${config.chainId}`);\n if (!usdc || token.toLowerCase() !== usdc.address.toLowerCase()) {\n throw new Error(\n `Refusing to approve Permit2 for ${token}: only the registry USDC on chain ${config.chainId} is allowed.`\n );\n }\n\n const data = encodeFunctionData({\n abi: erc20Abi,\n functionName: 'approve',\n args: [PERMIT2_ADDRESS, maxUint256],\n });\n\n try {\n // No permissionId: this is the session acting for itself.\n // Same shape tolerance the funder applies: `{ id, chainId }` from\n // Account.sendCalls, or a bare id from an older bridge.\n const sent: unknown = await account.sendCalls([{ to: usdc.address, data }]);\n const id = typeof sent === 'string' ? sent : (sent as { id?: string } | null)?.id;\n if (!id) throw new Error('approval submitted but no call id was returned');\n return id;\n } catch (err) {\n throw explainUnchargeableSender(err, config.sessionAddress);\n }\n }\n\n async request(method: string, params?: unknown): Promise<unknown> {\n const { account, config } = await this.getSession();\n\n switch (method) {\n case 'eth_requestAccounts':\n case 'eth_accounts':\n return [config.sessionAddress];\n\n case 'wallet_sendCalls': {\n const payload = Array.isArray(params) ? params[0] : params;\n const { calls } = payload as {\n calls: Array<{ to: string; value?: string; data?: string }>;\n };\n const sendOptions = { permissionId: config.permissionId as `0x${string}` };\n\n // Always charged, never sponsored. The grant leaves the session enough\n // to pay for its first op and every refill leaves `gasReserve` behind\n // for the next one, so the sender can be charged.\n try {\n return await account.sendCalls(calls, sendOptions);\n } catch (err) {\n throw explainUnchargeableSender(err, config.sessionAddress);\n }\n }\n\n case 'wallet_getCallsStatus': {\n const batchId = Array.isArray(params) ? params[0] : params;\n return account.getCallStatus(batchId as `0x${string}`);\n }\n\n // Refused rather than absent, so the reason is on screen instead of a\n // caller reading \"not supported in auto mode\" and looking for a flag. See\n // `supportsSessionMode` in rpc-classifier.ts for why.\n case 'personal_sign':\n case 'eth_signTypedData_v4':\n throw new Error(\n `${method} is not available in auto mode: a signature the session makes is not a call, so it never reaches the spend caps or the ledger. Run it through the browser instead.`\n );\n\n case 'wallet_grantPermissions':\n throw new Error('Requires browser — run `jaw session setup`.');\n\n case 'wallet_revokePermissions':\n throw new Error('Requires browser — run `jaw session revoke`.');\n\n default:\n throw new Error(`Method ${method} is not supported in auto mode.`);\n }\n }\n\n close(): void {\n // No-op — no WebSocket to close\n }\n}\n"]}
|