@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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fs4 from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as crypto from 'crypto';
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
|
+
import { parseUnits, formatUnits, erc20Abi, createPublicClient, http } from 'viem';
|
|
8
|
+
import { polygonAmoy, polygon, baseSepolia, base } from 'viem/chains';
|
|
7
9
|
|
|
8
10
|
// src/commands/session/setup.ts
|
|
9
11
|
var JAW_DIR = path.join(os.homedir(), ".jaw");
|
|
@@ -13,13 +15,15 @@ var PATHS = {
|
|
|
13
15
|
session: path.join(JAW_DIR, "session.json"),
|
|
14
16
|
relay: path.join(JAW_DIR, "relay.json"),
|
|
15
17
|
keystore: path.join(JAW_DIR, "keystore.json"),
|
|
16
|
-
sessionConfig: path.join(JAW_DIR, "session-config.json")
|
|
18
|
+
sessionConfig: path.join(JAW_DIR, "session-config.json"),
|
|
19
|
+
x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
|
|
20
|
+
paymentLock: path.join(JAW_DIR, "x402-payment.lock")
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
// src/lib/validation.ts
|
|
20
24
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
21
25
|
var SELECTOR_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
22
|
-
var
|
|
26
|
+
var ALLOWANCE_RE = /^(0x[0-9a-fA-F]+|[0-9]+)$/;
|
|
23
27
|
var VALID_SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
|
|
24
28
|
function parsePermissionsConfig(raw) {
|
|
25
29
|
const errors = [];
|
|
@@ -64,8 +68,8 @@ function parsePermissionsConfig(raw) {
|
|
|
64
68
|
if (typeof s.token !== "string" || !ADDRESS_RE.test(s.token)) {
|
|
65
69
|
errors.push(`spends.${i}.token: Must be a valid 0x address (40 hex chars)`);
|
|
66
70
|
}
|
|
67
|
-
if (typeof s.allowance !== "string" || !
|
|
68
|
-
errors.push(`spends.${i}.allowance: Must be a
|
|
71
|
+
if (typeof s.allowance !== "string" || !ALLOWANCE_RE.test(s.allowance)) {
|
|
72
|
+
errors.push(`spends.${i}.allowance: Must be a decimal or 0x hex integer`);
|
|
69
73
|
}
|
|
70
74
|
if (typeof s.unit !== "string" || !VALID_SPEND_UNITS.has(s.unit)) {
|
|
71
75
|
errors.push(`spends.${i}.unit: Must be one of: ${[...VALID_SPEND_UNITS].join(", ")}`);
|
|
@@ -106,8 +110,8 @@ function isValidRelayUrl(url) {
|
|
|
106
110
|
|
|
107
111
|
// src/lib/config.ts
|
|
108
112
|
function ensureDir(dir) {
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
114
|
+
fs4.chmodSync(dir, 448);
|
|
111
115
|
}
|
|
112
116
|
function migrateConfig(config) {
|
|
113
117
|
if (config.paymasterUrl && !config.paymasters) {
|
|
@@ -119,10 +123,10 @@ function migrateConfig(config) {
|
|
|
119
123
|
return config;
|
|
120
124
|
}
|
|
121
125
|
function loadConfig() {
|
|
122
|
-
if (!
|
|
126
|
+
if (!fs4.existsSync(PATHS.config)) {
|
|
123
127
|
return {};
|
|
124
128
|
}
|
|
125
|
-
const raw =
|
|
129
|
+
const raw = fs4.readFileSync(PATHS.config, "utf-8");
|
|
126
130
|
try {
|
|
127
131
|
const config = JSON.parse(raw);
|
|
128
132
|
return migrateConfig(config);
|
|
@@ -134,7 +138,7 @@ function loadConfig() {
|
|
|
134
138
|
}
|
|
135
139
|
function saveConfig(config) {
|
|
136
140
|
ensureDir(PATHS.root);
|
|
137
|
-
|
|
141
|
+
fs4.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
|
|
138
142
|
encoding: "utf-8",
|
|
139
143
|
mode: 384
|
|
140
144
|
});
|
|
@@ -288,7 +292,18 @@ function bufferToBase64(buf) {
|
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
// src/lib/ws-bridge.ts
|
|
295
|
+
function buildInitPayload(config) {
|
|
296
|
+
return {
|
|
297
|
+
type: "init",
|
|
298
|
+
apiKey: config.apiKey,
|
|
299
|
+
chainId: config.chainId,
|
|
300
|
+
ens: config.ens,
|
|
301
|
+
paymasterUrl: config.paymasterUrl,
|
|
302
|
+
...config.paymasterUrl && config.paymasterContext ? { paymasterContext: config.paymasterContext } : {}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
291
305
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
306
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
292
307
|
var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
|
|
293
308
|
var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
|
|
294
309
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
@@ -297,6 +312,7 @@ var WSBridge = class {
|
|
|
297
312
|
relayUrl;
|
|
298
313
|
session;
|
|
299
314
|
timeout;
|
|
315
|
+
connectTimeout;
|
|
300
316
|
config;
|
|
301
317
|
privateKeyHex;
|
|
302
318
|
publicKeyHex;
|
|
@@ -318,6 +334,7 @@ var WSBridge = class {
|
|
|
318
334
|
this.relayUrl = options.relayUrl;
|
|
319
335
|
this.session = options.session;
|
|
320
336
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
337
|
+
this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
321
338
|
this.config = options.config;
|
|
322
339
|
this.privateKeyHex = options.privateKeyHex;
|
|
323
340
|
this.publicKeyHex = options.publicKeyHex;
|
|
@@ -346,17 +363,16 @@ var WSBridge = class {
|
|
|
346
363
|
let expectingKeyExchange = !this.peerPublicKeyHex;
|
|
347
364
|
const timer = setTimeout(() => {
|
|
348
365
|
ws.close();
|
|
349
|
-
reject(
|
|
350
|
-
|
|
366
|
+
reject(
|
|
367
|
+
new Error(
|
|
368
|
+
`Browser did not connect within ${Math.round(this.connectTimeout / 1e3)}s.
|
|
369
|
+
Run \`jaw disconnect\` then try again, or raise JAW_BRIDGE_TIMEOUT_MS.`
|
|
370
|
+
)
|
|
371
|
+
);
|
|
372
|
+
}, this.connectTimeout);
|
|
351
373
|
const sendEncryptedInit = async () => {
|
|
352
374
|
if (!this.sharedSecret) return;
|
|
353
|
-
const envelope = await encryptMessage(this.sharedSecret,
|
|
354
|
-
type: "init",
|
|
355
|
-
apiKey: this.config.apiKey,
|
|
356
|
-
chainId: this.config.chainId,
|
|
357
|
-
ens: this.config.ens,
|
|
358
|
-
paymasterUrl: this.config.paymasterUrl
|
|
359
|
-
});
|
|
375
|
+
const envelope = await encryptMessage(this.sharedSecret, buildInitPayload(this.config));
|
|
360
376
|
this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
|
|
361
377
|
};
|
|
362
378
|
const waitForReady = () => {
|
|
@@ -604,8 +620,8 @@ function safeParse(data) {
|
|
|
604
620
|
}
|
|
605
621
|
function loadRelaySession() {
|
|
606
622
|
try {
|
|
607
|
-
if (!
|
|
608
|
-
const raw =
|
|
623
|
+
if (!fs4.existsSync(PATHS.relay)) return null;
|
|
624
|
+
const raw = fs4.readFileSync(PATHS.relay, "utf-8");
|
|
609
625
|
const parsed = JSON.parse(raw);
|
|
610
626
|
if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
|
|
611
627
|
return null;
|
|
@@ -617,14 +633,14 @@ function loadRelaySession() {
|
|
|
617
633
|
}
|
|
618
634
|
function saveRelaySession(info) {
|
|
619
635
|
ensureDir(PATHS.root);
|
|
620
|
-
|
|
636
|
+
fs4.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
|
|
621
637
|
encoding: "utf-8",
|
|
622
638
|
mode: 384
|
|
623
639
|
});
|
|
624
640
|
}
|
|
625
641
|
function deleteRelaySession() {
|
|
626
642
|
try {
|
|
627
|
-
if (
|
|
643
|
+
if (fs4.existsSync(PATHS.relay)) fs4.unlinkSync(PATHS.relay);
|
|
628
644
|
} catch {
|
|
629
645
|
}
|
|
630
646
|
}
|
|
@@ -634,6 +650,10 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
|
|
|
634
650
|
var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
|
|
635
651
|
async function getBridge(options) {
|
|
636
652
|
const config = loadConfig();
|
|
653
|
+
const envTimeout = Number(process.env["JAW_BRIDGE_TIMEOUT_MS"]);
|
|
654
|
+
const fromEnv = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0;
|
|
655
|
+
const timeout = options.timeout ?? fromEnv;
|
|
656
|
+
const connectTimeout = options.connectTimeout ?? fromEnv;
|
|
637
657
|
const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
|
|
638
658
|
const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;
|
|
639
659
|
const chainId = options.chainId ?? config.defaultChain ?? 1;
|
|
@@ -646,7 +666,7 @@ async function getBridge(options) {
|
|
|
646
666
|
let relaySession = loadRelaySession();
|
|
647
667
|
if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
|
|
648
668
|
try {
|
|
649
|
-
return await connectBridge(
|
|
669
|
+
return await connectBridge({ ...options, timeout }, relaySession, chainId, keysUrl, relayUrl, false);
|
|
650
670
|
} catch {
|
|
651
671
|
deleteRelaySession();
|
|
652
672
|
relaySession = null;
|
|
@@ -656,7 +676,7 @@ async function getBridge(options) {
|
|
|
656
676
|
}
|
|
657
677
|
const session = await createNewSession(relayUrl);
|
|
658
678
|
saveRelaySession(session);
|
|
659
|
-
return await connectBridge(
|
|
679
|
+
return await connectBridge({ ...options, timeout, connectTimeout }, session, chainId, keysUrl, relayUrl, true);
|
|
660
680
|
}
|
|
661
681
|
async function createNewSession(relayUrl) {
|
|
662
682
|
const kp = await generateKeyPair();
|
|
@@ -671,17 +691,20 @@ async function createNewSession(relayUrl) {
|
|
|
671
691
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
672
692
|
};
|
|
673
693
|
}
|
|
674
|
-
async function connectBridge(
|
|
694
|
+
async function connectBridge(options, relaySession, chainId, keysUrl, relayUrl, openBrowser) {
|
|
675
695
|
const config = loadConfig();
|
|
696
|
+
const paymaster = config.paymasters?.[chainId];
|
|
676
697
|
const bridge = new WSBridge({
|
|
677
698
|
relayUrl,
|
|
678
699
|
session: relaySession.session,
|
|
679
700
|
timeout: options.timeout,
|
|
701
|
+
connectTimeout: options.connectTimeout,
|
|
680
702
|
config: {
|
|
681
703
|
apiKey: options.apiKey,
|
|
682
704
|
chainId,
|
|
683
705
|
ens: options.ens ?? config.ens,
|
|
684
|
-
paymasterUrl:
|
|
706
|
+
paymasterUrl: paymaster?.url,
|
|
707
|
+
paymasterContext: paymaster?.context
|
|
685
708
|
},
|
|
686
709
|
privateKeyHex: relaySession.privateKey,
|
|
687
710
|
publicKeyHex: relaySession.publicKey,
|
|
@@ -691,6 +714,12 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl,
|
|
|
691
714
|
// onBrowserNeeded — only open a browser for new sessions
|
|
692
715
|
openBrowser ? async () => {
|
|
693
716
|
const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
|
|
717
|
+
if (process.env["JAW_NO_BROWSER"]) {
|
|
718
|
+
process.stderr.write(`Open this URL to approve:
|
|
719
|
+
${bridgeUrl}
|
|
720
|
+
`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
694
723
|
const { default: open } = await import('open');
|
|
695
724
|
await open(bridgeUrl);
|
|
696
725
|
} : void 0,
|
|
@@ -721,17 +750,17 @@ function saveKeystore(privateKeyHex, address) {
|
|
|
721
750
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
722
751
|
};
|
|
723
752
|
ensureDir(PATHS.root);
|
|
724
|
-
|
|
753
|
+
fs4.writeFileSync(PATHS.keystore, JSON.stringify(keystore, null, 2) + "\n", {
|
|
725
754
|
encoding: "utf-8",
|
|
726
755
|
mode: 384
|
|
727
756
|
});
|
|
728
|
-
|
|
757
|
+
fs4.chmodSync(PATHS.keystore, 384);
|
|
729
758
|
}
|
|
730
759
|
function loadSessionKey() {
|
|
731
|
-
if (!
|
|
760
|
+
if (!fs4.existsSync(PATHS.keystore)) {
|
|
732
761
|
throw new Error("No session configured. Run `jaw session setup` first.");
|
|
733
762
|
}
|
|
734
|
-
const contents =
|
|
763
|
+
const contents = fs4.readFileSync(PATHS.keystore, "utf-8");
|
|
735
764
|
let parsed;
|
|
736
765
|
try {
|
|
737
766
|
parsed = JSON.parse(contents);
|
|
@@ -740,37 +769,370 @@ function loadSessionKey() {
|
|
|
740
769
|
}
|
|
741
770
|
return parsed.privateKey;
|
|
742
771
|
}
|
|
772
|
+
function tryLoadKeystoreAddress() {
|
|
773
|
+
try {
|
|
774
|
+
if (!fs4.existsSync(PATHS.keystore)) return null;
|
|
775
|
+
const parsed = JSON.parse(fs4.readFileSync(PATHS.keystore, "utf-8"));
|
|
776
|
+
return parsed.address ?? null;
|
|
777
|
+
} catch {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
743
781
|
function keystoreExists() {
|
|
744
|
-
return
|
|
782
|
+
return fs4.existsSync(PATHS.keystore);
|
|
783
|
+
}
|
|
784
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
785
|
+
var SELECTOR_RE2 = /^0x[0-9a-fA-F]{8}$/;
|
|
786
|
+
var HEX_RE = /^0x[0-9a-fA-F]+$/;
|
|
787
|
+
var ALLOWANCE_RE2 = /^(0x[0-9a-fA-F]+|[0-9]+)$/;
|
|
788
|
+
var SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
|
|
789
|
+
function isPositiveInt(value) {
|
|
790
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
791
|
+
}
|
|
792
|
+
function parseGrantedPermission(raw) {
|
|
793
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
794
|
+
const r = raw;
|
|
795
|
+
const { account, spender, salt } = r;
|
|
796
|
+
if (typeof account !== "string" || !ADDRESS_RE2.test(account)) return void 0;
|
|
797
|
+
if (typeof spender !== "string" || !ADDRESS_RE2.test(spender)) return void 0;
|
|
798
|
+
if (typeof salt !== "string" || !HEX_RE.test(salt)) return void 0;
|
|
799
|
+
if (!isPositiveInt(r.start) || !isPositiveInt(r.end)) return void 0;
|
|
800
|
+
if (!Array.isArray(r.calls) || r.calls.length === 0) return void 0;
|
|
801
|
+
const calls = [];
|
|
802
|
+
for (const entry of r.calls) {
|
|
803
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
804
|
+
const { target, selector } = entry;
|
|
805
|
+
if (typeof target !== "string" || !ADDRESS_RE2.test(target)) return void 0;
|
|
806
|
+
if (typeof selector !== "string" || !SELECTOR_RE2.test(selector)) return void 0;
|
|
807
|
+
calls.push({ target, selector });
|
|
808
|
+
}
|
|
809
|
+
if (!Array.isArray(r.spends)) return void 0;
|
|
810
|
+
const spends = [];
|
|
811
|
+
for (const entry of r.spends) {
|
|
812
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
813
|
+
const { token, allowance, unit, multiplier } = entry;
|
|
814
|
+
if (typeof token !== "string" || !ADDRESS_RE2.test(token)) return void 0;
|
|
815
|
+
if (typeof allowance !== "string" || !ALLOWANCE_RE2.test(allowance)) return void 0;
|
|
816
|
+
if (typeof unit !== "string" || !SPEND_UNITS.has(unit)) return void 0;
|
|
817
|
+
if (!isPositiveInt(multiplier) || multiplier > 65535) return void 0;
|
|
818
|
+
spends.push({ token, allowance, unit, multiplier });
|
|
819
|
+
}
|
|
820
|
+
return { account, spender, start: r.start, end: r.end, salt, calls, spends };
|
|
821
|
+
}
|
|
822
|
+
function liveOrphans(orphans, now = Date.now() / 1e3) {
|
|
823
|
+
return (orphans ?? []).filter((orphan) => orphan.expiry > now);
|
|
745
824
|
}
|
|
746
825
|
function saveSessionConfig(input) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
};
|
|
826
|
+
writeSessionConfig({ ...input, createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() });
|
|
827
|
+
}
|
|
828
|
+
function writeSessionConfig(config) {
|
|
751
829
|
ensureDir(PATHS.root);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
fs.chmodSync(PATHS.sessionConfig, 384);
|
|
830
|
+
const temp = `${PATHS.sessionConfig}.${process.pid}.tmp`;
|
|
831
|
+
fs4.writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
832
|
+
fs4.chmodSync(temp, 384);
|
|
833
|
+
fs4.renameSync(temp, PATHS.sessionConfig);
|
|
757
834
|
}
|
|
758
835
|
function loadSessionConfig() {
|
|
759
|
-
if (!
|
|
836
|
+
if (!fs4.existsSync(PATHS.sessionConfig)) {
|
|
760
837
|
throw new Error("No session configured. Run `jaw session setup` first.");
|
|
761
838
|
}
|
|
762
|
-
const raw =
|
|
839
|
+
const raw = fs4.readFileSync(PATHS.sessionConfig, "utf-8");
|
|
763
840
|
try {
|
|
764
841
|
return JSON.parse(raw);
|
|
765
842
|
} catch {
|
|
766
843
|
throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
|
|
767
844
|
}
|
|
768
845
|
}
|
|
846
|
+
function tryLoadSessionConfig() {
|
|
847
|
+
try {
|
|
848
|
+
return loadSessionConfig();
|
|
849
|
+
} catch {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/x402/asset-registry.ts
|
|
855
|
+
var USDC_BY_NETWORK = {
|
|
856
|
+
"eip155:8453": {
|
|
857
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
858
|
+
chainId: 8453,
|
|
859
|
+
wireNetwork: "eip155:8453",
|
|
860
|
+
usdcName: "USD Coin",
|
|
861
|
+
usdcVersion: "2",
|
|
862
|
+
decimals: 6
|
|
863
|
+
},
|
|
864
|
+
"eip155:84532": {
|
|
865
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
866
|
+
chainId: 84532,
|
|
867
|
+
wireNetwork: "eip155:84532",
|
|
868
|
+
usdcName: "USDC",
|
|
869
|
+
usdcVersion: "2",
|
|
870
|
+
decimals: 6
|
|
871
|
+
},
|
|
872
|
+
"eip155:137": {
|
|
873
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
874
|
+
chainId: 137,
|
|
875
|
+
wireNetwork: "eip155:137",
|
|
876
|
+
usdcName: "USD Coin",
|
|
877
|
+
usdcVersion: "2",
|
|
878
|
+
decimals: 6
|
|
879
|
+
},
|
|
880
|
+
"eip155:80002": {
|
|
881
|
+
address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
|
|
882
|
+
chainId: 80002,
|
|
883
|
+
wireNetwork: "eip155:80002",
|
|
884
|
+
usdcName: "USDC",
|
|
885
|
+
usdcVersion: "2",
|
|
886
|
+
decimals: 6
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
function usdcForNetwork(network) {
|
|
890
|
+
return Object.hasOwn(USDC_BY_NETWORK, network) ? USDC_BY_NETWORK[network] : void 0;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/x402/grant-preset.ts
|
|
894
|
+
var TRANSFER_SIGNATURE = "transfer(address,uint256)";
|
|
895
|
+
var MAX_ALLOWANCE = 2n ** 160n - 1n;
|
|
896
|
+
var LIMIT_PERIODS = ["minute", "hour", "day", "week", "month", "year", "forever"];
|
|
897
|
+
var DEFAULT_X402_LIMIT = "10/day";
|
|
898
|
+
function parseLimit(input) {
|
|
899
|
+
const trimmed = input.trim();
|
|
900
|
+
if (!trimmed) throw new Error("Limit is empty. Use --limit <amount>/<period>, for example 10/day.");
|
|
901
|
+
const [rawAmount, rawPeriod = "day", ...rest] = trimmed.split("/");
|
|
902
|
+
if (rest.length > 0) {
|
|
903
|
+
throw new Error(`Invalid limit "${input}". Expected <amount>/<period>, for example 10/day.`);
|
|
904
|
+
}
|
|
905
|
+
const amount = rawAmount.trim();
|
|
906
|
+
if (!/^\d+(\.\d+)?$/.test(amount)) {
|
|
907
|
+
throw new Error(`Invalid limit amount "${rawAmount}". Expected a positive number, for example 10 or 2.5.`);
|
|
908
|
+
}
|
|
909
|
+
const period = rawPeriod.trim().toLowerCase();
|
|
910
|
+
if (!LIMIT_PERIODS.includes(period)) {
|
|
911
|
+
throw new Error(`Invalid limit period "${rawPeriod}". Expected one of: ${LIMIT_PERIODS.join(", ")}.`);
|
|
912
|
+
}
|
|
913
|
+
return { amount, period };
|
|
914
|
+
}
|
|
915
|
+
function buildX402Permissions(chainId, limit = DEFAULT_X402_LIMIT) {
|
|
916
|
+
const usdc = Object.values(USDC_BY_NETWORK).find((asset) => asset.chainId === chainId);
|
|
917
|
+
if (!usdc) {
|
|
918
|
+
const supported = Object.values(USDC_BY_NETWORK).map((a) => a.chainId).sort((a, b) => a - b).join(", ");
|
|
919
|
+
throw new Error(`No USDC configured for chain ${chainId}. x402 payments are supported on: ${supported}.`);
|
|
920
|
+
}
|
|
921
|
+
const { amount, period } = parseLimit(limit);
|
|
922
|
+
let allowance;
|
|
923
|
+
try {
|
|
924
|
+
allowance = parseUnits(amount, usdc.decimals);
|
|
925
|
+
} catch {
|
|
926
|
+
throw new Error(`Invalid limit amount "${amount}" for a token with ${usdc.decimals} decimals.`);
|
|
927
|
+
}
|
|
928
|
+
if (allowance <= 0n) {
|
|
929
|
+
throw new Error(`Limit "${limit}" resolves to zero, which would refuse every payment.`);
|
|
930
|
+
}
|
|
931
|
+
if (allowance > MAX_ALLOWANCE) {
|
|
932
|
+
throw new Error(`Limit "${limit}" is larger than a spend allowance can hold (max ${MAX_ALLOWANCE}).`);
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
calls: [{ target: usdc.address, functionSignature: TRANSFER_SIGNATURE }],
|
|
936
|
+
spends: [{ token: usdc.address, allowance: allowance.toString(), unit: period, multiplier: 1 }]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function describeX402Grant(limit = DEFAULT_X402_LIMIT) {
|
|
940
|
+
const { amount, period } = parseLimit(limit);
|
|
941
|
+
const per = period === "forever" ? "in total" : `per ${period}`;
|
|
942
|
+
return `${amount} USDC ${per}, transfers only`;
|
|
943
|
+
}
|
|
944
|
+
var JAW_RPC_URL = "https://api.justaname.id/proxy/v1/rpc";
|
|
945
|
+
var CHAINS = {
|
|
946
|
+
[base.id]: base,
|
|
947
|
+
[baseSepolia.id]: baseSepolia,
|
|
948
|
+
[polygon.id]: polygon,
|
|
949
|
+
[polygonAmoy.id]: polygonAmoy
|
|
950
|
+
};
|
|
951
|
+
for (const chainId of Object.values(USDC_BY_NETWORK).map((a) => a.chainId)) {
|
|
952
|
+
if (!CHAINS[chainId]) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`x402 balance: USDC registry has chain ${chainId} but no viem chain is mapped for it in balance.ts`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
var clients = /* @__PURE__ */ new Map();
|
|
959
|
+
function rpcTransport(chainId, apiKey) {
|
|
960
|
+
if (!apiKey) return http();
|
|
961
|
+
return http(`${JAW_RPC_URL}?chainId=${chainId}&api-key=${apiKey}`);
|
|
962
|
+
}
|
|
963
|
+
function publicClientFor(chainId) {
|
|
964
|
+
const chain = CHAINS[chainId];
|
|
965
|
+
if (!chain) throw new Error(`x402: no viem chain configured for chainId ${chainId}`);
|
|
966
|
+
const apiKey = loadConfig().apiKey;
|
|
967
|
+
const key = `${chainId}:${apiKey ?? ""}`;
|
|
968
|
+
let client = clients.get(key);
|
|
969
|
+
if (!client) {
|
|
970
|
+
client = createPublicClient({ chain, transport: rpcTransport(chainId, apiKey) });
|
|
971
|
+
clients.set(key, client);
|
|
972
|
+
}
|
|
973
|
+
return client;
|
|
974
|
+
}
|
|
975
|
+
var readOnChain = (asset, owner) => publicClientFor(asset.chainId).readContract({
|
|
976
|
+
address: asset.address,
|
|
977
|
+
abi: erc20Abi,
|
|
978
|
+
functionName: "balanceOf",
|
|
979
|
+
args: [owner]
|
|
980
|
+
});
|
|
981
|
+
async function usdcBalance(network, owner, read = readOnChain) {
|
|
982
|
+
const asset = usdcForNetwork(network);
|
|
983
|
+
if (!asset) throw new Error(`Unsupported x402 network: ${network}`);
|
|
984
|
+
const raw = await read(asset, owner);
|
|
985
|
+
return { network, asset: asset.address, raw: raw.toString(), formatted: formatUnits(raw, asset.decimals) };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/x402/gas-reserve.ts
|
|
989
|
+
function gasReserve(asset) {
|
|
990
|
+
return 10n ** BigInt(asset.decimals) / 10n;
|
|
991
|
+
}
|
|
992
|
+
function firstOperationCost(asset) {
|
|
993
|
+
return 10n ** BigInt(asset.decimals) / 100n;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/x402/amount.ts
|
|
997
|
+
function parseBigInt(value) {
|
|
998
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
999
|
+
try {
|
|
1000
|
+
return BigInt(value);
|
|
1001
|
+
} catch {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/lib/terminal.ts
|
|
1007
|
+
var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
|
|
1008
|
+
var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
1009
|
+
var REPLACEMENT = "\uFFFD";
|
|
1010
|
+
var DEFAULT_LINE_LENGTH = 200;
|
|
1011
|
+
function bound(text, maxLength) {
|
|
1012
|
+
if (text.length <= maxLength) return text;
|
|
1013
|
+
return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
|
|
1014
|
+
}
|
|
1015
|
+
function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
|
|
1016
|
+
const text = typeof value === "string" ? value : String(value);
|
|
1017
|
+
return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/x402/status-report.ts
|
|
1021
|
+
function formatUsdc(base2, decimals) {
|
|
1022
|
+
if (base2 === void 0) return "unlimited";
|
|
1023
|
+
const value = parseBigInt(base2);
|
|
1024
|
+
if (value === null) return `${sanitizeLine(base2, 32)} (invalid)`;
|
|
1025
|
+
const scale = 10n ** BigInt(decimals);
|
|
1026
|
+
const whole = value / scale;
|
|
1027
|
+
const frac = (value % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
1028
|
+
return `${whole}${frac ? `.${frac}` : ""} USDC`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/x402/funded-owner.ts
|
|
1032
|
+
async function whyOwnerCannotFundSession(check) {
|
|
1033
|
+
const asset = usdcForNetwork(`eip155:${check.chainId}`);
|
|
1034
|
+
if (!asset) return null;
|
|
1035
|
+
const accounts = await check.request("eth_requestAccounts");
|
|
1036
|
+
const owner = accounts?.[0];
|
|
1037
|
+
if (!owner) return null;
|
|
1038
|
+
const read = check.readBalance ?? (async (network, address) => BigInt((await usdcBalance(network, address)).raw));
|
|
1039
|
+
let held;
|
|
1040
|
+
try {
|
|
1041
|
+
held = await read(asset.wireNetwork, owner);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
const needed = gasReserve(asset);
|
|
1046
|
+
if (held >= needed) return null;
|
|
1047
|
+
return `${owner} holds ${formatUsdc(held.toString(), asset.decimals)} on chain ${check.chainId}, and setting up a session needs at least ${formatUsdc(needed.toString(), asset.decimals)} there. That much rides along in the grant so the session can pay for its own first transaction. Fund the account and run this again.`;
|
|
1048
|
+
}
|
|
1049
|
+
async function whySpenderCannotPay(check) {
|
|
1050
|
+
const asset = usdcForNetwork(`eip155:${check.chainId}`);
|
|
1051
|
+
if (!asset) return null;
|
|
1052
|
+
const read = check.readBalance ?? (async (network, address) => BigInt((await usdcBalance(network, address)).raw));
|
|
1053
|
+
let held;
|
|
1054
|
+
let timer;
|
|
1055
|
+
try {
|
|
1056
|
+
const expired = new Promise((_, reject) => {
|
|
1057
|
+
timer = setTimeout(() => reject(new Error("timed out")), check.timeoutMs ?? 5e3);
|
|
1058
|
+
});
|
|
1059
|
+
held = await Promise.race([read(asset.wireNetwork, check.spender), expired]);
|
|
1060
|
+
} catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
} finally {
|
|
1063
|
+
clearTimeout(timer);
|
|
1064
|
+
}
|
|
1065
|
+
const needed = firstOperationCost(asset);
|
|
1066
|
+
if (held >= needed) return null;
|
|
1067
|
+
return `${check.spender} holds ${formatUsdc(held.toString(), asset.decimals)} on chain ${check.chainId}, which is not enough to pay for its first operation. The session pays its own gas, and the grant asked this wallet to send it a little along with the permission. Nothing usable arrived: the wallet may not implement that yet, or it could not price the transfer, or the granted allowance was too small to cover one. Send ${formatUsdc(needed.toString(), asset.decimals)} to that address and the session works from there.`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/x402/period.ts
|
|
1071
|
+
function describeSpendPeriod(unit, multiplier) {
|
|
1072
|
+
const n = Math.max(1, Math.floor(multiplier ?? 1));
|
|
1073
|
+
return n === 1 ? unit : `${n} ${unit}s`;
|
|
1074
|
+
}
|
|
1075
|
+
function periodLengthSeconds(unit, multiplier, bound2) {
|
|
1076
|
+
const lengths = {
|
|
1077
|
+
minute: 60,
|
|
1078
|
+
hour: 3600,
|
|
1079
|
+
day: 86400,
|
|
1080
|
+
week: 604800,
|
|
1081
|
+
month: (bound2 === "min" ? 28 : 31) * 86400,
|
|
1082
|
+
year: (bound2 === "min" ? 365 : 366) * 86400,
|
|
1083
|
+
forever: Number.POSITIVE_INFINITY
|
|
1084
|
+
};
|
|
1085
|
+
if (!Object.hasOwn(lengths, unit)) return null;
|
|
1086
|
+
return lengths[unit] * Math.max(1, Math.floor(multiplier));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/x402/grant-ceiling.ts
|
|
1090
|
+
function whyGrantExceedsCeiling(permissions, chainId, ceiling) {
|
|
1091
|
+
if (!ceiling) return null;
|
|
1092
|
+
let parsed;
|
|
1093
|
+
try {
|
|
1094
|
+
parsed = parseLimit(ceiling);
|
|
1095
|
+
} catch {
|
|
1096
|
+
return `The grant ceiling in your config is not a valid limit: ${ceiling}. Fix it with \`jaw config set grantCeiling <amount>/<period>\`, or remove it.`;
|
|
1097
|
+
}
|
|
1098
|
+
const usdc = Object.values(USDC_BY_NETWORK).find((asset) => asset.chainId === chainId);
|
|
1099
|
+
if (!usdc) {
|
|
1100
|
+
return (permissions.spends ?? []).length > 0 ? `The grant ceiling is set to ${ceiling}, and chain ${chainId} has no USDC in the registry to measure a spend against it. Grant on a supported chain, or remove the ceiling with \`jaw config set grantCeiling ""\`.` : null;
|
|
1101
|
+
}
|
|
1102
|
+
const maxAllowance = parseUnits(parsed.amount, usdc.decimals);
|
|
1103
|
+
const ceilingSeconds = periodLengthSeconds(parsed.period, 1, "max");
|
|
1104
|
+
if (ceilingSeconds === null) return null;
|
|
1105
|
+
for (const spend of permissions.spends ?? []) {
|
|
1106
|
+
if (spend.token.toLowerCase() !== usdc.address.toLowerCase()) {
|
|
1107
|
+
return `The grant ceiling is set to ${ceiling}, and this permission spends ${spend.token}, which cannot be measured against it. Grant USDC, or remove the ceiling with \`jaw config set grantCeiling ""\`.`;
|
|
1108
|
+
}
|
|
1109
|
+
let allowance;
|
|
1110
|
+
try {
|
|
1111
|
+
allowance = BigInt(spend.allowance);
|
|
1112
|
+
} catch {
|
|
1113
|
+
return `This permission asks for an allowance that cannot be read: ${spend.allowance}.`;
|
|
1114
|
+
}
|
|
1115
|
+
if (allowance > maxAllowance) {
|
|
1116
|
+
return `This grant asks for ${formatUsdc(allowance.toString(), usdc.decimals)} per period, over the ${ceiling} ceiling set on this machine. Lower it, or raise the ceiling with \`jaw config set grantCeiling <amount>/<period>\`.`;
|
|
1117
|
+
}
|
|
1118
|
+
const grantSeconds = periodLengthSeconds(spend.unit, spend.multiplier ?? 1, "min");
|
|
1119
|
+
if (grantSeconds === null) {
|
|
1120
|
+
return `This permission uses a spend period this CLI does not recognise: ${spend.unit}.`;
|
|
1121
|
+
}
|
|
1122
|
+
const sameUnit = spend.unit === parsed.period;
|
|
1123
|
+
if (!sameUnit && grantSeconds < ceilingSeconds) {
|
|
1124
|
+
return `This grant resets its allowance every ${describeSpendPeriod(spend.unit, spend.multiplier ?? 1)}, which is more often than the ${ceiling} ceiling set on this machine allows. A shorter period is more money over the same time, even at the same allowance.`;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
769
1129
|
|
|
770
1130
|
// src/commands/session/setup.ts
|
|
771
1131
|
var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
772
1132
|
static description = "Generate a session key and grant scoped on-chain permissions (one-time browser approval).";
|
|
773
1133
|
static examples = [
|
|
1134
|
+
"<%= config.bin %> session setup --chain 8453 --x402",
|
|
1135
|
+
"<%= config.bin %> session setup --chain 8453 --x402 --limit 25/day --expiry 14",
|
|
774
1136
|
"<%= config.bin %> session setup --chain 84532",
|
|
775
1137
|
`<%= config.bin %> session setup --permissions '{"calls":[...]}' --expiry 14`,
|
|
776
1138
|
"<%= config.bin %> session setup --permissions ./permissions.json"
|
|
@@ -778,7 +1140,18 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
778
1140
|
static flags = {
|
|
779
1141
|
...BaseCommand.baseFlags,
|
|
780
1142
|
permissions: Flags.string({
|
|
781
|
-
description: "Permission scope (inline JSON or file path). Overrides config.permissions."
|
|
1143
|
+
description: "Permission scope (inline JSON or file path). Overrides config.permissions.",
|
|
1144
|
+
exclusive: ["x402"]
|
|
1145
|
+
}),
|
|
1146
|
+
x402: Flags.boolean({
|
|
1147
|
+
description: "Grant exactly what x402 payments need on this chain: a USDC transfer capped per period. Builds the permission from the asset registry so the USDC address and function signature do not have to be written by hand. Tune the cap with --limit.",
|
|
1148
|
+
default: false,
|
|
1149
|
+
exclusive: ["permissions"]
|
|
1150
|
+
}),
|
|
1151
|
+
limit: Flags.string({
|
|
1152
|
+
description: `Spend cap for --x402, as <amount>/<period> (default ${DEFAULT_X402_LIMIT}). Examples: 25/day, 2.5/week, 100/month.`
|
|
1153
|
+
// No `dependsOn: ['x402']`: a boolean flag with a default always reads as
|
|
1154
|
+
// provided, so oclif would never fire it. Checked in run() instead.
|
|
782
1155
|
}),
|
|
783
1156
|
expiry: Flags.integer({
|
|
784
1157
|
description: "Permission expiry in days. Overrides config.sessionExpiry."
|
|
@@ -790,21 +1163,46 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
790
1163
|
const format = flags.output;
|
|
791
1164
|
const apiKey = this.resolveApiKey(flags);
|
|
792
1165
|
const chainId = this.resolveChainId(flags);
|
|
1166
|
+
if (flags.limit && !flags.x402) {
|
|
1167
|
+
this.error("--limit only applies to --x402. Re-run with --x402, or set the cap inside --permissions.");
|
|
1168
|
+
}
|
|
1169
|
+
const resolvedPermissions = this.resolvePermissions(flags.permissions, config.permissions, {
|
|
1170
|
+
x402: flags.x402,
|
|
1171
|
+
limit: flags.limit,
|
|
1172
|
+
chainId
|
|
1173
|
+
});
|
|
1174
|
+
const overCeiling = whyGrantExceedsCeiling(resolvedPermissions, chainId, config.grantCeiling);
|
|
1175
|
+
if (overCeiling) this.error(overCeiling);
|
|
793
1176
|
let reuseKey = null;
|
|
794
1177
|
let oldPermissionRevoked = false;
|
|
1178
|
+
let orphaned = [];
|
|
795
1179
|
if (keystoreExists()) {
|
|
796
|
-
const existing =
|
|
797
|
-
const isActive = existing.expiry > Date.now() / 1e3;
|
|
1180
|
+
const existing = tryLoadSessionConfig();
|
|
1181
|
+
const isActive = existing !== null && existing.expiry > Date.now() / 1e3;
|
|
1182
|
+
orphaned = liveOrphans(existing?.orphanedPermissions);
|
|
798
1183
|
if (!flags.yes && !process.stdin.isTTY) {
|
|
799
1184
|
this.error(
|
|
800
|
-
"Existing session found, but stdin is not a terminal (piped, redirected, or running in CI). Re-run with --yes to overwrite the existing session non-interactively."
|
|
1185
|
+
"Existing session key found, but stdin is not a terminal (piped, redirected, or running in CI). Re-run with --yes to overwrite the existing session non-interactively."
|
|
801
1186
|
);
|
|
802
1187
|
}
|
|
803
1188
|
if (!flags.yes) {
|
|
804
1189
|
const readline = await import('readline');
|
|
805
1190
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
806
1191
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
807
|
-
if (
|
|
1192
|
+
if (!existing) {
|
|
1193
|
+
const orphanAddress = tryLoadKeystoreAddress();
|
|
1194
|
+
this.log("Session key found, but no session config alongside it.\n");
|
|
1195
|
+
if (orphanAddress) {
|
|
1196
|
+
this.log(` Key address: ${orphanAddress}`);
|
|
1197
|
+
}
|
|
1198
|
+
this.log(
|
|
1199
|
+
"\nIf that key still holds a live on-chain permission it cannot be revoked\nautomatically, because the permission id lived in the missing config.\nReusing the key keeps a single key in play instead of leaving two.\n"
|
|
1200
|
+
);
|
|
1201
|
+
const reuseAnswer = await ask("Reuse existing session key? (Y/n) ");
|
|
1202
|
+
if (reuseAnswer.toLowerCase() !== "n") {
|
|
1203
|
+
reuseKey = loadSessionKey();
|
|
1204
|
+
}
|
|
1205
|
+
} else if (isActive) {
|
|
808
1206
|
const remaining = Math.floor((existing.expiry - Date.now() / 1e3) / 86400);
|
|
809
1207
|
this.log("Active session found:\n");
|
|
810
1208
|
this.log(` Session address: ${existing.sessionAddress}`);
|
|
@@ -818,13 +1216,11 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
818
1216
|
const revokeAnswer = await ask("Revoke old permission on-chain first? (Y/n) ");
|
|
819
1217
|
if (revokeAnswer.toLowerCase() !== "n") {
|
|
820
1218
|
this.log("Opening browser to revoke old permission...");
|
|
821
|
-
const pm = config.paymasters?.[existing.chainId];
|
|
822
1219
|
const revokeBridge = await getBridge({
|
|
823
1220
|
keysUrl: config.keysUrl,
|
|
824
1221
|
apiKey,
|
|
825
1222
|
chainId: existing.chainId,
|
|
826
|
-
ens: config.ens
|
|
827
|
-
paymasterUrl: pm?.url
|
|
1223
|
+
ens: config.ens
|
|
828
1224
|
});
|
|
829
1225
|
try {
|
|
830
1226
|
await revokeBridge.request("wallet_revokePermissions", [{ id: existing.permissionId }]);
|
|
@@ -833,6 +1229,11 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
833
1229
|
}
|
|
834
1230
|
oldPermissionRevoked = true;
|
|
835
1231
|
this.log("Old permission revoked.");
|
|
1232
|
+
} else {
|
|
1233
|
+
orphaned = [orphanOf(existing), ...orphaned];
|
|
1234
|
+
this.log(
|
|
1235
|
+
"Keeping the old permission. It stays live until it expires, and the new session will not name it, so `jaw session revoke` will revoke both."
|
|
1236
|
+
);
|
|
836
1237
|
}
|
|
837
1238
|
const reuseAnswer = await ask("Reuse existing session key? (Y/n) ");
|
|
838
1239
|
if (reuseAnswer.toLowerCase() !== "n") {
|
|
@@ -847,14 +1248,20 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
847
1248
|
}
|
|
848
1249
|
}
|
|
849
1250
|
rl.close();
|
|
1251
|
+
} else if (!existing) {
|
|
1252
|
+
const orphanAddress = tryLoadKeystoreAddress();
|
|
1253
|
+
this.logToStderr(
|
|
1254
|
+
`Warning: session key${orphanAddress ? ` ${orphanAddress}` : ""} has no session config. Generating a new key; any permission the old one still holds cannot be revoked automatically because the permission id is unknown.`
|
|
1255
|
+
);
|
|
850
1256
|
} else if (isActive) {
|
|
1257
|
+
orphaned = [orphanOf(existing), ...orphaned];
|
|
851
1258
|
this.logToStderr(
|
|
852
|
-
`Warning: overwriting active session without revoking. Old permission ${existing.permissionId} on chain ${existing.chainId} remains live until ${new Date(existing.expiry * 1e3).toISOString()}.`
|
|
1259
|
+
`Warning: overwriting active session without revoking. Old permission ${existing.permissionId} on chain ${existing.chainId} remains live until ${new Date(existing.expiry * 1e3).toISOString()}. Recorded on the new session, so \`jaw session revoke\` will revoke it too.`
|
|
853
1260
|
);
|
|
854
1261
|
}
|
|
855
1262
|
}
|
|
856
1263
|
try {
|
|
857
|
-
const permissions =
|
|
1264
|
+
const permissions = resolvedPermissions;
|
|
858
1265
|
const expiryDays = flags.expiry ?? config.sessionExpiry ?? 7;
|
|
859
1266
|
const expiryTimestamp = Math.floor(Date.now() / 1e3) + expiryDays * 86400;
|
|
860
1267
|
const privateKeyHex = reuseKey ?? generateSessionKey();
|
|
@@ -862,6 +1269,7 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
862
1269
|
const localAccount = privateKeyToAccount(privateKeyHex);
|
|
863
1270
|
const { Account } = await import('@jaw.id/core');
|
|
864
1271
|
const pm = config.paymasters?.[chainId];
|
|
1272
|
+
const mode = "eip7702";
|
|
865
1273
|
const account = await Account.fromLocalAccount(
|
|
866
1274
|
{
|
|
867
1275
|
chainId,
|
|
@@ -869,52 +1277,84 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
869
1277
|
paymasterUrl: pm?.url,
|
|
870
1278
|
paymasterContext: pm?.context
|
|
871
1279
|
},
|
|
872
|
-
localAccount
|
|
1280
|
+
localAccount,
|
|
1281
|
+
{ eip7702: true }
|
|
873
1282
|
);
|
|
874
1283
|
const sessionAddress = account.address;
|
|
875
1284
|
if (!flags.quiet) {
|
|
1285
|
+
if (flags.x402) {
|
|
1286
|
+
this.log(
|
|
1287
|
+
`Connect with an account that holds USDC on chain ${chainId}.
|
|
1288
|
+
Payments pull from it through the permission, and the grant carries 0.1 USDC
|
|
1289
|
+
to the session so it can pay for its own first transaction.
|
|
1290
|
+
`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
876
1293
|
this.log("Opening browser to approve permissions...");
|
|
877
1294
|
}
|
|
878
1295
|
const bridge = await getBridge({
|
|
879
1296
|
keysUrl: config.keysUrl,
|
|
880
1297
|
apiKey,
|
|
881
1298
|
chainId,
|
|
882
|
-
ens: config.ens
|
|
883
|
-
paymasterUrl: pm?.url
|
|
1299
|
+
ens: config.ens
|
|
884
1300
|
});
|
|
885
|
-
let
|
|
1301
|
+
let granted;
|
|
886
1302
|
try {
|
|
887
|
-
|
|
1303
|
+
if (flags.x402) {
|
|
1304
|
+
const blocked = await whyOwnerCannotFundSession({ chainId, request: (m, p) => bridge.request(m, p) });
|
|
1305
|
+
if (blocked) this.error(blocked);
|
|
1306
|
+
}
|
|
1307
|
+
granted = await bridge.request("wallet_grantPermissions", [
|
|
888
1308
|
{
|
|
889
1309
|
spender: sessionAddress,
|
|
890
1310
|
expiry: expiryTimestamp,
|
|
891
1311
|
permissions,
|
|
892
|
-
chainId
|
|
1312
|
+
chainId,
|
|
1313
|
+
// The session account sends every op the permission authorises, and
|
|
1314
|
+
// the ERC-20 paymaster charges the sender, so its first one has
|
|
1315
|
+
// nothing to be charged. The wallet rides a small transfer along in
|
|
1316
|
+
// this same transaction; it decides the amount.
|
|
1317
|
+
capabilities: { prefundSpender: true }
|
|
893
1318
|
}
|
|
894
1319
|
]);
|
|
895
1320
|
} finally {
|
|
896
1321
|
bridge.close();
|
|
897
1322
|
}
|
|
1323
|
+
const grantResponse = granted;
|
|
1324
|
+
const permission = parseGrantedPermission(granted);
|
|
898
1325
|
saveKeystore(privateKeyHex, sessionAddress);
|
|
899
1326
|
saveSessionConfig({
|
|
900
1327
|
ownerAddress: grantResponse.account,
|
|
901
1328
|
sessionAddress,
|
|
902
1329
|
permissionId: grantResponse.permissionId,
|
|
903
1330
|
chainId,
|
|
904
|
-
expiry: expiryTimestamp
|
|
1331
|
+
expiry: expiryTimestamp,
|
|
1332
|
+
mode,
|
|
1333
|
+
...permission ? { permission } : {},
|
|
1334
|
+
...orphaned.length > 0 ? { orphanedPermissions: orphaned } : {}
|
|
905
1335
|
});
|
|
1336
|
+
if (flags.x402) {
|
|
1337
|
+
const unfunded = await whySpenderCannotPay({ chainId, spender: sessionAddress });
|
|
1338
|
+
if (unfunded) this.logToStderr(`
|
|
1339
|
+
Warning: ${unfunded}`);
|
|
1340
|
+
}
|
|
906
1341
|
const summary = {
|
|
907
1342
|
ownerAddress: grantResponse.account,
|
|
908
1343
|
sessionAddress,
|
|
909
1344
|
permissionId: grantResponse.permissionId,
|
|
910
|
-
expiry: expiryTimestamp
|
|
1345
|
+
expiry: expiryTimestamp,
|
|
1346
|
+
mode
|
|
911
1347
|
};
|
|
912
1348
|
if (flags.quiet) {
|
|
913
1349
|
this.outputResult(summary, format);
|
|
914
1350
|
} else {
|
|
915
1351
|
this.log("\nSession created successfully.\n");
|
|
916
1352
|
this.log(` Session address: ${sessionAddress}`);
|
|
1353
|
+
this.log(" (the session key EOA, and the x402 payer)");
|
|
917
1354
|
this.log(` Owner address: ${grantResponse.account}`);
|
|
1355
|
+
if (flags.x402) {
|
|
1356
|
+
this.log(` (payments pull from here, capped at ${describeX402Grant(flags.limit)})`);
|
|
1357
|
+
}
|
|
918
1358
|
this.log(` Permission ID: ${grantResponse.permissionId}`);
|
|
919
1359
|
this.log(` Chain: ${chainId}`);
|
|
920
1360
|
this.log(` Expires: ${new Date(expiryTimestamp * 1e3).toISOString()} (${expiryDays} days)`);
|
|
@@ -929,9 +1369,15 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
929
1369
|
throw error;
|
|
930
1370
|
}
|
|
931
1371
|
}
|
|
932
|
-
resolvePermissions(flagValue, configValue) {
|
|
1372
|
+
resolvePermissions(flagValue, configValue, preset) {
|
|
933
1373
|
let raw;
|
|
934
|
-
if (
|
|
1374
|
+
if (preset.x402) {
|
|
1375
|
+
try {
|
|
1376
|
+
raw = buildX402Permissions(preset.chainId, preset.limit);
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
this.error(err instanceof Error ? err.message : String(err));
|
|
1379
|
+
}
|
|
1380
|
+
} else if (flagValue) {
|
|
935
1381
|
if (flagValue.trimStart().startsWith("{")) {
|
|
936
1382
|
try {
|
|
937
1383
|
raw = JSON.parse(flagValue);
|
|
@@ -939,7 +1385,7 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
939
1385
|
this.error(`--permissions is not valid JSON: ${flagValue}`);
|
|
940
1386
|
}
|
|
941
1387
|
} else {
|
|
942
|
-
const content =
|
|
1388
|
+
const content = fs4.readFileSync(flagValue, "utf-8");
|
|
943
1389
|
try {
|
|
944
1390
|
raw = JSON.parse(content);
|
|
945
1391
|
} catch {
|
|
@@ -949,11 +1395,16 @@ var SessionSetup = class _SessionSetup extends BaseCommand {
|
|
|
949
1395
|
} else if (configValue) {
|
|
950
1396
|
raw = configValue;
|
|
951
1397
|
} else {
|
|
952
|
-
this.error(
|
|
1398
|
+
this.error(
|
|
1399
|
+
'Permissions required. For x402 payments run `jaw session setup --x402` and the scope is built for you. Otherwise pass --permissions or add "permissions" to ~/.jaw/config.json.'
|
|
1400
|
+
);
|
|
953
1401
|
}
|
|
954
1402
|
return parsePermissionsConfig(raw);
|
|
955
1403
|
}
|
|
956
1404
|
};
|
|
1405
|
+
function orphanOf(session) {
|
|
1406
|
+
return { id: session.permissionId, chainId: session.chainId, expiry: session.expiry };
|
|
1407
|
+
}
|
|
957
1408
|
|
|
958
1409
|
export { SessionSetup as default };
|
|
959
1410
|
//# sourceMappingURL=setup.js.map
|