@metamask-previews/wallet-cli 0.0.0-preview-2a256aff2 → 0.0.0-preview-9de9aa4

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 (64) hide show
  1. package/CHANGELOG.md +1 -2
  2. package/dist/daemon/daemon-client.cjs +140 -0
  3. package/dist/daemon/daemon-client.cjs.map +1 -0
  4. package/dist/daemon/daemon-client.d.cts +79 -0
  5. package/dist/daemon/daemon-client.d.cts.map +1 -0
  6. package/dist/daemon/daemon-client.d.mts +79 -0
  7. package/dist/daemon/daemon-client.d.mts.map +1 -0
  8. package/dist/daemon/daemon-client.mjs +135 -0
  9. package/dist/daemon/daemon-client.mjs.map +1 -0
  10. package/dist/daemon/daemon-spawn.cjs +106 -0
  11. package/dist/daemon/daemon-spawn.cjs.map +1 -0
  12. package/dist/daemon/daemon-spawn.d.cts +28 -0
  13. package/dist/daemon/daemon-spawn.d.cts.map +1 -0
  14. package/dist/daemon/daemon-spawn.d.mts +28 -0
  15. package/dist/daemon/daemon-spawn.d.mts.map +1 -0
  16. package/dist/daemon/daemon-spawn.mjs +102 -0
  17. package/dist/daemon/daemon-spawn.mjs.map +1 -0
  18. package/dist/daemon/prompts.cjs +21 -0
  19. package/dist/daemon/prompts.cjs.map +1 -0
  20. package/dist/daemon/prompts.d.cts +11 -0
  21. package/dist/daemon/prompts.d.cts.map +1 -0
  22. package/dist/daemon/prompts.d.mts +11 -0
  23. package/dist/daemon/prompts.d.mts.map +1 -0
  24. package/dist/daemon/prompts.mjs +17 -0
  25. package/dist/daemon/prompts.mjs.map +1 -0
  26. package/dist/daemon/rpc-socket-server.cjs +271 -0
  27. package/dist/daemon/rpc-socket-server.cjs.map +1 -0
  28. package/dist/daemon/rpc-socket-server.d.cts +44 -0
  29. package/dist/daemon/rpc-socket-server.d.cts.map +1 -0
  30. package/dist/daemon/rpc-socket-server.d.mts +44 -0
  31. package/dist/daemon/rpc-socket-server.d.mts.map +1 -0
  32. package/dist/daemon/rpc-socket-server.mjs +267 -0
  33. package/dist/daemon/rpc-socket-server.mjs.map +1 -0
  34. package/dist/daemon/socket-line.cjs +89 -0
  35. package/dist/daemon/socket-line.cjs.map +1 -0
  36. package/dist/daemon/socket-line.d.cts +19 -0
  37. package/dist/daemon/socket-line.d.cts.map +1 -0
  38. package/dist/daemon/socket-line.d.mts +19 -0
  39. package/dist/daemon/socket-line.d.mts.map +1 -0
  40. package/dist/daemon/socket-line.mjs +84 -0
  41. package/dist/daemon/socket-line.mjs.map +1 -0
  42. package/dist/daemon/stop-daemon.cjs +103 -0
  43. package/dist/daemon/stop-daemon.cjs.map +1 -0
  44. package/dist/daemon/stop-daemon.d.cts +18 -0
  45. package/dist/daemon/stop-daemon.d.cts.map +1 -0
  46. package/dist/daemon/stop-daemon.d.mts +18 -0
  47. package/dist/daemon/stop-daemon.d.mts.map +1 -0
  48. package/dist/daemon/stop-daemon.mjs +99 -0
  49. package/dist/daemon/stop-daemon.mjs.map +1 -0
  50. package/dist/daemon/types.cjs.map +1 -1
  51. package/dist/daemon/types.d.cts +28 -0
  52. package/dist/daemon/types.d.cts.map +1 -1
  53. package/dist/daemon/types.d.mts +28 -0
  54. package/dist/daemon/types.d.mts.map +1 -1
  55. package/dist/daemon/types.mjs.map +1 -1
  56. package/dist/daemon/utils.cjs +109 -0
  57. package/dist/daemon/utils.cjs.map +1 -0
  58. package/dist/daemon/utils.d.cts +50 -0
  59. package/dist/daemon/utils.d.cts.map +1 -0
  60. package/dist/daemon/utils.d.mts +50 -0
  61. package/dist/daemon/utils.d.mts.map +1 -0
  62. package/dist/daemon/utils.mjs +101 -0
  63. package/dist/daemon/utils.mjs.map +1 -0
  64. package/package.json +3 -1
@@ -0,0 +1,28 @@
1
+ import type { DaemonSpawnConfig } from "./types.cjs";
2
+ /**
3
+ * Outcome of {@link ensureDaemon}.
4
+ *
5
+ * - `'already-running'`: a responsive daemon was found at the configured
6
+ * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)
7
+ * were NOT applied to that daemon; the caller should surface this so a
8
+ * user who is trying to change them isn't silently ignored.
9
+ * - `'started'`: a new daemon was spawned and is now responsive.
10
+ */
11
+ export type EnsureDaemonResult = {
12
+ state: 'already-running' | 'started';
13
+ socketPath: string;
14
+ };
15
+ /**
16
+ * Ensure the daemon is running. If a responsive daemon already exists, return
17
+ * `'already-running'` (caller decides how to surface that). Otherwise spawn
18
+ * one as a detached process and wait until the socket becomes responsive.
19
+ *
20
+ * Refuses to spawn when pinging the existing socket fails with anything other
21
+ * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the
22
+ * existing process and corrupt its PID file.
23
+ *
24
+ * @param config - Spawn configuration.
25
+ * @returns The state of the daemon and the socket path it's listening on.
26
+ */
27
+ export declare function ensureDaemon(config: DaemonSpawnConfig): Promise<EnsureDaemonResult>;
28
+ //# sourceMappingURL=daemon-spawn.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-spawn.d.cts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAgF7B"}
@@ -0,0 +1,28 @@
1
+ import type { DaemonSpawnConfig } from "./types.mjs";
2
+ /**
3
+ * Outcome of {@link ensureDaemon}.
4
+ *
5
+ * - `'already-running'`: a responsive daemon was found at the configured
6
+ * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)
7
+ * were NOT applied to that daemon; the caller should surface this so a
8
+ * user who is trying to change them isn't silently ignored.
9
+ * - `'started'`: a new daemon was spawned and is now responsive.
10
+ */
11
+ export type EnsureDaemonResult = {
12
+ state: 'already-running' | 'started';
13
+ socketPath: string;
14
+ };
15
+ /**
16
+ * Ensure the daemon is running. If a responsive daemon already exists, return
17
+ * `'already-running'` (caller decides how to surface that). Otherwise spawn
18
+ * one as a detached process and wait until the socket becomes responsive.
19
+ *
20
+ * Refuses to spawn when pinging the existing socket fails with anything other
21
+ * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the
22
+ * existing process and corrupt its PID file.
23
+ *
24
+ * @param config - Spawn configuration.
25
+ * @returns The state of the daemon and the socket path it's listening on.
26
+ */
27
+ export declare function ensureDaemon(config: DaemonSpawnConfig): Promise<EnsureDaemonResult>;
28
+ //# sourceMappingURL=daemon-spawn.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-spawn.d.mts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAgF7B"}
@@ -0,0 +1,102 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { pingDaemon } from "./daemon-client.mjs";
5
+ import { getDaemonPaths } from "./paths.mjs";
6
+ const POLL_INTERVAL_MS = 100;
7
+ const MAX_POLLS = 300; // 30 seconds
8
+ /**
9
+ * Ensure the daemon is running. If a responsive daemon already exists, return
10
+ * `'already-running'` (caller decides how to surface that). Otherwise spawn
11
+ * one as a detached process and wait until the socket becomes responsive.
12
+ *
13
+ * Refuses to spawn when pinging the existing socket fails with anything other
14
+ * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the
15
+ * existing process and corrupt its PID file.
16
+ *
17
+ * @param config - Spawn configuration.
18
+ * @returns The state of the daemon and the socket path it's listening on.
19
+ */
20
+ export async function ensureDaemon(config) {
21
+ const { socketPath } = getDaemonPaths(config.dataDir);
22
+ const initialPing = await pingDaemon(socketPath);
23
+ if (initialPing.status === 'responsive') {
24
+ return { state: 'already-running', socketPath };
25
+ }
26
+ if (initialPing.status === 'unreachable') {
27
+ if (initialPing.reason === 'permission') {
28
+ throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
29
+ `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
30
+ `(${initialPing.error.message})`);
31
+ }
32
+ throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
33
+ `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
34
+ `(${initialPing.error.message})`);
35
+ }
36
+ process.stderr.write('Starting daemon...\n');
37
+ const { entryPath, args } = resolveEntryPoint(config.packageRoot);
38
+ const child = spawn(process.execPath, [...args, entryPath], {
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ env: {
42
+ ...process.env,
43
+ MM_DAEMON_DATA_DIR: config.dataDir,
44
+ MM_DAEMON_SOCKET_PATH: socketPath,
45
+ INFURA_PROJECT_ID: config.infuraProjectId,
46
+ MM_WALLET_PASSWORD: config.password,
47
+ MM_WALLET_SRP: config.srp,
48
+ },
49
+ });
50
+ const exitInfo = { value: null };
51
+ // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may
52
+ // never emit 'exit'. Capture it so the readiness loop can surface the real
53
+ // cause immediately instead of hanging for the full timeout.
54
+ const spawnError = { value: null };
55
+ child.on('error', (error) => {
56
+ process.stderr.write(`Failed to spawn daemon process: ${String(error)}\n`);
57
+ spawnError.value = error;
58
+ });
59
+ child.on('exit', (code, signal) => {
60
+ exitInfo.value = { code, signal };
61
+ });
62
+ child.unref();
63
+ for (let i = 0; i < MAX_POLLS; i++) {
64
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
65
+ if (spawnError.value !== null) {
66
+ throw new Error(`Failed to spawn daemon process: ${spawnError.value.message}. ` +
67
+ `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`);
68
+ }
69
+ if (exitInfo.value !== null) {
70
+ const { code, signal } = exitInfo.value;
71
+ throw new Error(`Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +
72
+ `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`);
73
+ }
74
+ const ping = await pingDaemon(socketPath);
75
+ if (ping.status === 'responsive') {
76
+ process.stderr.write('Daemon ready.\n');
77
+ return { state: 'started', socketPath };
78
+ }
79
+ }
80
+ throw new Error(`Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`);
81
+ }
82
+ /**
83
+ * Resolve the daemon entry point path and any extra Node.js args needed.
84
+ *
85
+ * In production, uses the compiled dist output. In development, uses tsx
86
+ * to run TypeScript source directly.
87
+ *
88
+ * @param packageRoot - The root directory of the wallet-cli package.
89
+ * @returns The entry path and any extra node args.
90
+ */
91
+ function resolveEntryPoint(packageRoot) {
92
+ const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');
93
+ if (existsSync(distEntry)) {
94
+ return { entryPath: distEntry, args: [] };
95
+ }
96
+ const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');
97
+ return {
98
+ entryPath: srcEntry,
99
+ args: ['--import', 'tsx'],
100
+ };
101
+ }
102
+ //# sourceMappingURL=daemon-spawn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-spawn.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n if (initialPing.status === 'responsive') {\n return { state: 'already-running', socketPath };\n }\n if (initialPing.status === 'unreachable') {\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: 'ignore',\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n\n type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };\n const exitInfo: { value: ExitInfo | null } = { value: null };\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit'. Capture it so the readiness loop can surface the real\n // cause immediately instead of hanging for the full timeout.\n const spawnError: { value: Error | null } = { value: null };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n spawnError.value = error;\n });\n child.on('exit', (code, signal) => {\n exitInfo.value = { code, signal };\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n if (spawnError.value !== null) {\n throw new Error(\n `Failed to spawn daemon process: ${spawnError.value.message}. ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n if (exitInfo.value !== null) {\n const { code, signal } = exitInfo.value;\n throw new Error(\n `Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.confirmPurge = void 0;
4
+ /**
5
+ * Ask the user to confirm the destructive `daemon purge` operation.
6
+ *
7
+ * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled
8
+ * package can interop with that ESM-only dependency, and so tests can mock
9
+ * the prompt without going through jest's ESM mock machinery.
10
+ *
11
+ * @returns True if the user confirmed.
12
+ */
13
+ async function confirmPurge() {
14
+ const { default: confirm } = await import("@inquirer/confirm");
15
+ return confirm({
16
+ message: 'This will stop the daemon and delete all state. Continue?',
17
+ default: false,
18
+ });
19
+ }
20
+ exports.confirmPurge = confirmPurge;
21
+ //# sourceMappingURL=prompts.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.cjs","sourceRoot":"","sources":["../../src/daemon/prompts.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACI,KAAK,UAAU,YAAY;IAChC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,qBAAqB,CAAC;IAC/D,OAAO,OAAO,CAAC;QACb,OAAO,EAAE,2DAA2D;QACpE,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;AACL,CAAC;AAND,oCAMC","sourcesContent":["/**\n * Ask the user to confirm the destructive `daemon purge` operation.\n *\n * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled\n * package can interop with that ESM-only dependency, and so tests can mock\n * the prompt without going through jest's ESM mock machinery.\n *\n * @returns True if the user confirmed.\n */\nexport async function confirmPurge(): Promise<boolean> {\n const { default: confirm } = await import('@inquirer/confirm');\n return confirm({\n message: 'This will stop the daemon and delete all state. Continue?',\n default: false,\n });\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ask the user to confirm the destructive `daemon purge` operation.
3
+ *
4
+ * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled
5
+ * package can interop with that ESM-only dependency, and so tests can mock
6
+ * the prompt without going through jest's ESM mock machinery.
7
+ *
8
+ * @returns True if the user confirmed.
9
+ */
10
+ export declare function confirmPurge(): Promise<boolean>;
11
+ //# sourceMappingURL=prompts.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.cts","sourceRoot":"","sources":["../../src/daemon/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAMrD"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ask the user to confirm the destructive `daemon purge` operation.
3
+ *
4
+ * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled
5
+ * package can interop with that ESM-only dependency, and so tests can mock
6
+ * the prompt without going through jest's ESM mock machinery.
7
+ *
8
+ * @returns True if the user confirmed.
9
+ */
10
+ export declare function confirmPurge(): Promise<boolean>;
11
+ //# sourceMappingURL=prompts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.mts","sourceRoot":"","sources":["../../src/daemon/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAMrD"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Ask the user to confirm the destructive `daemon purge` operation.
3
+ *
4
+ * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled
5
+ * package can interop with that ESM-only dependency, and so tests can mock
6
+ * the prompt without going through jest's ESM mock machinery.
7
+ *
8
+ * @returns True if the user confirmed.
9
+ */
10
+ export async function confirmPurge() {
11
+ const { default: confirm } = await import("@inquirer/confirm");
12
+ return confirm({
13
+ message: 'This will stop the daemon and delete all state. Continue?',
14
+ default: false,
15
+ });
16
+ }
17
+ //# sourceMappingURL=prompts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.mjs","sourceRoot":"","sources":["../../src/daemon/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,qBAAqB,CAAC;IAC/D,OAAO,OAAO,CAAC;QACb,OAAO,EAAE,2DAA2D;QACpE,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Ask the user to confirm the destructive `daemon purge` operation.\n *\n * Wraps `@inquirer/confirm` in a dynamic import so this CommonJS-compiled\n * package can interop with that ESM-only dependency, and so tests can mock\n * the prompt without going through jest's ESM mock machinery.\n *\n * @returns True if the user confirmed.\n */\nexport async function confirmPurge(): Promise<boolean> {\n const { default: confirm } = await import('@inquirer/confirm');\n return confirm({\n message: 'This will stop the daemon and delete all state. Continue?',\n default: false,\n });\n}\n"]}
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startRpcSocketServer = void 0;
4
+ const rpc_errors_1 = require("@metamask/rpc-errors");
5
+ const utils_1 = require("@metamask/utils");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_net_1 = require("node:net");
8
+ const utils_2 = require("./utils.cjs");
9
+ const CONNECTION_TIMEOUT_MS = 30000;
10
+ /**
11
+ * Start a Unix socket server that processes JSON-RPC requests.
12
+ *
13
+ * Each connection reads one newline-delimited JSON-RPC request, processes it
14
+ * via the provided handler map, writes a JSON-RPC response, and closes.
15
+ *
16
+ * The special `shutdown` method is intercepted before handler dispatch and
17
+ * triggers the provided {@link StartRpcSocketServerOptions.onShutdown} callback
18
+ * after responding.
19
+ *
20
+ * @param options - Server options.
21
+ * @param options.socketPath - The Unix socket path to listen on.
22
+ * @param options.handlers - Map of RPC method names to handler functions.
23
+ * @param options.onShutdown - Optional callback invoked when a `shutdown` RPC is received.
24
+ * @param options.log - Optional logger for server-side diagnostics.
25
+ * @returns A handle with a `close()` function for cleanup.
26
+ */
27
+ async function startRpcSocketServer({ socketPath, handlers, onShutdown, log, }) {
28
+ const logFn = log ?? defaultLog;
29
+ const server = (0, node_net_1.createServer)((socket) => {
30
+ let buffer = '';
31
+ // Destroy connections that never send a complete request line. `unref` so
32
+ // the timer alone cannot keep the event loop alive at shutdown.
33
+ const timer = setTimeout(() => {
34
+ socket.destroy();
35
+ }, CONNECTION_TIMEOUT_MS);
36
+ timer.unref();
37
+ /**
38
+ * Clear the idle-connection timer. Called from data, close, and error
39
+ * paths so the timer never outlives the connection itself.
40
+ */
41
+ const clearIdleTimer = () => {
42
+ clearTimeout(timer);
43
+ };
44
+ const onData = (data) => {
45
+ buffer += data.toString();
46
+ const idx = buffer.indexOf('\n');
47
+ if (idx === -1) {
48
+ return;
49
+ }
50
+ clearIdleTimer();
51
+ // One request per connection.
52
+ socket.removeListener('data', onData);
53
+ const line = buffer.slice(0, idx);
54
+ const remaining = buffer.slice(idx + 1);
55
+ buffer = '';
56
+ if (remaining.length > 0) {
57
+ socket.end(`${JSON.stringify({
58
+ jsonrpc: '2.0',
59
+ error: rpc_errors_1.rpcErrors
60
+ .invalidRequest({
61
+ message: 'Only one request per connection is allowed',
62
+ })
63
+ .serialize(),
64
+ })}\n`);
65
+ return;
66
+ }
67
+ handleRequest(handlers, line, onShutdown, logFn)
68
+ .then((response) => {
69
+ socket.end(`${JSON.stringify(response)}\n`);
70
+ return undefined;
71
+ })
72
+ .catch((dispatchError) => {
73
+ logFn(`Unhandled RPC dispatch error: ${String(dispatchError)}`);
74
+ socket.end(`${JSON.stringify({
75
+ jsonrpc: '2.0',
76
+ error: rpc_errors_1.rpcErrors
77
+ .internal({ message: 'Internal error' })
78
+ .serialize(),
79
+ })}\n`);
80
+ });
81
+ };
82
+ socket.on('data', onData);
83
+ socket.once('close', clearIdleTimer);
84
+ socket.on('error', (socketError) => {
85
+ clearIdleTimer();
86
+ const { code } = socketError;
87
+ if (code === 'EPIPE' || code === 'ECONNRESET') {
88
+ return; // Expected during probe/disconnect.
89
+ }
90
+ logFn(`Unexpected socket error: ${String(socketError)}`);
91
+ });
92
+ });
93
+ await listen(server, socketPath);
94
+ return {
95
+ close: async () => closeServer(server),
96
+ };
97
+ }
98
+ exports.startRpcSocketServer = startRpcSocketServer;
99
+ /**
100
+ * Default fallback logger: writes to stderr. Daemons spawned with
101
+ * `stdio: 'ignore'` should always pass an explicit `log`.
102
+ *
103
+ * @param message - The message to log.
104
+ */
105
+ function defaultLog(message) {
106
+ process.stderr.write(`${message}\n`);
107
+ }
108
+ /**
109
+ * Handle a single JSON-RPC request line, intercepting the `shutdown` method.
110
+ *
111
+ * @param handlers - The RPC handler map.
112
+ * @param line - The raw JSON line from the socket.
113
+ * @param onShutdown - Optional shutdown callback.
114
+ * @param log - Logger for diagnostic messages.
115
+ * @returns A JSON-RPC response object.
116
+ */
117
+ async function handleRequest(handlers, line, onShutdown, log) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(line);
121
+ }
122
+ catch {
123
+ return {
124
+ jsonrpc: '2.0',
125
+ id: null,
126
+ error: rpc_errors_1.rpcErrors.parse({ message: 'Parse error' }).serialize(),
127
+ };
128
+ }
129
+ if (!(0, utils_1.isJsonRpcRequest)(parsed)) {
130
+ const id = typeof parsed === 'object' &&
131
+ parsed !== null &&
132
+ (0, utils_1.hasProperty)(parsed, 'id') &&
133
+ isValidJsonRpcId(parsed.id)
134
+ ? parsed.id
135
+ : null;
136
+ return {
137
+ jsonrpc: '2.0',
138
+ id,
139
+ error: rpc_errors_1.rpcErrors
140
+ .invalidRequest({ message: 'Invalid JSON-RPC request' })
141
+ .serialize(),
142
+ };
143
+ }
144
+ const { id, method, params } = parsed;
145
+ try {
146
+ if (method === 'shutdown') {
147
+ if (onShutdown) {
148
+ setTimeout(() => {
149
+ onShutdown().catch((error) => {
150
+ log(`onShutdown callback failed: ${String(error)}`);
151
+ });
152
+ }, 0);
153
+ }
154
+ return { jsonrpc: '2.0', id, result: { status: 'shutting down' } };
155
+ }
156
+ const handler = Object.prototype.hasOwnProperty.call(handlers, method)
157
+ ? handlers[method]
158
+ : undefined;
159
+ if (!handler) {
160
+ return {
161
+ jsonrpc: '2.0',
162
+ id,
163
+ error: rpc_errors_1.rpcErrors
164
+ .methodNotFound({ message: `Method not found: ${method}` })
165
+ .serialize(),
166
+ };
167
+ }
168
+ const result = await handler(coerceHandlerParams(params));
169
+ return { jsonrpc: '2.0', id, result: result ?? null };
170
+ }
171
+ catch (error) {
172
+ log(`RPC handler "${method}" failed: ${String(error)}`);
173
+ if (isRpcError(error)) {
174
+ return { jsonrpc: '2.0', id, error };
175
+ }
176
+ const message = error instanceof Error ? error.message : 'Internal error';
177
+ return {
178
+ jsonrpc: '2.0',
179
+ id,
180
+ error: rpc_errors_1.rpcErrors.internal({ message }).serialize(),
181
+ };
182
+ }
183
+ }
184
+ /**
185
+ * Narrow `params` to the shape handlers expect. JSON-RPC 2.0 requires
186
+ * `params`, when present, to be an array or object; both are valid `Json`.
187
+ *
188
+ * @param params - The validated `params` field from a JSON-RPC request.
189
+ * @returns The same value, or `null` when absent.
190
+ */
191
+ function coerceHandlerParams(params) {
192
+ return params ?? null;
193
+ }
194
+ /**
195
+ * Per JSON-RPC 2.0, `id` must be a string, number, or null. Used when
196
+ * salvaging an `id` from a parse-success-but-not-valid-request payload.
197
+ *
198
+ * @param value - The candidate id.
199
+ * @returns True if the value is an acceptable JSON-RPC id.
200
+ */
201
+ function isValidJsonRpcId(value) {
202
+ return (value === null || typeof value === 'string' || typeof value === 'number');
203
+ }
204
+ /**
205
+ * Check if an error is an RPC error with a numeric code.
206
+ *
207
+ * @param error - The error to check.
208
+ * @returns True if the error has a numeric code property.
209
+ */
210
+ function isRpcError(error) {
211
+ return (typeof error === 'object' &&
212
+ error !== null &&
213
+ (0, utils_1.hasProperty)(error, 'code') &&
214
+ typeof error.code === 'number' &&
215
+ (0, utils_1.hasProperty)(error, 'message') &&
216
+ typeof error.message === 'string');
217
+ }
218
+ /**
219
+ * Start listening on a Unix socket path, removing any stale socket file.
220
+ *
221
+ * @param server - The net.Server instance.
222
+ * @param socketPath - The Unix socket path.
223
+ */
224
+ async function listen(server, socketPath) {
225
+ try {
226
+ await (0, promises_1.unlink)(socketPath);
227
+ }
228
+ catch (error) {
229
+ if (!(0, utils_2.isErrorWithCode)(error, 'ENOENT')) {
230
+ throw error;
231
+ }
232
+ }
233
+ await new Promise((resolve, reject) => {
234
+ server.on('error', reject);
235
+ server.listen(socketPath, () => {
236
+ server.removeListener('error', reject);
237
+ resolve();
238
+ });
239
+ });
240
+ // Restrict the socket to its owner. The daemon hosts an unlocked wallet, so
241
+ // a world-connectable socket would let any local user drive it. listen()
242
+ // creates the socket with umask-derived (typically world-accessible) perms.
243
+ try {
244
+ await (0, promises_1.chmod)(socketPath, 0o600);
245
+ }
246
+ catch (error) {
247
+ // Never leave a possibly world-accessible socket listening with no handle
248
+ // to close it. Cleanup is best-effort so the chmod failure still surfaces.
249
+ await closeServer(server).catch(() => undefined);
250
+ await (0, promises_1.unlink)(socketPath).catch(() => undefined);
251
+ throw error;
252
+ }
253
+ }
254
+ /**
255
+ * Close a server, resolving once it stops accepting connections.
256
+ *
257
+ * @param server - The net.Server instance.
258
+ */
259
+ async function closeServer(server) {
260
+ await new Promise((resolve, reject) => {
261
+ server.close((error) => {
262
+ if (error) {
263
+ reject(error);
264
+ }
265
+ else {
266
+ resolve();
267
+ }
268
+ });
269
+ });
270
+ }
271
+ //# sourceMappingURL=rpc-socket-server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-socket-server.cjs","sourceRoot":"","sources":["../../src/daemon/rpc-socket-server.ts"],"names":[],"mappings":"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","sourcesContent":["import { rpcErrors } from '@metamask/rpc-errors';\nimport type {\n JsonRpcId,\n JsonRpcParams,\n JsonRpcResponse,\n} from '@metamask/utils';\nimport { hasProperty, isJsonRpcRequest } from '@metamask/utils';\nimport { chmod, unlink } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport type { Server } from 'node:net';\n\nimport type { RpcHandlerMap } from './types';\nimport { isErrorWithCode } from './utils';\n\nconst CONNECTION_TIMEOUT_MS = 30_000;\n\n/**\n * Handle returned by {@link startRpcSocketServer}.\n */\nexport type RpcSocketServerHandle = {\n close: () => Promise<void>;\n};\n\n/**\n * Options for {@link startRpcSocketServer}.\n */\nexport type StartRpcSocketServerOptions = {\n /** The Unix socket path to listen on. */\n socketPath: string;\n /** Map of RPC method names to handler functions. */\n handlers: RpcHandlerMap;\n /** Callback invoked when a `shutdown` RPC is received. */\n onShutdown?: (() => Promise<void>) | undefined;\n /**\n * Optional logger for server-side diagnostics (unexpected socket errors,\n * unhandled handler rejections, `onShutdown` callback failures). Without\n * this, failures fall back to `process.stderr.write`, which is discarded\n * when the daemon is spawned with `stdio: 'ignore'`.\n */\n log?: ((message: string) => void) | undefined;\n};\n\n/**\n * Start a Unix socket server that processes JSON-RPC requests.\n *\n * Each connection reads one newline-delimited JSON-RPC request, processes it\n * via the provided handler map, writes a JSON-RPC response, and closes.\n *\n * The special `shutdown` method is intercepted before handler dispatch and\n * triggers the provided {@link StartRpcSocketServerOptions.onShutdown} callback\n * after responding.\n *\n * @param options - Server options.\n * @param options.socketPath - The Unix socket path to listen on.\n * @param options.handlers - Map of RPC method names to handler functions.\n * @param options.onShutdown - Optional callback invoked when a `shutdown` RPC is received.\n * @param options.log - Optional logger for server-side diagnostics.\n * @returns A handle with a `close()` function for cleanup.\n */\nexport async function startRpcSocketServer({\n socketPath,\n handlers,\n onShutdown,\n log,\n}: StartRpcSocketServerOptions): Promise<RpcSocketServerHandle> {\n const logFn = log ?? defaultLog;\n\n const server = createServer((socket) => {\n let buffer = '';\n\n // Destroy connections that never send a complete request line. `unref` so\n // the timer alone cannot keep the event loop alive at shutdown.\n const timer = setTimeout(() => {\n socket.destroy();\n }, CONNECTION_TIMEOUT_MS);\n timer.unref();\n\n /**\n * Clear the idle-connection timer. Called from data, close, and error\n * paths so the timer never outlives the connection itself.\n */\n const clearIdleTimer = (): void => {\n clearTimeout(timer);\n };\n\n const onData = (data: Buffer): void => {\n buffer += data.toString();\n const idx = buffer.indexOf('\\n');\n if (idx === -1) {\n return;\n }\n\n clearIdleTimer();\n\n // One request per connection.\n socket.removeListener('data', onData);\n\n const line = buffer.slice(0, idx);\n const remaining = buffer.slice(idx + 1);\n buffer = '';\n\n if (remaining.length > 0) {\n socket.end(\n `${JSON.stringify({\n jsonrpc: '2.0',\n error: rpcErrors\n .invalidRequest({\n message: 'Only one request per connection is allowed',\n })\n .serialize(),\n })}\\n`,\n );\n return;\n }\n\n handleRequest(handlers, line, onShutdown, logFn)\n .then((response) => {\n socket.end(`${JSON.stringify(response)}\\n`);\n return undefined;\n })\n .catch((dispatchError: unknown) => {\n logFn(`Unhandled RPC dispatch error: ${String(dispatchError)}`);\n socket.end(\n `${JSON.stringify({\n jsonrpc: '2.0',\n error: rpcErrors\n .internal({ message: 'Internal error' })\n .serialize(),\n })}\\n`,\n );\n });\n };\n socket.on('data', onData);\n socket.once('close', clearIdleTimer);\n socket.on('error', (socketError: NodeJS.ErrnoException) => {\n clearIdleTimer();\n const { code } = socketError;\n if (code === 'EPIPE' || code === 'ECONNRESET') {\n return; // Expected during probe/disconnect.\n }\n logFn(`Unexpected socket error: ${String(socketError)}`);\n });\n });\n\n await listen(server, socketPath);\n\n return {\n close: async (): Promise<void> => closeServer(server),\n };\n}\n\n/**\n * Default fallback logger: writes to stderr. Daemons spawned with\n * `stdio: 'ignore'` should always pass an explicit `log`.\n *\n * @param message - The message to log.\n */\nfunction defaultLog(message: string): void {\n process.stderr.write(`${message}\\n`);\n}\n\n/**\n * Handle a single JSON-RPC request line, intercepting the `shutdown` method.\n *\n * @param handlers - The RPC handler map.\n * @param line - The raw JSON line from the socket.\n * @param onShutdown - Optional shutdown callback.\n * @param log - Logger for diagnostic messages.\n * @returns A JSON-RPC response object.\n */\nasync function handleRequest(\n handlers: RpcHandlerMap,\n line: string,\n onShutdown: (() => Promise<void>) | undefined,\n log: (message: string) => void,\n): Promise<JsonRpcResponse> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return {\n jsonrpc: '2.0',\n id: null,\n error: rpcErrors.parse({ message: 'Parse error' }).serialize(),\n };\n }\n\n if (!isJsonRpcRequest(parsed)) {\n const id: JsonRpcId =\n typeof parsed === 'object' &&\n parsed !== null &&\n hasProperty(parsed, 'id') &&\n isValidJsonRpcId(parsed.id)\n ? parsed.id\n : null;\n return {\n jsonrpc: '2.0',\n id,\n error: rpcErrors\n .invalidRequest({ message: 'Invalid JSON-RPC request' })\n .serialize(),\n };\n }\n\n const { id, method, params } = parsed;\n\n try {\n if (method === 'shutdown') {\n if (onShutdown) {\n setTimeout(() => {\n onShutdown().catch((error: unknown) => {\n log(`onShutdown callback failed: ${String(error)}`);\n });\n }, 0);\n }\n return { jsonrpc: '2.0', id, result: { status: 'shutting down' } };\n }\n\n const handler = Object.prototype.hasOwnProperty.call(handlers, method)\n ? handlers[method]\n : undefined;\n if (!handler) {\n return {\n jsonrpc: '2.0',\n id,\n error: rpcErrors\n .methodNotFound({ message: `Method not found: ${method}` })\n .serialize(),\n };\n }\n\n const result = await handler(coerceHandlerParams(params));\n return { jsonrpc: '2.0', id, result: result ?? null };\n } catch (error) {\n log(`RPC handler \"${method}\" failed: ${String(error)}`);\n if (isRpcError(error)) {\n return { jsonrpc: '2.0', id, error };\n }\n const message = error instanceof Error ? error.message : 'Internal error';\n return {\n jsonrpc: '2.0',\n id,\n error: rpcErrors.internal({ message }).serialize(),\n };\n }\n}\n\n/**\n * Narrow `params` to the shape handlers expect. JSON-RPC 2.0 requires\n * `params`, when present, to be an array or object; both are valid `Json`.\n *\n * @param params - The validated `params` field from a JSON-RPC request.\n * @returns The same value, or `null` when absent.\n */\nfunction coerceHandlerParams(\n params: JsonRpcParams | undefined,\n): JsonRpcParams | null {\n return params ?? null;\n}\n\n/**\n * Per JSON-RPC 2.0, `id` must be a string, number, or null. Used when\n * salvaging an `id` from a parse-success-but-not-valid-request payload.\n *\n * @param value - The candidate id.\n * @returns True if the value is an acceptable JSON-RPC id.\n */\nfunction isValidJsonRpcId(value: unknown): value is JsonRpcId {\n return (\n value === null || typeof value === 'string' || typeof value === 'number'\n );\n}\n\n/**\n * Check if an error is an RPC error with a numeric code.\n *\n * @param error - The error to check.\n * @returns True if the error has a numeric code property.\n */\nfunction isRpcError(\n error: unknown,\n): error is { code: number; message: string } {\n return (\n typeof error === 'object' &&\n error !== null &&\n hasProperty(error, 'code') &&\n typeof error.code === 'number' &&\n hasProperty(error, 'message') &&\n typeof error.message === 'string'\n );\n}\n\n/**\n * Start listening on a Unix socket path, removing any stale socket file.\n *\n * @param server - The net.Server instance.\n * @param socketPath - The Unix socket path.\n */\nasync function listen(server: Server, socketPath: string): Promise<void> {\n try {\n await unlink(socketPath);\n } catch (error) {\n if (!isErrorWithCode(error, 'ENOENT')) {\n throw error;\n }\n }\n\n await new Promise<void>((resolve, reject) => {\n server.on('error', reject);\n server.listen(socketPath, () => {\n server.removeListener('error', reject);\n resolve();\n });\n });\n\n // Restrict the socket to its owner. The daemon hosts an unlocked wallet, so\n // a world-connectable socket would let any local user drive it. listen()\n // creates the socket with umask-derived (typically world-accessible) perms.\n try {\n await chmod(socketPath, 0o600);\n } catch (error) {\n // Never leave a possibly world-accessible socket listening with no handle\n // to close it. Cleanup is best-effort so the chmod failure still surfaces.\n await closeServer(server).catch(() => undefined);\n await unlink(socketPath).catch(() => undefined);\n throw error;\n }\n}\n\n/**\n * Close a server, resolving once it stops accepting connections.\n *\n * @param server - The net.Server instance.\n */\nasync function closeServer(server: Server): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import type { RpcHandlerMap } from "./types.cjs";
2
+ /**
3
+ * Handle returned by {@link startRpcSocketServer}.
4
+ */
5
+ export type RpcSocketServerHandle = {
6
+ close: () => Promise<void>;
7
+ };
8
+ /**
9
+ * Options for {@link startRpcSocketServer}.
10
+ */
11
+ export type StartRpcSocketServerOptions = {
12
+ /** The Unix socket path to listen on. */
13
+ socketPath: string;
14
+ /** Map of RPC method names to handler functions. */
15
+ handlers: RpcHandlerMap;
16
+ /** Callback invoked when a `shutdown` RPC is received. */
17
+ onShutdown?: (() => Promise<void>) | undefined;
18
+ /**
19
+ * Optional logger for server-side diagnostics (unexpected socket errors,
20
+ * unhandled handler rejections, `onShutdown` callback failures). Without
21
+ * this, failures fall back to `process.stderr.write`, which is discarded
22
+ * when the daemon is spawned with `stdio: 'ignore'`.
23
+ */
24
+ log?: ((message: string) => void) | undefined;
25
+ };
26
+ /**
27
+ * Start a Unix socket server that processes JSON-RPC requests.
28
+ *
29
+ * Each connection reads one newline-delimited JSON-RPC request, processes it
30
+ * via the provided handler map, writes a JSON-RPC response, and closes.
31
+ *
32
+ * The special `shutdown` method is intercepted before handler dispatch and
33
+ * triggers the provided {@link StartRpcSocketServerOptions.onShutdown} callback
34
+ * after responding.
35
+ *
36
+ * @param options - Server options.
37
+ * @param options.socketPath - The Unix socket path to listen on.
38
+ * @param options.handlers - Map of RPC method names to handler functions.
39
+ * @param options.onShutdown - Optional callback invoked when a `shutdown` RPC is received.
40
+ * @param options.log - Optional logger for server-side diagnostics.
41
+ * @returns A handle with a `close()` function for cleanup.
42
+ */
43
+ export declare function startRpcSocketServer({ socketPath, handlers, onShutdown, log, }: StartRpcSocketServerOptions): Promise<RpcSocketServerHandle>;
44
+ //# sourceMappingURL=rpc-socket-server.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-socket-server.d.cts","sourceRoot":"","sources":["../../src/daemon/rpc-socket-server.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAgB;AAK7C;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,QAAQ,EAAE,aAAa,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IAC/C;;;;;OAKG;IACH,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,UAAU,EACV,QAAQ,EACR,UAAU,EACV,GAAG,GACJ,EAAE,2BAA2B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAqF9D"}