@jaw.id/cli 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -24
- package/dist/base-command.js +28 -8
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +28 -38
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +29 -12
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/config/write.js +288 -0
- package/dist/commands/config/write.js.map +1 -0
- package/dist/commands/disconnect.js +42 -54
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +62 -102
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +218 -92
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/session/revoke.js +727 -0
- package/dist/commands/session/revoke.js.map +1 -0
- package/dist/commands/session/setup.js +960 -0
- package/dist/commands/session/setup.js.map +1 -0
- package/dist/commands/session/status.js +206 -0
- package/dist/commands/session/status.js.map +1 -0
- package/dist/commands/version.js +28 -8
- package/dist/commands/version.js.map +1 -1
- package/dist/index.js +42 -67
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +47 -61
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +17 -13
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crypto.js +3 -15
- package/dist/lib/crypto.js.map +1 -1
- package/dist/lib/keystore.js +64 -0
- package/dist/lib/keystore.js.map +1 -0
- package/dist/lib/output.js +1 -16
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/paths.js +3 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/session-bridge.js +168 -0
- package/dist/lib/session-bridge.js.map +1 -0
- package/dist/lib/session-config.js +52 -0
- package/dist/lib/session-config.js.map +1 -0
- package/dist/lib/validation.js +62 -27
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +15 -46
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +28 -29
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +41 -51
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/resources.js +1 -3
- package/dist/mcp/handlers/resources.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +63 -79
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/server.js +61 -99
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +2 -4
- package/dist/mcp/tools.js.map +1 -1
- package/oclif.manifest.json +304 -4
- package/package.json +19 -1
- package/dist/lib/session-store.js +0 -80
- package/dist/lib/session-store.js.map +0 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
// src/commands/config/write.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
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/lib/validation.ts
|
|
18
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
19
|
+
var SELECTOR_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
20
|
+
var HEX_RE = /^0x[0-9a-fA-F]+$/;
|
|
21
|
+
var VALID_SPEND_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day", "week", "month", "year", "forever"]);
|
|
22
|
+
function parsePermissionsConfig(raw) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
25
|
+
throw new Error("Invalid permissions:\n - Must be an object");
|
|
26
|
+
}
|
|
27
|
+
const obj = raw;
|
|
28
|
+
const calls = obj.calls;
|
|
29
|
+
const spends = obj.spends;
|
|
30
|
+
if (!calls && !spends) {
|
|
31
|
+
throw new Error('Invalid permissions:\n - Must include at least "calls" or "spends"');
|
|
32
|
+
}
|
|
33
|
+
if (calls !== void 0) {
|
|
34
|
+
if (!Array.isArray(calls) || calls.length === 0) {
|
|
35
|
+
errors.push("calls: Must be a non-empty array");
|
|
36
|
+
} else {
|
|
37
|
+
for (let i = 0; i < calls.length; i++) {
|
|
38
|
+
const c = calls[i];
|
|
39
|
+
if (!c || typeof c !== "object") {
|
|
40
|
+
errors.push(`calls.${i}: Must be an object`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (typeof c.target !== "string" || !ADDRESS_RE.test(c.target)) {
|
|
44
|
+
errors.push(`calls.${i}.target: Must be a valid 0x address (40 hex chars)`);
|
|
45
|
+
}
|
|
46
|
+
if (c.selector !== void 0 && (typeof c.selector !== "string" || !SELECTOR_RE.test(c.selector))) {
|
|
47
|
+
errors.push(`calls.${i}.selector: Must be a 4-byte hex selector (e.g. 0xa9059cbb)`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (spends !== void 0) {
|
|
53
|
+
if (!Array.isArray(spends) || spends.length === 0) {
|
|
54
|
+
errors.push("spends: Must be a non-empty array");
|
|
55
|
+
} else {
|
|
56
|
+
for (let i = 0; i < spends.length; i++) {
|
|
57
|
+
const s = spends[i];
|
|
58
|
+
if (!s || typeof s !== "object") {
|
|
59
|
+
errors.push(`spends.${i}: Must be an object`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof s.token !== "string" || !ADDRESS_RE.test(s.token)) {
|
|
63
|
+
errors.push(`spends.${i}.token: Must be a valid 0x address (40 hex chars)`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof s.allowance !== "string" || !HEX_RE.test(s.allowance)) {
|
|
66
|
+
errors.push(`spends.${i}.allowance: Must be a non-empty 0x hex value`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof s.unit !== "string" || !VALID_SPEND_UNITS.has(s.unit)) {
|
|
69
|
+
errors.push(`spends.${i}.unit: Must be one of: ${[...VALID_SPEND_UNITS].join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
if (s.multiplier !== void 0 && (!Number.isInteger(s.multiplier) || s.multiplier < 1)) {
|
|
72
|
+
errors.push(`spends.${i}.multiplier: Must be a positive integer`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (errors.length > 0) {
|
|
78
|
+
throw new Error(`Invalid permissions:
|
|
79
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
80
|
+
}
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
function isValidKeysUrl(url) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(url);
|
|
86
|
+
const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
87
|
+
const isSecure = parsed.protocol === "https:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
88
|
+
return isTrustedHost && isSecure;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isValidRelayUrl(url) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(url);
|
|
96
|
+
const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
97
|
+
const isSecure = parsed.protocol === "wss:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
98
|
+
const isWebSocket = parsed.protocol === "wss:" || parsed.protocol === "ws:";
|
|
99
|
+
return isTrustedHost && isSecure && isWebSocket;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/lib/config.ts
|
|
106
|
+
function ensureDir(dir) {
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
108
|
+
fs.chmodSync(dir, 448);
|
|
109
|
+
}
|
|
110
|
+
function migrateConfig(config) {
|
|
111
|
+
if (config.paymasterUrl && !config.paymasters) {
|
|
112
|
+
const chainId = config.defaultChain ?? 1;
|
|
113
|
+
config.paymasters = { [chainId]: { url: config.paymasterUrl } };
|
|
114
|
+
delete config.paymasterUrl;
|
|
115
|
+
saveConfig(config);
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
function loadConfig() {
|
|
120
|
+
if (!fs.existsSync(PATHS.config)) {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
const raw = fs.readFileSync(PATHS.config, "utf-8");
|
|
124
|
+
try {
|
|
125
|
+
const config = JSON.parse(raw);
|
|
126
|
+
return migrateConfig(config);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function saveConfig(config) {
|
|
134
|
+
ensureDir(PATHS.root);
|
|
135
|
+
fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
|
|
136
|
+
encoding: "utf-8",
|
|
137
|
+
mode: 384
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/lib/output.ts
|
|
142
|
+
function formatOutput(data, format) {
|
|
143
|
+
if (format === "json") {
|
|
144
|
+
return JSON.stringify(data, replaceBigInt, 2);
|
|
145
|
+
}
|
|
146
|
+
return formatHuman(data);
|
|
147
|
+
}
|
|
148
|
+
function replaceBigInt(_key, value) {
|
|
149
|
+
if (typeof value === "bigint") {
|
|
150
|
+
return value.toString();
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function formatHuman(data, indent = 0) {
|
|
155
|
+
if (data === null || data === void 0) {
|
|
156
|
+
return "null";
|
|
157
|
+
}
|
|
158
|
+
if (typeof data === "string" || typeof data === "number" || typeof data === "boolean" || typeof data === "bigint") {
|
|
159
|
+
return String(data);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(data)) {
|
|
162
|
+
if (data.length === 0) return "(empty)";
|
|
163
|
+
return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join("\n");
|
|
164
|
+
}
|
|
165
|
+
if (typeof data === "object") {
|
|
166
|
+
const entries = Object.entries(data);
|
|
167
|
+
if (entries.length === 0) return "(empty)";
|
|
168
|
+
const pad = " ".repeat(indent);
|
|
169
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
170
|
+
return entries.map(([key, val]) => {
|
|
171
|
+
const paddedKey = key.padEnd(maxKeyLen);
|
|
172
|
+
const valStr = typeof val === "object" && val !== null ? "\n" + formatHuman(val, indent + 2) : String(val);
|
|
173
|
+
return `${pad}${paddedKey} ${valStr}`;
|
|
174
|
+
}).join("\n");
|
|
175
|
+
}
|
|
176
|
+
return String(data);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/base-command.ts
|
|
180
|
+
var BaseCommand = class extends Command {
|
|
181
|
+
static baseFlags = {
|
|
182
|
+
output: Flags.string({
|
|
183
|
+
char: "o",
|
|
184
|
+
description: "Output format",
|
|
185
|
+
options: ["json", "human"],
|
|
186
|
+
default: "human",
|
|
187
|
+
env: "JAW_OUTPUT"
|
|
188
|
+
}),
|
|
189
|
+
chain: Flags.integer({
|
|
190
|
+
char: "c",
|
|
191
|
+
description: "Chain ID",
|
|
192
|
+
env: "JAW_CHAIN_ID"
|
|
193
|
+
}),
|
|
194
|
+
"api-key": Flags.string({
|
|
195
|
+
description: "JAW API key",
|
|
196
|
+
env: "JAW_API_KEY"
|
|
197
|
+
}),
|
|
198
|
+
yes: Flags.boolean({
|
|
199
|
+
char: "y",
|
|
200
|
+
description: "Skip confirmations (for AI agents)",
|
|
201
|
+
default: false
|
|
202
|
+
}),
|
|
203
|
+
quiet: Flags.boolean({
|
|
204
|
+
char: "q",
|
|
205
|
+
description: "Suppress non-essential output",
|
|
206
|
+
default: false
|
|
207
|
+
})
|
|
208
|
+
};
|
|
209
|
+
resolveApiKey(flags) {
|
|
210
|
+
const apiKey = flags["api-key"] ?? loadConfig().apiKey;
|
|
211
|
+
if (!apiKey) {
|
|
212
|
+
this.error("API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`");
|
|
213
|
+
}
|
|
214
|
+
return apiKey;
|
|
215
|
+
}
|
|
216
|
+
resolveChainId(flags) {
|
|
217
|
+
const chainId = flags.chain ?? loadConfig().defaultChain;
|
|
218
|
+
if (!chainId) {
|
|
219
|
+
this.error("Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`");
|
|
220
|
+
}
|
|
221
|
+
return chainId;
|
|
222
|
+
}
|
|
223
|
+
outputResult(data, format) {
|
|
224
|
+
const output = formatOutput(data, format);
|
|
225
|
+
this.log(output);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/commands/config/write.ts
|
|
230
|
+
var ConfigWrite = class _ConfigWrite extends BaseCommand {
|
|
231
|
+
static description = "Write full config from inline JSON or a file path to ~/.jaw/config.json.";
|
|
232
|
+
static examples = [
|
|
233
|
+
`<%= config.bin %> config write '{"apiKey":"...","defaultChain":84532}'`,
|
|
234
|
+
"<%= config.bin %> config write ./jaw-config.json"
|
|
235
|
+
];
|
|
236
|
+
static strict = false;
|
|
237
|
+
static flags = {
|
|
238
|
+
...BaseCommand.baseFlags
|
|
239
|
+
};
|
|
240
|
+
async run() {
|
|
241
|
+
const { argv } = await this.parse(_ConfigWrite);
|
|
242
|
+
const rawArgs = argv;
|
|
243
|
+
if (rawArgs.length === 0) {
|
|
244
|
+
this.error(`Provide inline JSON or a file path.
|
|
245
|
+
Usage: jaw config write '{"apiKey":"..."}'`);
|
|
246
|
+
}
|
|
247
|
+
const input = rawArgs.join("").trim();
|
|
248
|
+
let raw;
|
|
249
|
+
if (input.startsWith("{")) {
|
|
250
|
+
raw = input;
|
|
251
|
+
} else {
|
|
252
|
+
if (!fs.existsSync(input)) {
|
|
253
|
+
this.error(`File not found: ${input}`);
|
|
254
|
+
}
|
|
255
|
+
raw = fs.readFileSync(input, "utf-8");
|
|
256
|
+
}
|
|
257
|
+
let config;
|
|
258
|
+
try {
|
|
259
|
+
config = JSON.parse(raw);
|
|
260
|
+
} catch {
|
|
261
|
+
try {
|
|
262
|
+
const compact = raw.replace(/\s+/g, "");
|
|
263
|
+
config = JSON.parse(compact);
|
|
264
|
+
} catch {
|
|
265
|
+
this.error("Invalid JSON");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (config.keysUrl && !isValidKeysUrl(config.keysUrl)) {
|
|
269
|
+
this.error(`Untrusted keysUrl: ${config.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`);
|
|
270
|
+
}
|
|
271
|
+
if (config.relayUrl && !isValidRelayUrl(config.relayUrl)) {
|
|
272
|
+
this.error(`Untrusted relayUrl: ${config.relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);
|
|
273
|
+
}
|
|
274
|
+
if (config.permissions) {
|
|
275
|
+
try {
|
|
276
|
+
parsePermissionsConfig(config.permissions);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.error(error.message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
saveConfig(config);
|
|
282
|
+
this.log("Config written to ~/.jaw/config.json");
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export { ConfigWrite as default };
|
|
287
|
+
//# sourceMappingURL=write.js.map
|
|
288
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/paths.ts","../../../src/lib/validation.ts","../../../src/lib/config.ts","../../../src/lib/output.ts","../../../src/base-command.ts","../../../src/commands/config/write.ts"],"names":["fs2"],"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;AACzD,CAAA;;;ACVA,IAAM,UAAA,GAAa,qBAAA;AACnB,IAAM,WAAA,GAAc,oBAAA;AACpB,IAAM,MAAA,GAAS,kBAAA;AACf,IAAM,iBAAA,mBAAoB,IAAI,GAAA,CAAI,CAAC,QAAA,EAAU,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,SAAS,CAAC,CAAA;AAExF,SAAS,uBAAuB,GAAA,EAAiC;AACtE,EAAA,MAAM,SAAmB,EAAC;AAE1B,EAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,QAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,EAAG;AACjE,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EAC/D;AAEA,EAAA,MAAM,GAAA,GAAM,GAAA;AACZ,EAAA,MAAM,QAAQ,GAAA,CAAI,KAAA;AAClB,EAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AAEnB,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,MAAA,EAAQ;AACrB,IAAA,MAAM,IAAI,MAAM,qEAAqE,CAAA;AAAA,EACvF;AAEA,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC/C,MAAA,MAAA,CAAO,KAAK,kCAAkC,CAAA;AAAA,IAChD,CAAA,MAAO;AACL,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,QAAA,MAAM,CAAA,GAAI,MAAM,CAAC,CAAA;AACjB,QAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,KAAM,QAAA,EAAU;AAC/B,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,MAAA,EAAS,CAAC,CAAA,mBAAA,CAAqB,CAAA;AAC3C,UAAA;AAAA,QACF;AACA,QAAA,IAAI,OAAO,EAAE,MAAA,KAAW,QAAA,IAAY,CAAC,UAAA,CAAW,IAAA,CAAK,CAAA,CAAE,MAAM,CAAA,EAAG;AAC9D,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,MAAA,EAAS,CAAC,CAAA,kDAAA,CAAoD,CAAA;AAAA,QAC5E;AACA,QAAA,IAAI,CAAA,CAAE,QAAA,KAAa,MAAA,KAAc,OAAO,CAAA,CAAE,QAAA,KAAa,QAAA,IAAY,CAAC,WAAA,CAAY,IAAA,CAAK,CAAA,CAAE,QAAQ,CAAA,CAAA,EAAI;AACjG,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,MAAA,EAAS,CAAC,CAAA,0DAAA,CAA4D,CAAA;AAAA,QACpF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,IAAK,MAAA,CAAO,WAAW,CAAA,EAAG;AACjD,MAAA,MAAA,CAAO,KAAK,mCAAmC,CAAA;AAAA,IACjD,CAAA,MAAO;AACL,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACtC,QAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,QAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,KAAM,QAAA,EAAU;AAC/B,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,mBAAA,CAAqB,CAAA;AAC5C,UAAA;AAAA,QACF;AACA,QAAA,IAAI,OAAO,EAAE,KAAA,KAAU,QAAA,IAAY,CAAC,UAAA,CAAW,IAAA,CAAK,CAAA,CAAE,KAAK,CAAA,EAAG;AAC5D,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,iDAAA,CAAmD,CAAA;AAAA,QAC5E;AACA,QAAA,IAAI,OAAO,EAAE,SAAA,KAAc,QAAA,IAAY,CAAC,MAAA,CAAO,IAAA,CAAK,CAAA,CAAE,SAAS,CAAA,EAAG;AAChE,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,4CAAA,CAA8C,CAAA;AAAA,QACvE;AACA,QAAA,IAAI,OAAO,EAAE,IAAA,KAAS,QAAA,IAAY,CAAC,iBAAA,CAAkB,GAAA,CAAI,CAAA,CAAE,IAAI,CAAA,EAAG;AAChE,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,uBAAA,EAA0B,CAAC,GAAG,iBAAiB,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,QACtF;AACA,QAAA,IAAI,CAAA,CAAE,UAAA,KAAe,MAAA,KAAc,CAAC,MAAA,CAAO,SAAA,CAAU,CAAA,CAAE,UAAU,CAAA,IAAM,CAAA,CAAE,UAAA,GAAwB,CAAA,CAAA,EAAI;AACnG,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,uCAAA,CAAyC,CAAA;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA;AAAA,EAAyB,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,IAAA,EAAO,CAAC,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EACrF;AAEA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,KAAa,QAAA,IAAY,OAAO,QAAA,KAAa,WAAA,IAAe,OAAO,QAAA,KAAa,WAAA;AACxG,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,GAAA,EAAsB;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GAAW,OAAO,QAAA,KAAa,MAAA,IAAU,OAAO,QAAA,KAAa,WAAA,IAAe,OAAO,QAAA,KAAa,WAAA;AACtG,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,KAAa,MAAA,IAAU,OAAO,QAAA,KAAa,KAAA;AACtE,IAAA,OAAO,iBAAiB,QAAA,IAAY,WAAA;AAAA,EACtC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;ACnGO,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;;;ACvCO,SAAS,YAAA,CAAa,MAAe,MAAA,EAA8B;AACxE,EAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,IAAA,OAAO,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,aAAA,EAAe,CAAC,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,YAAY,IAAI,CAAA;AACzB;AAEA,SAAS,aAAA,CAAc,MAAc,KAAA,EAAyB;AAC5D,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,MAAM,QAAA,EAAS;AAAA,EACxB;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,WAAA,CAAY,IAAA,EAAe,MAAA,GAAS,CAAA,EAAW;AACtD,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,MAAA,EAAW;AACvC,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,IAAA,KAAS,SAAA,IAAa,OAAO,IAAA,KAAS,QAAA,EAAU;AACjH,IAAA,OAAO,OAAO,IAAI,CAAA;AAAA,EACpB;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACvB,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,SAAA;AAC9B,IAAA,OAAO,KAAK,GAAA,CAAI,CAAC,IAAA,EAAM,CAAA,KAAM,GAAG,CAAA,GAAI,CAAC,CAAA,EAAA,EAAK,WAAA,CAAY,MAAM,MAAA,GAAS,CAAC,CAAC,CAAA,CAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AAAA,EACtF;AAEA,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,IAA+B,CAAA;AAC9D,IAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,SAAA;AACjC,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,MAAA,CAAO,MAAM,CAAA;AAC7B,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,GAAG,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,CAAC,CAAA,KAAM,CAAA,CAAE,MAAM,CAAC,CAAA;AAC5D,IAAA,OAAO,QACJ,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,GAAG,CAAA,KAAM;AACnB,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,MAAA,CAAO,SAAS,CAAA;AACtC,MAAA,MAAM,MAAA,GAAS,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,IAAA,GAAO,IAAA,GAAO,WAAA,CAAY,GAAA,EAAK,MAAA,GAAS,CAAC,CAAA,GAAI,OAAO,GAAG,CAAA;AACzG,MAAA,OAAO,CAAA,EAAG,GAAG,CAAA,EAAG,SAAS,KAAK,MAAM,CAAA,CAAA;AAAA,IACtC,CAAC,CAAA,CACA,IAAA,CAAK,IAAI,CAAA;AAAA,EACd;AAEA,EAAA,OAAO,OAAO,IAAI,CAAA;AACpB;;;ACxCO,IAAe,WAAA,GAAf,cAAmC,OAAA,CAAQ;AAAA,EAChD,OAAgB,SAAA,GAAY;AAAA,IAC1B,MAAA,EAAQ,MAAM,MAAA,CAAO;AAAA,MACnB,IAAA,EAAM,GAAA;AAAA,MACN,WAAA,EAAa,eAAA;AAAA,MACb,OAAA,EAAS,CAAC,MAAA,EAAQ,OAAO,CAAA;AAAA,MACzB,OAAA,EAAS,OAAA;AAAA,MACT,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,IACD,KAAA,EAAO,MAAM,OAAA,CAAQ;AAAA,MACnB,IAAA,EAAM,GAAA;AAAA,MACN,WAAA,EAAa,UAAA;AAAA,MACb,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,IACD,SAAA,EAAW,MAAM,MAAA,CAAO;AAAA,MACtB,WAAA,EAAa,aAAA;AAAA,MACb,GAAA,EAAK;AAAA,KACN,CAAA;AAAA,IACD,GAAA,EAAK,MAAM,OAAA,CAAQ;AAAA,MACjB,IAAA,EAAM,GAAA;AAAA,MACN,WAAA,EAAa,oCAAA;AAAA,MACb,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,IACD,KAAA,EAAO,MAAM,OAAA,CAAQ;AAAA,MACnB,IAAA,EAAM,GAAA;AAAA,MACN,WAAA,EAAa,+BAAA;AAAA,MACb,OAAA,EAAS;AAAA,KACV;AAAA,GACH;AAAA,EAEU,cAAc,KAAA,EAAuC;AAC7D,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,SAAS,CAAA,IAAK,YAAW,CAAE,MAAA;AAChD,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,IAAA,CAAK,MAAM,wFAAwF,CAAA;AAAA,IACrG;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEU,eAAe,KAAA,EAAmC;AAC1D,IAAA,MAAM,OAAA,GAAU,KAAA,CAAM,KAAA,IAAS,UAAA,EAAW,CAAE,YAAA;AAC5C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,IAAA,CAAK,MAAM,6FAA6F,CAAA;AAAA,IAC1G;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEU,YAAA,CAAa,MAAe,MAAA,EAA4B;AAChE,IAAA,MAAM,MAAA,GAAS,YAAA,CAAa,IAAA,EAAM,MAAM,CAAA;AACxC,IAAA,IAAA,CAAK,IAAI,MAAM,CAAA;AAAA,EACjB;AACF,CAAA;;;ACjDA,IAAqB,WAAA,GAArB,MAAqB,YAAA,SAAoB,WAAA,CAAY;AAAA,EACnD,OAAgB,WAAA,GAAc,0EAAA;AAAA,EAE9B,OAAgB,QAAA,GAAW;AAAA,IACzB,CAAA,sEAAA,CAAA;AAAA,IACA;AAAA,GACF;AAAA,EAEA,OAAgB,MAAA,GAAS,KAAA;AAAA,EAEzB,OAAgB,KAAA,GAAQ;AAAA,IACtB,GAAG,WAAA,CAAY;AAAA,GACjB;AAAA,EAEA,MAAM,GAAA,GAAqB;AACzB,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,IAAA,CAAK,MAAM,YAAW,CAAA;AAC7C,IAAA,MAAM,OAAA,GAAU,IAAA;AAEhB,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,IAAA,CAAK,KAAA,CAAM,CAAA;AAAA,0CAAA,CAAmF,CAAA;AAAA,IAChG;AAGA,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,CAAK,EAAE,EAAE,IAAA,EAAK;AAEpC,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG;AACzB,MAAA,GAAA,GAAM,KAAA;AAAA,IACR,CAAA,MAAO;AACL,MAAA,IAAI,CAAIA,EAAA,CAAA,UAAA,CAAW,KAAK,CAAA,EAAG;AACzB,QAAA,IAAA,CAAK,KAAA,CAAM,CAAA,gBAAA,EAAmB,KAAK,CAAA,CAAE,CAAA;AAAA,MACvC;AACA,MAAA,GAAA,GAASA,EAAA,CAAA,YAAA,CAAa,OAAO,OAAO,CAAA;AAAA,IACtC;AAEA,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACzB,CAAA,CAAA,MAAQ;AAEN,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,QAAA,MAAA,GAAS,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,MAC7B,CAAA,CAAA,MAAQ;AACN,QAAA,IAAA,CAAK,MAAM,cAAc,CAAA;AAAA,MAC3B;AAAA,IACF;AAEA,IAAA,IAAI,OAAO,OAAA,IAAW,CAAC,cAAA,CAAe,MAAA,CAAO,OAAO,CAAA,EAAG;AACrD,MAAA,IAAA,CAAK,KAAA,CAAM,CAAA,mBAAA,EAAsB,MAAA,CAAO,OAAO,CAAA,iDAAA,CAAmD,CAAA;AAAA,IACpG;AACA,IAAA,IAAI,OAAO,QAAA,IAAY,CAAC,eAAA,CAAgB,MAAA,CAAO,QAAQ,CAAA,EAAG;AACxD,MAAA,IAAA,CAAK,KAAA,CAAM,CAAA,oBAAA,EAAuB,MAAA,CAAO,QAAQ,CAAA,2CAAA,CAA6C,CAAA;AAAA,IAChG;AACA,IAAA,IAAI,OAAO,WAAA,EAAa;AACtB,MAAA,IAAI;AACF,QAAA,sBAAA,CAAuB,OAAO,WAAW,CAAA;AAAA,MAC3C,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,KAAA,CAAO,MAAgB,OAAO,CAAA;AAAA,MACrC;AAAA,IACF;AAEA,IAAA,UAAA,CAAW,MAAM,CAAA;AACjB,IAAA,IAAA,CAAK,IAAI,sCAAsC,CAAA;AAAA,EACjD;AACF","file":"write.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 type { PermissionsConfig } from './types.js';\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]+$/;\nconst VALID_SPEND_UNITS = new Set(['minute', 'hour', 'day', 'week', 'month', 'year', 'forever']);\n\nexport function parsePermissionsConfig(raw: unknown): PermissionsConfig {\n const errors: string[] = [];\n\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new Error('Invalid permissions:\\n - Must be an object');\n }\n\n const obj = raw as Record<string, unknown>;\n const calls = obj.calls as unknown[] | undefined;\n const spends = obj.spends as unknown[] | undefined;\n\n if (!calls && !spends) {\n throw new Error('Invalid permissions:\\n - Must include at least \"calls\" or \"spends\"');\n }\n\n if (calls !== undefined) {\n if (!Array.isArray(calls) || calls.length === 0) {\n errors.push('calls: Must be a non-empty array');\n } else {\n for (let i = 0; i < calls.length; i++) {\n const c = calls[i] as Record<string, unknown>;\n if (!c || typeof c !== 'object') {\n errors.push(`calls.${i}: Must be an object`);\n continue;\n }\n if (typeof c.target !== 'string' || !ADDRESS_RE.test(c.target)) {\n errors.push(`calls.${i}.target: Must be a valid 0x address (40 hex chars)`);\n }\n if (c.selector !== undefined && (typeof c.selector !== 'string' || !SELECTOR_RE.test(c.selector))) {\n errors.push(`calls.${i}.selector: Must be a 4-byte hex selector (e.g. 0xa9059cbb)`);\n }\n }\n }\n }\n\n if (spends !== undefined) {\n if (!Array.isArray(spends) || spends.length === 0) {\n errors.push('spends: Must be a non-empty array');\n } else {\n for (let i = 0; i < spends.length; i++) {\n const s = spends[i] as Record<string, unknown>;\n if (!s || typeof s !== 'object') {\n errors.push(`spends.${i}: Must be an object`);\n continue;\n }\n if (typeof s.token !== 'string' || !ADDRESS_RE.test(s.token)) {\n errors.push(`spends.${i}.token: Must be a valid 0x address (40 hex chars)`);\n }\n if (typeof s.allowance !== 'string' || !HEX_RE.test(s.allowance)) {\n errors.push(`spends.${i}.allowance: Must be a non-empty 0x hex value`);\n }\n if (typeof s.unit !== 'string' || !VALID_SPEND_UNITS.has(s.unit)) {\n errors.push(`spends.${i}.unit: Must be one of: ${[...VALID_SPEND_UNITS].join(', ')}`);\n }\n if (s.multiplier !== undefined && (!Number.isInteger(s.multiplier) || (s.multiplier as number) < 1)) {\n errors.push(`spends.${i}.multiplier: Must be a positive integer`);\n }\n }\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Invalid permissions:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`);\n }\n\n return raw as PermissionsConfig;\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith('.jaw.id') ||\n parsed.hostname === 'jaw.id' ||\n parsed.hostname === 'localhost' ||\n parsed.hostname === '127.0.0.1';\n const isSecure = parsed.protocol === 'https:' || parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\nexport function isValidRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith('.jaw.id') ||\n parsed.hostname === 'jaw.id' ||\n parsed.hostname === 'localhost' ||\n parsed.hostname === '127.0.0.1';\n const isSecure = parsed.protocol === 'wss:' || parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';\n const isWebSocket = parsed.protocol === 'wss:' || parsed.protocol === 'ws:';\n return isTrustedHost && isSecure && isWebSocket;\n } catch {\n return false;\n }\n}\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\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\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 type { OutputFormat } from './types.js';\n\nexport function formatOutput(data: unknown, format: OutputFormat): string {\n if (format === 'json') {\n return JSON.stringify(data, replaceBigInt, 2);\n }\n return formatHuman(data);\n}\n\nfunction replaceBigInt(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') {\n return value.toString();\n }\n return value;\n}\n\nfunction formatHuman(data: unknown, indent = 0): string {\n if (data === null || data === undefined) {\n return 'null';\n }\n\n if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || typeof data === 'bigint') {\n return String(data);\n }\n\n if (Array.isArray(data)) {\n if (data.length === 0) return '(empty)';\n return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join('\\n');\n }\n\n if (typeof data === 'object') {\n const entries = Object.entries(data as Record<string, unknown>);\n if (entries.length === 0) return '(empty)';\n const pad = ' '.repeat(indent);\n const maxKeyLen = Math.max(...entries.map(([k]) => k.length));\n return entries\n .map(([key, val]) => {\n const paddedKey = key.padEnd(maxKeyLen);\n const valStr = typeof val === 'object' && val !== null ? '\\n' + formatHuman(val, indent + 2) : String(val);\n return `${pad}${paddedKey} ${valStr}`;\n })\n .join('\\n');\n }\n\n return String(data);\n}\n","import { Command, Flags } from '@oclif/core';\nimport type { OutputFormat } from './lib/types.js';\nimport { loadConfig } from './lib/config.js';\nimport { formatOutput } from './lib/output.js';\n\nexport abstract class BaseCommand extends Command {\n static override baseFlags = {\n output: Flags.string({\n char: 'o',\n description: 'Output format',\n options: ['json', 'human'],\n default: 'human',\n env: 'JAW_OUTPUT',\n }),\n chain: Flags.integer({\n char: 'c',\n description: 'Chain ID',\n env: 'JAW_CHAIN_ID',\n }),\n 'api-key': Flags.string({\n description: 'JAW API key',\n env: 'JAW_API_KEY',\n }),\n yes: Flags.boolean({\n char: 'y',\n description: 'Skip confirmations (for AI agents)',\n default: false,\n }),\n quiet: Flags.boolean({\n char: 'q',\n description: 'Suppress non-essential output',\n default: false,\n }),\n };\n\n protected resolveApiKey(flags: { 'api-key'?: string }): string {\n const apiKey = flags['api-key'] ?? loadConfig().apiKey;\n if (!apiKey) {\n this.error('API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`');\n }\n return apiKey;\n }\n\n protected resolveChainId(flags: { chain?: number }): number {\n const chainId = flags.chain ?? loadConfig().defaultChain;\n if (!chainId) {\n this.error('Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`');\n }\n return chainId;\n }\n\n protected outputResult(data: unknown, format: OutputFormat): void {\n const output = formatOutput(data, format);\n this.log(output);\n }\n}\n","import * as fs from 'node:fs';\nimport { BaseCommand } from '../../base-command.js';\nimport { saveConfig } from '../../lib/config.js';\nimport type { JawConfig } from '../../lib/types.js';\nimport { isValidKeysUrl, isValidRelayUrl, parsePermissionsConfig } from '../../lib/validation.js';\n\nexport default class ConfigWrite extends BaseCommand {\n static override description = 'Write full config from inline JSON or a file path to ~/.jaw/config.json.';\n\n static override examples = [\n '<%= config.bin %> config write \\'{\"apiKey\":\"...\",\"defaultChain\":84532}\\'',\n '<%= config.bin %> config write ./jaw-config.json',\n ];\n\n static override strict = false;\n\n static override flags = {\n ...BaseCommand.baseFlags,\n };\n\n async run(): Promise<void> {\n const { argv } = await this.parse(ConfigWrite);\n const rawArgs = argv as string[];\n\n if (rawArgs.length === 0) {\n this.error('Provide inline JSON or a file path.\\nUsage: jaw config write \\'{\"apiKey\":\"...\"}\\'');\n }\n\n // Join all args (handles terminal line wrapping)\n const input = rawArgs.join('').trim();\n\n let raw: string;\n if (input.startsWith('{')) {\n raw = input;\n } else {\n if (!fs.existsSync(input)) {\n this.error(`File not found: ${input}`);\n }\n raw = fs.readFileSync(input, 'utf-8');\n }\n\n let config: JawConfig;\n try {\n config = JSON.parse(raw) as JawConfig;\n } catch {\n // Terminal wrapping may insert spaces/newlines inside strings — try removing all whitespace\n try {\n const compact = raw.replace(/\\s+/g, '');\n config = JSON.parse(compact) as JawConfig;\n } catch {\n this.error('Invalid JSON');\n }\n }\n\n if (config.keysUrl && !isValidKeysUrl(config.keysUrl)) {\n this.error(`Untrusted keysUrl: ${config.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`);\n }\n if (config.relayUrl && !isValidRelayUrl(config.relayUrl)) {\n this.error(`Untrusted relayUrl: ${config.relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);\n }\n if (config.permissions) {\n try {\n parsePermissionsConfig(config.permissions);\n } catch (error) {\n this.error((error as Error).message);\n }\n }\n\n saveConfig(config);\n this.log('Config written to ~/.jaw/config.json');\n }\n}\n"]}
|
|
@@ -11,23 +11,46 @@ var PATHS = {
|
|
|
11
11
|
root: JAW_DIR,
|
|
12
12
|
config: path.join(JAW_DIR, "config.json"),
|
|
13
13
|
session: path.join(JAW_DIR, "session.json"),
|
|
14
|
-
relay: path.join(JAW_DIR, "relay.json")
|
|
14
|
+
relay: path.join(JAW_DIR, "relay.json"),
|
|
15
|
+
keystore: path.join(JAW_DIR, "keystore.json"),
|
|
16
|
+
sessionConfig: path.join(JAW_DIR, "session-config.json")
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
// src/lib/config.ts
|
|
20
|
+
function ensureDir(dir) {
|
|
21
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22
|
+
fs2.chmodSync(dir, 448);
|
|
23
|
+
}
|
|
24
|
+
function migrateConfig(config) {
|
|
25
|
+
if (config.paymasterUrl && !config.paymasters) {
|
|
26
|
+
const chainId = config.defaultChain ?? 1;
|
|
27
|
+
config.paymasters = { [chainId]: { url: config.paymasterUrl } };
|
|
28
|
+
delete config.paymasterUrl;
|
|
29
|
+
saveConfig(config);
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
18
33
|
function loadConfig() {
|
|
19
34
|
if (!fs2.existsSync(PATHS.config)) {
|
|
20
35
|
return {};
|
|
21
36
|
}
|
|
22
37
|
const raw = fs2.readFileSync(PATHS.config, "utf-8");
|
|
23
38
|
try {
|
|
24
|
-
|
|
39
|
+
const config = JSON.parse(raw);
|
|
40
|
+
return migrateConfig(config);
|
|
25
41
|
} catch {
|
|
26
42
|
throw new Error(
|
|
27
43
|
`Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
|
|
28
44
|
);
|
|
29
45
|
}
|
|
30
46
|
}
|
|
47
|
+
function saveConfig(config) {
|
|
48
|
+
ensureDir(PATHS.root);
|
|
49
|
+
fs2.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
mode: 384
|
|
52
|
+
});
|
|
53
|
+
}
|
|
31
54
|
|
|
32
55
|
// src/lib/output.ts
|
|
33
56
|
function formatOutput(data, format) {
|
|
@@ -100,18 +123,14 @@ var BaseCommand = class extends Command {
|
|
|
100
123
|
resolveApiKey(flags) {
|
|
101
124
|
const apiKey = flags["api-key"] ?? loadConfig().apiKey;
|
|
102
125
|
if (!apiKey) {
|
|
103
|
-
this.error(
|
|
104
|
-
"API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`"
|
|
105
|
-
);
|
|
126
|
+
this.error("API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`");
|
|
106
127
|
}
|
|
107
128
|
return apiKey;
|
|
108
129
|
}
|
|
109
130
|
resolveChainId(flags) {
|
|
110
131
|
const chainId = flags.chain ?? loadConfig().defaultChain;
|
|
111
132
|
if (!chainId) {
|
|
112
|
-
this.error(
|
|
113
|
-
"Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`"
|
|
114
|
-
);
|
|
133
|
+
this.error("Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`");
|
|
115
134
|
}
|
|
116
135
|
return chainId;
|
|
117
136
|
}
|
|
@@ -135,11 +154,7 @@ async function deriveSharedSecret(privateKey, peerPublicKey) {
|
|
|
135
154
|
async function encryptMessage(sharedSecret, payload) {
|
|
136
155
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
137
156
|
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
138
|
-
const cipherBuf = await subtle.encrypt(
|
|
139
|
-
{ name: "AES-GCM", iv },
|
|
140
|
-
sharedSecret,
|
|
141
|
-
plaintext
|
|
142
|
-
);
|
|
157
|
+
const cipherBuf = await subtle.encrypt({ name: "AES-GCM", iv }, sharedSecret, plaintext);
|
|
143
158
|
return {
|
|
144
159
|
iv: bufferToBase64(iv),
|
|
145
160
|
ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
|
|
@@ -148,11 +163,7 @@ async function encryptMessage(sharedSecret, payload) {
|
|
|
148
163
|
async function decryptMessage(sharedSecret, envelope) {
|
|
149
164
|
const iv = Buffer.from(envelope.iv, "base64");
|
|
150
165
|
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
151
|
-
const plainBuf = await subtle.decrypt(
|
|
152
|
-
{ name: "AES-GCM", iv },
|
|
153
|
-
sharedSecret,
|
|
154
|
-
ciphertext
|
|
155
|
-
);
|
|
166
|
+
const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv }, sharedSecret, ciphertext);
|
|
156
167
|
return JSON.parse(new TextDecoder().decode(plainBuf));
|
|
157
168
|
}
|
|
158
169
|
async function importKeyFromHex(type, hex) {
|
|
@@ -236,11 +247,7 @@ var WSBridge = class {
|
|
|
236
247
|
let expectingKeyExchange = !this.peerPublicKeyHex;
|
|
237
248
|
const timer = setTimeout(() => {
|
|
238
249
|
ws.close();
|
|
239
|
-
reject(
|
|
240
|
-
new Error(
|
|
241
|
-
"Browser did not connect in time.\nRun `jaw disconnect` then try again."
|
|
242
|
-
)
|
|
243
|
-
);
|
|
250
|
+
reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
|
|
244
251
|
}, 3e4);
|
|
245
252
|
const sendEncryptedInit = async () => {
|
|
246
253
|
if (!this.sharedSecret) return;
|
|
@@ -263,10 +270,7 @@ var WSBridge = class {
|
|
|
263
270
|
if (!msg) return;
|
|
264
271
|
if (msg.type === "encrypted" && this.sharedSecret) {
|
|
265
272
|
try {
|
|
266
|
-
const inner = await decryptMessage(
|
|
267
|
-
this.sharedSecret,
|
|
268
|
-
msg
|
|
269
|
-
);
|
|
273
|
+
const inner = await decryptMessage(this.sharedSecret, msg);
|
|
270
274
|
if (inner.type === "ready") {
|
|
271
275
|
clearTimeout(readyTimer);
|
|
272
276
|
ws.off("message", onMsg);
|
|
@@ -304,6 +308,10 @@ var WSBridge = class {
|
|
|
304
308
|
expectingKeyExchange = true;
|
|
305
309
|
onBrowserNeeded().catch(() => {
|
|
306
310
|
});
|
|
311
|
+
} else if (!onBrowserNeeded) {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
ws.close();
|
|
314
|
+
reject(new Error("Browser not connected \u2014 relay session is stale."));
|
|
307
315
|
}
|
|
308
316
|
} else if (msg.type === "browser_connected") {
|
|
309
317
|
expectingKeyExchange = true;
|
|
@@ -353,9 +361,7 @@ var WSBridge = class {
|
|
|
353
361
|
return new Promise((resolve, reject) => {
|
|
354
362
|
const timer = setTimeout(() => {
|
|
355
363
|
reject(
|
|
356
|
-
new Error(
|
|
357
|
-
`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`
|
|
358
|
-
)
|
|
364
|
+
new Error(`Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`)
|
|
359
365
|
);
|
|
360
366
|
this.close();
|
|
361
367
|
}, this.timeout);
|
|
@@ -363,10 +369,7 @@ var WSBridge = class {
|
|
|
363
369
|
const msg = safeParse(data);
|
|
364
370
|
if (!msg || msg.type !== "encrypted" || !this.sharedSecret) return;
|
|
365
371
|
try {
|
|
366
|
-
const inner = await decryptMessage(
|
|
367
|
-
this.sharedSecret,
|
|
368
|
-
msg
|
|
369
|
-
);
|
|
372
|
+
const inner = await decryptMessage(this.sharedSecret, msg);
|
|
370
373
|
if (inner.type === "rpc_response" && inner.id === id) {
|
|
371
374
|
clearTimeout(timer);
|
|
372
375
|
ws.off("message", onMessage);
|
|
@@ -374,11 +377,7 @@ var WSBridge = class {
|
|
|
374
377
|
resolve(inner.data);
|
|
375
378
|
} else {
|
|
376
379
|
const err = inner.error;
|
|
377
|
-
reject(
|
|
378
|
-
new Error(
|
|
379
|
-
err ? `[${err.code}] ${err.message}` : "Request failed"
|
|
380
|
-
)
|
|
381
|
-
);
|
|
380
|
+
reject(new Error(err ? `[${err.code}] ${err.message}` : "Request failed"));
|
|
382
381
|
}
|
|
383
382
|
}
|
|
384
383
|
} catch {
|
|
@@ -388,9 +387,6 @@ var WSBridge = class {
|
|
|
388
387
|
this.sendRaw(ws, serialized);
|
|
389
388
|
});
|
|
390
389
|
}
|
|
391
|
-
isOpen() {
|
|
392
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
393
|
-
}
|
|
394
390
|
async shutdown() {
|
|
395
391
|
this.disposed = true;
|
|
396
392
|
if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {
|
|
@@ -398,10 +394,7 @@ var WSBridge = class {
|
|
|
398
394
|
const envelope = await encryptMessage(this.sharedSecret, {
|
|
399
395
|
type: "shutdown"
|
|
400
396
|
});
|
|
401
|
-
this.sendRaw(
|
|
402
|
-
this.ws,
|
|
403
|
-
JSON.stringify({ type: "encrypted", ...envelope })
|
|
404
|
-
);
|
|
397
|
+
this.sendRaw(this.ws, JSON.stringify({ type: "encrypted", ...envelope }));
|
|
405
398
|
} catch {
|
|
406
399
|
}
|
|
407
400
|
}
|
|
@@ -479,10 +472,8 @@ var WSBridge = class {
|
|
|
479
472
|
this.reconnectAttempts++;
|
|
480
473
|
setTimeout(() => {
|
|
481
474
|
if (this.disposed) return;
|
|
482
|
-
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
);
|
|
475
|
+
this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(() => {
|
|
476
|
+
});
|
|
486
477
|
}, delay);
|
|
487
478
|
}
|
|
488
479
|
/** Send a raw string over the WebSocket, enforcing message size limits. */
|
|
@@ -492,10 +483,7 @@ var WSBridge = class {
|
|
|
492
483
|
async deriveSecret() {
|
|
493
484
|
if (!this.peerPublicKeyHex) return;
|
|
494
485
|
const privateKey = await importKeyFromHex("private", this.privateKeyHex);
|
|
495
|
-
const peerPublicKey = await importKeyFromHex(
|
|
496
|
-
"public",
|
|
497
|
-
this.peerPublicKeyHex
|
|
498
|
-
);
|
|
486
|
+
const peerPublicKey = await importKeyFromHex("public", this.peerPublicKeyHex);
|
|
499
487
|
this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);
|
|
500
488
|
}
|
|
501
489
|
};
|