@jaw.id/cli 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +239 -0
  2. package/bin/run.js +5 -0
  3. package/dist/base-command.js +124 -0
  4. package/dist/base-command.js.map +1 -0
  5. package/dist/commands/config/set.js +244 -0
  6. package/dist/commands/config/set.js.map +1 -0
  7. package/dist/commands/config/show.js +147 -0
  8. package/dist/commands/config/show.js.map +1 -0
  9. package/dist/commands/disconnect.js +196 -0
  10. package/dist/commands/disconnect.js.map +1 -0
  11. package/dist/commands/mcp/index.js +670 -0
  12. package/dist/commands/mcp/index.js.map +1 -0
  13. package/dist/commands/rpc/call.js +531 -0
  14. package/dist/commands/rpc/call.js.map +1 -0
  15. package/dist/index.js +460 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/bridge-singleton.js +362 -0
  18. package/dist/lib/bridge-singleton.js.map +1 -0
  19. package/dist/lib/config.js +75 -0
  20. package/dist/lib/config.js.map +1 -0
  21. package/dist/lib/output.js +56 -0
  22. package/dist/lib/output.js.map +1 -0
  23. package/dist/lib/paths.js +16 -0
  24. package/dist/lib/paths.js.map +1 -0
  25. package/dist/lib/session-store.js +81 -0
  26. package/dist/lib/session-store.js.map +1 -0
  27. package/dist/lib/types.js +3 -0
  28. package/dist/lib/types.js.map +1 -0
  29. package/dist/lib/validation.js +45 -0
  30. package/dist/lib/validation.js.map +1 -0
  31. package/dist/lib/ws-bridge.js +141 -0
  32. package/dist/lib/ws-bridge.js.map +1 -0
  33. package/dist/lib/ws-daemon.js +344 -0
  34. package/dist/lib/ws-daemon.js.map +1 -0
  35. package/dist/mcp/handlers/config.js +149 -0
  36. package/dist/mcp/handlers/config.js.map +1 -0
  37. package/dist/mcp/handlers/daemon.js +185 -0
  38. package/dist/mcp/handlers/daemon.js.map +1 -0
  39. package/dist/mcp/handlers/resources.js +60 -0
  40. package/dist/mcp/handlers/resources.js.map +1 -0
  41. package/dist/mcp/handlers/rpc.js +444 -0
  42. package/dist/mcp/handlers/rpc.js.map +1 -0
  43. package/dist/mcp/helpers.js +26 -0
  44. package/dist/mcp/helpers.js.map +1 -0
  45. package/dist/mcp/server.js +658 -0
  46. package/dist/mcp/server.js.map +1 -0
  47. package/dist/mcp/tools.js +22 -0
  48. package/dist/mcp/tools.js.map +1 -0
  49. package/oclif.manifest.json +335 -0
  50. package/package.json +100 -0
@@ -0,0 +1,81 @@
1
+ import * as fs2 from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ // src/lib/session-store.ts
6
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
7
+ var PATHS = {
8
+ root: JAW_DIR,
9
+ config: path.join(JAW_DIR, "config.json"),
10
+ session: path.join(JAW_DIR, "session.json"),
11
+ bridge: path.join(JAW_DIR, "bridge.json"),
12
+ daemonLog: path.join(JAW_DIR, "daemon.log")
13
+ };
14
+
15
+ // src/lib/validation.ts
16
+ function isValidAddress(value) {
17
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
18
+ }
19
+
20
+ // src/lib/config.ts
21
+ function ensureDir(dir) {
22
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
23
+ fs2.chmodSync(dir, 448);
24
+ }
25
+
26
+ // src/lib/session-store.ts
27
+ var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
28
+ function isValidSessionShape(session) {
29
+ if (!session || typeof session !== "object") return false;
30
+ const s = session;
31
+ return typeof s.address === "string" && isValidAddress(s.address) && typeof s.chainId === "number" && Number.isInteger(s.chainId) && s.chainId > 0 && typeof s.connectedAt === "string" && !Number.isNaN(Date.parse(s.connectedAt)) && typeof s.expiresAt === "string" && !Number.isNaN(Date.parse(s.expiresAt));
32
+ }
33
+ function loadSession() {
34
+ try {
35
+ if (!fs2.existsSync(PATHS.session)) return null;
36
+ const raw = fs2.readFileSync(PATHS.session, "utf-8");
37
+ const parsed = JSON.parse(raw);
38
+ if (!isValidSessionShape(parsed)) {
39
+ clearSession();
40
+ return null;
41
+ }
42
+ if (!isSessionValid(parsed)) {
43
+ clearSession();
44
+ return null;
45
+ }
46
+ return parsed;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function saveSession(session) {
52
+ ensureDir(PATHS.root);
53
+ fs2.writeFileSync(PATHS.session, JSON.stringify(session, null, 2) + "\n", {
54
+ encoding: "utf-8",
55
+ mode: 384
56
+ });
57
+ }
58
+ function clearSession() {
59
+ try {
60
+ if (fs2.existsSync(PATHS.session)) {
61
+ fs2.unlinkSync(PATHS.session);
62
+ }
63
+ } catch {
64
+ }
65
+ }
66
+ function isSessionValid(session) {
67
+ return new Date(session.expiresAt).getTime() > Date.now();
68
+ }
69
+ function createSession(address, chainId) {
70
+ const now = /* @__PURE__ */ new Date();
71
+ return {
72
+ address,
73
+ chainId,
74
+ connectedAt: now.toISOString(),
75
+ expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString()
76
+ };
77
+ }
78
+
79
+ export { clearSession, createSession, isSessionValid, loadSession, saveSession };
80
+ //# sourceMappingURL=session-store.js.map
81
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts","../../src/lib/session-store.ts"],"names":["fs"],"mappings":"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","file":"session-store.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 bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\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 =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\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 } 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\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 return JSON.parse(raw) as JawConfig;\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 getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","/**\n * Persists CLI session state to ~/.jaw/session.json\n * Avoids re-authenticating on every CLI invocation.\n */\n\nimport * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir } from \"./config.js\";\nimport { isValidAddress } from \"./validation.js\";\n\nexport interface CLISession {\n readonly address: string;\n readonly chainId: number;\n readonly connectedAt: string;\n readonly expiresAt: string;\n}\n\nconst SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nfunction isValidSessionShape(session: unknown): session is CLISession {\n if (!session || typeof session !== \"object\") return false;\n const s = session as Record<string, unknown>;\n return (\n typeof s.address === \"string\" &&\n isValidAddress(s.address) &&\n typeof s.chainId === \"number\" &&\n Number.isInteger(s.chainId) &&\n s.chainId > 0 &&\n typeof s.connectedAt === \"string\" &&\n !Number.isNaN(Date.parse(s.connectedAt)) &&\n typeof s.expiresAt === \"string\" &&\n !Number.isNaN(Date.parse(s.expiresAt))\n );\n}\n\nexport function loadSession(): CLISession | null {\n try {\n if (!fs.existsSync(PATHS.session)) return null;\n const raw = fs.readFileSync(PATHS.session, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!isValidSessionShape(parsed)) {\n clearSession();\n return null;\n }\n if (!isSessionValid(parsed)) {\n clearSession();\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport function saveSession(session: CLISession): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.session, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function clearSession(): void {\n try {\n if (fs.existsSync(PATHS.session)) {\n fs.unlinkSync(PATHS.session);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isSessionValid(session: CLISession): boolean {\n return new Date(session.expiresAt).getTime() > Date.now();\n}\n\nexport function createSession(address: string, chainId: number): CLISession {\n const now = new Date();\n return {\n address,\n chainId,\n connectedAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString(),\n };\n}\n"]}
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=types.js.map
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
@@ -0,0 +1,45 @@
1
+ // src/lib/validation.ts
2
+ function isValidAddress(value) {
3
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
4
+ }
5
+ function isValidHex(value) {
6
+ return /^0x[0-9a-fA-F]*$/.test(value);
7
+ }
8
+ function isValidChainId(value) {
9
+ const num = typeof value === "string" ? parseInt(value, 10) : value;
10
+ return Number.isInteger(num) && num > 0;
11
+ }
12
+ function parseChainId(value) {
13
+ const num = parseInt(value, 10);
14
+ if (!isValidChainId(num)) {
15
+ throw new Error(`Invalid chain ID: ${value}`);
16
+ }
17
+ return num;
18
+ }
19
+ function assertAddress(value, label = "address") {
20
+ if (!isValidAddress(value)) {
21
+ throw new Error(`Invalid ${label}: ${value}`);
22
+ }
23
+ return value;
24
+ }
25
+ function parseWei(raw, label = "value") {
26
+ try {
27
+ return BigInt(raw);
28
+ } catch {
29
+ throw new Error(`Invalid ${label}: "${raw}" is not a valid wei amount`);
30
+ }
31
+ }
32
+ function isValidKeysUrl(url) {
33
+ try {
34
+ const parsed = new URL(url);
35
+ const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
36
+ const isSecure = parsed.protocol === "https:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
37
+ return isTrustedHost && isSecure;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export { assertAddress, isValidAddress, isValidChainId, isValidHex, isValidKeysUrl, parseChainId, parseWei };
44
+ //# sourceMappingURL=validation.js.map
45
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/validation.ts"],"names":[],"mappings":";AAAO,SAAS,eAAe,KAAA,EAAwB;AACrD,EAAA,OAAO,qBAAA,CAAsB,KAAK,KAAK,CAAA;AACzC;AAEO,SAAS,WAAW,KAAA,EAAwB;AACjD,EAAA,OAAO,kBAAA,CAAmB,KAAK,KAAK,CAAA;AACtC;AAEO,SAAS,eAAe,KAAA,EAAiC;AAC9D,EAAA,MAAM,MAAM,OAAO,KAAA,KAAU,WAAW,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,GAAI,KAAA;AAC9D,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,GAAG,CAAA,IAAK,GAAA,GAAM,CAAA;AACxC;AAEO,SAAS,aAAa,KAAA,EAAuB;AAClD,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AAC9B,EAAA,IAAI,CAAC,cAAA,CAAe,GAAG,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,aAAA,CAAc,KAAA,EAAe,KAAA,GAAQ,SAAA,EAA0B;AAC7E,EAAA,IAAI,CAAC,cAAA,CAAe,KAAK,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,QAAA,CAAS,GAAA,EAAa,KAAA,GAAQ,OAAA,EAAiB;AAC7D,EAAA,IAAI;AACF,IAAA,OAAO,OAAO,GAAG,CAAA;AAAA,EACnB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,GAAA,EAAM,GAAG,CAAA,2BAAA,CAA6B,CAAA;AAAA,EACxE;AACF;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,GACJ,OAAO,QAAA,KAAa,QAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF","file":"validation.js","sourcesContent":["export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\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 =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n"]}
@@ -0,0 +1,141 @@
1
+ import * as crypto from 'crypto';
2
+ import WebSocket from 'ws';
3
+
4
+ // src/lib/ws-bridge.ts
5
+ var DEFAULT_TIMEOUT_MS = 12e4;
6
+ var WSBridge = class {
7
+ port;
8
+ token;
9
+ timeout;
10
+ ws = null;
11
+ constructor(options) {
12
+ this.port = options.port;
13
+ this.token = options.token;
14
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
15
+ }
16
+ /**
17
+ * Connect to the daemon's WebSocket server.
18
+ */
19
+ async connect() {
20
+ return new Promise((resolve, reject) => {
21
+ const url = `ws://127.0.0.1:${this.port}?token=${encodeURIComponent(this.token)}&role=cli`;
22
+ const ws = new WebSocket(url);
23
+ const timer = setTimeout(() => {
24
+ ws.close();
25
+ reject(
26
+ new Error(
27
+ "Browser SDK did not connect in time.\nIf the browser tab failed to open, run `jaw disconnect` then try again."
28
+ )
29
+ );
30
+ }, 3e4);
31
+ ws.on("open", () => {
32
+ clearTimeout(timer);
33
+ this.ws = ws;
34
+ });
35
+ ws.on("message", (data) => {
36
+ let msg;
37
+ try {
38
+ msg = JSON.parse(data.toString());
39
+ } catch {
40
+ return;
41
+ }
42
+ if (msg.type === "status" && msg.browserConnected) {
43
+ clearTimeout(timer);
44
+ resolve();
45
+ } else if (msg.type === "browser_connected") {
46
+ clearTimeout(timer);
47
+ resolve();
48
+ }
49
+ });
50
+ ws.on("error", (err) => {
51
+ clearTimeout(timer);
52
+ reject(err);
53
+ });
54
+ ws.on("close", () => {
55
+ clearTimeout(timer);
56
+ });
57
+ });
58
+ }
59
+ /**
60
+ * Send an RPC request through the daemon to the browser SDK.
61
+ */
62
+ async request(method, params) {
63
+ const ws = this.ws;
64
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
65
+ throw new Error("Not connected to bridge daemon");
66
+ }
67
+ const id = crypto.randomUUID();
68
+ return new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ reject(
71
+ new Error(
72
+ `Request timed out after ${this.timeout / 1e3}s. Did you complete the action in the browser?`
73
+ )
74
+ );
75
+ this.close();
76
+ }, this.timeout);
77
+ const onMessage = (data) => {
78
+ let msg;
79
+ try {
80
+ msg = JSON.parse(data.toString());
81
+ } catch {
82
+ return;
83
+ }
84
+ if (msg.type === "rpc_response" && msg.id === id) {
85
+ clearTimeout(timer);
86
+ ws.off("message", onMessage);
87
+ if (msg.success) {
88
+ resolve(msg.data);
89
+ } else {
90
+ const err = msg.error;
91
+ reject(
92
+ new Error(
93
+ err ? `[${err.code}] ${err.message}` : "Request failed"
94
+ )
95
+ );
96
+ }
97
+ }
98
+ };
99
+ ws.on("message", onMessage);
100
+ ws.send(
101
+ JSON.stringify({
102
+ id,
103
+ type: "rpc_request",
104
+ method,
105
+ params
106
+ })
107
+ );
108
+ });
109
+ }
110
+ /**
111
+ * Check if the WebSocket connection is open.
112
+ */
113
+ isOpen() {
114
+ return this.ws?.readyState === WebSocket.OPEN;
115
+ }
116
+ /**
117
+ * Send a shutdown signal to the daemon.
118
+ */
119
+ shutdown() {
120
+ if (this.ws?.readyState === WebSocket.OPEN) {
121
+ this.ws.send(JSON.stringify({ type: "shutdown" }));
122
+ }
123
+ this.close();
124
+ }
125
+ /**
126
+ * Close the client connection (daemon stays alive).
127
+ */
128
+ close() {
129
+ if (this.ws) {
130
+ try {
131
+ this.ws.close();
132
+ } catch {
133
+ }
134
+ this.ws = null;
135
+ }
136
+ }
137
+ };
138
+
139
+ export { WSBridge };
140
+ //# sourceMappingURL=ws-bridge.js.map
141
+ //# sourceMappingURL=ws-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/ws-bridge.ts"],"names":[],"mappings":"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","file":"ws-bridge.js","sourcesContent":["/**\n * WSBridge — CLI-side client\n *\n * Lightweight WebSocket client that connects to the background daemon.\n * Each CLI command creates a WSBridge, sends one request, and exits.\n * The daemon (and browser SDK) stay alive across commands.\n */\n\nimport * as crypto from \"node:crypto\";\nimport WebSocket from \"ws\";\n\nexport interface WSBridgeOptions {\n port: number;\n token: string;\n timeout?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nexport class WSBridge {\n private readonly port: number;\n private readonly token: string;\n private readonly timeout: number;\n private ws: WebSocket | null = null;\n\n constructor(options: WSBridgeOptions) {\n this.port = options.port;\n this.token = options.token;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Connect to the daemon's WebSocket server.\n */\n async connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const url = `ws://127.0.0.1:${this.port}?token=${encodeURIComponent(this.token)}&role=cli`;\n const ws = new WebSocket(url);\n\n const timer = setTimeout(() => {\n ws.close();\n reject(\n new Error(\n \"Browser SDK did not connect in time.\\n\" +\n \"If the browser tab failed to open, run `jaw disconnect` then try again.\",\n ),\n );\n }, 30_000);\n\n ws.on(\"open\", () => {\n clearTimeout(timer);\n this.ws = ws;\n });\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n // Wait for the status message confirming daemon is ready.\n // If browser isn't connected yet, stay connected and wait for\n // a \"browser_connected\" event (the daemon notifies CLI clients\n // when the browser joins).\n if (msg.type === \"status\" && msg.browserConnected) {\n clearTimeout(timer);\n resolve();\n } else if (msg.type === \"browser_connected\") {\n clearTimeout(timer);\n resolve();\n }\n });\n\n ws.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n ws.on(\"close\", () => {\n clearTimeout(timer);\n });\n });\n }\n\n /**\n * Send an RPC request through the daemon to the browser SDK.\n */\n async request(method: string, params?: unknown): Promise<unknown> {\n const ws = this.ws;\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"Not connected to bridge daemon\");\n }\n\n const id = crypto.randomUUID();\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new Error(\n `Request timed out after ${this.timeout / 1000}s. ` +\n \"Did you complete the action in the browser?\",\n ),\n );\n this.close();\n }, this.timeout);\n\n const onMessage = (data: WebSocket.Data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n if (msg.type === \"rpc_response\" && msg.id === id) {\n clearTimeout(timer);\n ws.off(\"message\", onMessage);\n\n if (msg.success) {\n resolve(msg.data);\n } else {\n const err = msg.error as\n | { code: number; message: string }\n | undefined;\n reject(\n new Error(\n err ? `[${err.code}] ${err.message}` : \"Request failed\",\n ),\n );\n }\n }\n };\n\n ws.on(\"message\", onMessage);\n\n ws.send(\n JSON.stringify({\n id,\n type: \"rpc_request\",\n method,\n params,\n }),\n );\n });\n }\n\n /**\n * Check if the WebSocket connection is open.\n */\n isOpen(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Send a shutdown signal to the daemon.\n */\n shutdown(): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({ type: \"shutdown\" }));\n }\n this.close();\n }\n\n /**\n * Close the client connection (daemon stays alive).\n */\n close(): void {\n if (this.ws) {\n try {\n this.ws.close();\n } catch {\n // ignore\n }\n this.ws = null;\n }\n }\n}\n"]}
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ import * as crypto from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import { WebSocketServer, WebSocket } from 'ws';
7
+
8
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
9
+ var BRIDGE_PATH = path.join(JAW_DIR, "bridge.json");
10
+ var HEARTBEAT_INTERVAL_MS = 3e4;
11
+ var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
12
+ var MAX_PENDING_REQUESTS = 100;
13
+ var MAX_CLI_CLIENTS = 50;
14
+ var RATE_LIMIT_WINDOW_MS = 6e4;
15
+ var RATE_LIMIT_MAX_REQUESTS = 60;
16
+ function isValidKeysUrl(url) {
17
+ try {
18
+ const parsed = new URL(url);
19
+ const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
20
+ const isSecure = parsed.protocol === "https:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
21
+ return isTrustedHost && isSecure;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ var args = JSON.parse(process.argv[2] ?? "{}");
27
+ if (!isValidKeysUrl(args.keysUrl)) {
28
+ process.stderr.write(
29
+ `Untrusted keysUrl: ${args.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.
30
+ `
31
+ );
32
+ process.exit(1);
33
+ }
34
+ var apiKey = process.env.JAW_DAEMON_API_KEY ?? "";
35
+ delete process.env.JAW_DAEMON_API_KEY;
36
+ var token = crypto.randomUUID();
37
+ var timeout = args.timeout ?? 12e4;
38
+ var pendingRequests = /* @__PURE__ */ new Map();
39
+ var browserWs = null;
40
+ var browserReady = false;
41
+ var idleTimer = null;
42
+ var heartbeat = null;
43
+ var lastBrowserOpenTime = 0;
44
+ var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
45
+ var serverPort = null;
46
+ var cliClients = /* @__PURE__ */ new Set();
47
+ var rateLimits = /* @__PURE__ */ new WeakMap();
48
+ function isRateLimited(ws) {
49
+ const now = Date.now();
50
+ let entry = rateLimits.get(ws);
51
+ if (!entry) {
52
+ entry = { timestamps: [] };
53
+ rateLimits.set(ws, entry);
54
+ }
55
+ entry.timestamps = entry.timestamps.filter(
56
+ (t) => now - t < RATE_LIMIT_WINDOW_MS
57
+ );
58
+ if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
59
+ return true;
60
+ }
61
+ entry.timestamps.push(now);
62
+ return false;
63
+ }
64
+ async function reopenBrowser() {
65
+ if (!serverPort) return;
66
+ if (browserReady || browserWs?.readyState === WebSocket.OPEN) return;
67
+ const now = Date.now();
68
+ if (now - lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) return;
69
+ lastBrowserOpenTime = now;
70
+ const bridgeUrl = buildBridgeUrl(serverPort);
71
+ try {
72
+ const { default: open } = await import('open');
73
+ await open(bridgeUrl);
74
+ } catch {
75
+ }
76
+ }
77
+ function resetIdleTimer() {
78
+ if (idleTimer) clearTimeout(idleTimer);
79
+ idleTimer = setTimeout(() => {
80
+ cleanup();
81
+ process.exit(0);
82
+ }, IDLE_TIMEOUT_MS);
83
+ }
84
+ function cleanup() {
85
+ if (heartbeat) clearInterval(heartbeat);
86
+ if (browserWs?.readyState === WebSocket.OPEN) {
87
+ browserWs.send(JSON.stringify({ type: "shutdown" }));
88
+ }
89
+ try {
90
+ if (fs.existsSync(BRIDGE_PATH)) fs.unlinkSync(BRIDGE_PATH);
91
+ } catch {
92
+ }
93
+ }
94
+ process.on("SIGTERM", () => {
95
+ cleanup();
96
+ process.exit(0);
97
+ });
98
+ process.on("SIGINT", () => {
99
+ cleanup();
100
+ process.exit(0);
101
+ });
102
+ process.on("exit", cleanup);
103
+ var MAX_PAYLOAD_BYTES = 1048576;
104
+ var wss = new WebSocketServer({
105
+ host: "127.0.0.1",
106
+ port: 0,
107
+ maxPayload: MAX_PAYLOAD_BYTES
108
+ });
109
+ wss.on("listening", async () => {
110
+ const addr = wss.address();
111
+ if (typeof addr === "string" || !addr) {
112
+ process.exit(1);
113
+ }
114
+ const port = addr.port;
115
+ serverPort = port;
116
+ fs.mkdirSync(JAW_DIR, { recursive: true, mode: 448 });
117
+ const info = {
118
+ port,
119
+ token,
120
+ pid: process.pid,
121
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
122
+ };
123
+ fs.writeFileSync(BRIDGE_PATH, JSON.stringify(info, null, 2) + "\n", {
124
+ encoding: "utf-8",
125
+ mode: 384
126
+ });
127
+ lastBrowserOpenTime = Date.now();
128
+ const bridgeUrl = buildBridgeUrl(port);
129
+ const { default: open } = await import('open');
130
+ await open(bridgeUrl);
131
+ resetIdleTimer();
132
+ });
133
+ wss.on("connection", (ws, req) => {
134
+ const url = new URL(req.url ?? "", "http://127.0.0.1");
135
+ const urlToken = url.searchParams.get("token");
136
+ const role = url.searchParams.get("role");
137
+ if (urlToken !== token) {
138
+ ws.close(4001, "Invalid token");
139
+ return;
140
+ }
141
+ if (role === "browser") {
142
+ handleBrowserConnection(ws);
143
+ } else {
144
+ handleCliConnection(ws);
145
+ }
146
+ });
147
+ function handleBrowserConnection(ws) {
148
+ if (browserWs) {
149
+ browserWs.close(4e3, "Replaced by new browser connection");
150
+ }
151
+ browserWs = ws;
152
+ browserReady = false;
153
+ ws.send(
154
+ JSON.stringify({
155
+ type: "init",
156
+ apiKey,
157
+ chainId: args.chainId,
158
+ ens: args.ens,
159
+ paymasterUrl: args.paymasterUrl
160
+ })
161
+ );
162
+ ws.on("message", (data) => {
163
+ let msg;
164
+ try {
165
+ msg = JSON.parse(data.toString());
166
+ } catch {
167
+ return;
168
+ }
169
+ switch (msg.type) {
170
+ case "ready":
171
+ browserReady = true;
172
+ if (heartbeat) clearInterval(heartbeat);
173
+ heartbeat = setInterval(() => {
174
+ if (browserWs?.readyState === WebSocket.OPEN) {
175
+ browserWs.send(JSON.stringify({ type: "ping" }));
176
+ }
177
+ }, HEARTBEAT_INTERVAL_MS);
178
+ for (const cli of cliClients) {
179
+ if (cli.readyState === WebSocket.OPEN) {
180
+ cli.send(JSON.stringify({ type: "browser_connected" }));
181
+ }
182
+ }
183
+ break;
184
+ case "rpc_response": {
185
+ const id = msg.id;
186
+ const pending = pendingRequests.get(id);
187
+ if (!pending) return;
188
+ clearTimeout(pending.timer);
189
+ pendingRequests.delete(id);
190
+ if (pending.clientWs.readyState === WebSocket.OPEN) {
191
+ pending.clientWs.send(
192
+ JSON.stringify({
193
+ id: pending.clientId,
194
+ type: "rpc_response",
195
+ success: msg.success,
196
+ data: msg.data,
197
+ error: msg.error,
198
+ address: msg.address
199
+ })
200
+ );
201
+ }
202
+ break;
203
+ }
204
+ }
205
+ });
206
+ ws.on("close", () => {
207
+ if (browserWs === ws) {
208
+ browserReady = false;
209
+ browserWs = null;
210
+ for (const [, pending] of pendingRequests) {
211
+ clearTimeout(pending.timer);
212
+ if (pending.clientWs.readyState === WebSocket.OPEN) {
213
+ pending.clientWs.send(
214
+ JSON.stringify({
215
+ id: pending.clientId,
216
+ type: "rpc_response",
217
+ success: false,
218
+ error: {
219
+ code: -32001,
220
+ message: "Browser tab was closed. Reopening \u2014 please retry in a few seconds."
221
+ }
222
+ })
223
+ );
224
+ }
225
+ }
226
+ pendingRequests.clear();
227
+ }
228
+ });
229
+ }
230
+ function handleCliConnection(ws) {
231
+ if (cliClients.size >= MAX_CLI_CLIENTS) {
232
+ ws.close(4002, "Too many CLI connections");
233
+ return;
234
+ }
235
+ resetIdleTimer();
236
+ cliClients.add(ws);
237
+ ws.send(
238
+ JSON.stringify({
239
+ type: "status",
240
+ browserConnected: browserReady
241
+ })
242
+ );
243
+ if (!browserReady) {
244
+ reopenBrowser();
245
+ }
246
+ ws.on("close", () => {
247
+ cliClients.delete(ws);
248
+ });
249
+ ws.on("message", (data) => {
250
+ let msg;
251
+ try {
252
+ msg = JSON.parse(data.toString());
253
+ } catch {
254
+ return;
255
+ }
256
+ if (msg.type === "rpc_request") {
257
+ resetIdleTimer();
258
+ handleCliRpcRequest(ws, msg);
259
+ } else if (msg.type === "shutdown") {
260
+ cleanup();
261
+ process.exit(0);
262
+ }
263
+ });
264
+ }
265
+ function handleCliRpcRequest(clientWs, msg) {
266
+ const clientId = msg.id;
267
+ if (isRateLimited(clientWs)) {
268
+ clientWs.send(
269
+ JSON.stringify({
270
+ id: clientId,
271
+ type: "rpc_response",
272
+ success: false,
273
+ error: {
274
+ code: -32005,
275
+ message: "Rate limited. Too many requests \u2014 try again shortly."
276
+ }
277
+ })
278
+ );
279
+ return;
280
+ }
281
+ if (pendingRequests.size >= MAX_PENDING_REQUESTS) {
282
+ clientWs.send(
283
+ JSON.stringify({
284
+ id: clientId,
285
+ type: "rpc_response",
286
+ success: false,
287
+ error: {
288
+ code: -32006,
289
+ message: "Too many pending requests. Wait for current requests to complete."
290
+ }
291
+ })
292
+ );
293
+ return;
294
+ }
295
+ if (!browserReady || !browserWs || browserWs.readyState !== WebSocket.OPEN) {
296
+ reopenBrowser();
297
+ clientWs.send(
298
+ JSON.stringify({
299
+ id: clientId,
300
+ type: "rpc_response",
301
+ success: false,
302
+ error: {
303
+ code: -32001,
304
+ message: "Browser tab was closed. Reopening \u2014 please retry in a few seconds."
305
+ }
306
+ })
307
+ );
308
+ return;
309
+ }
310
+ const daemonId = crypto.randomUUID();
311
+ const timer = setTimeout(() => {
312
+ pendingRequests.delete(daemonId);
313
+ if (clientWs.readyState === WebSocket.OPEN) {
314
+ clientWs.send(
315
+ JSON.stringify({
316
+ id: clientId,
317
+ type: "rpc_response",
318
+ success: false,
319
+ error: {
320
+ code: -32e3,
321
+ message: `Request timed out after ${timeout / 1e3}s`
322
+ }
323
+ })
324
+ );
325
+ }
326
+ }, timeout);
327
+ pendingRequests.set(daemonId, { clientWs, clientId, timer });
328
+ browserWs.send(
329
+ JSON.stringify({
330
+ id: daemonId,
331
+ type: "rpc_request",
332
+ method: msg.method,
333
+ params: msg.params
334
+ })
335
+ );
336
+ }
337
+ function buildBridgeUrl(port) {
338
+ const url = new URL("/cli-bridge", args.keysUrl);
339
+ url.searchParams.set("wsPort", String(port));
340
+ url.hash = `token=${encodeURIComponent(token)}`;
341
+ return url.toString();
342
+ }
343
+ //# sourceMappingURL=ws-daemon.js.map
344
+ //# sourceMappingURL=ws-daemon.js.map