@coinfello/agent-cli 0.1.12 → 0.1.14
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
CHANGED
|
@@ -101,6 +101,23 @@ Transaction submitted successfully.
|
|
|
101
101
|
Transaction ID: <txn_hash_>
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
### 6. signer-daemon
|
|
105
|
+
|
|
106
|
+
Manages the Secure Enclave signing daemon. Without the daemon, each signing operation (account creation, sign-in, delegation signing) triggers a separate Touch ID / password prompt. Starting the daemon authenticates once and caches the authorization for subsequent operations.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Start the daemon (prompts Touch ID / password once)
|
|
110
|
+
node dist/index.js signer-daemon start
|
|
111
|
+
|
|
112
|
+
# Check if the daemon is running
|
|
113
|
+
node dist/index.js signer-daemon status
|
|
114
|
+
|
|
115
|
+
# Stop the daemon
|
|
116
|
+
node dist/index.js signer-daemon stop
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If the daemon is not running, all signing operations fall back to direct Secure Enclave binary execution (which prompts Touch ID each time).
|
|
120
|
+
|
|
104
121
|
### Help
|
|
105
122
|
|
|
106
123
|
View all commands and options:
|
|
@@ -109,4 +126,5 @@ View all commands and options:
|
|
|
109
126
|
node dist/index.js --help
|
|
110
127
|
node dist/index.js create_account --help
|
|
111
128
|
node dist/index.js send_prompt --help
|
|
129
|
+
node dist/index.js signer-daemon --help
|
|
112
130
|
```
|
package/dist/index.js
CHANGED
|
@@ -5,24 +5,141 @@ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
|
|
|
5
5
|
import { toWebAuthnAccount } from "viem/account-abstraction";
|
|
6
6
|
import * as chains from "viem/chains";
|
|
7
7
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
-
import { execFile } from "node:child_process";
|
|
8
|
+
import { execFile, spawn } from "node:child_process";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { platform, homedir } from "node:os";
|
|
13
|
-
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { userInfo, platform, homedir } from "node:os";
|
|
13
|
+
import { readFile, unlink, mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { createConnection } from "node:net";
|
|
14
15
|
import { http, createPublicClient as createPublicClient$1, serializeErc6492Signature } from "viem";
|
|
15
16
|
import { createSiweMessage } from "viem/siwe";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname$1 = dirname(__filename$1);
|
|
19
20
|
function getBinaryPath() {
|
|
20
|
-
|
|
21
|
+
const bundlePath = join(
|
|
22
|
+
__dirname$1,
|
|
23
|
+
"secure-enclave-signer.app",
|
|
24
|
+
"Contents",
|
|
25
|
+
"MacOS",
|
|
26
|
+
"secure-enclave-signer"
|
|
27
|
+
);
|
|
28
|
+
if (__dirname$1.includes("/src/")) {
|
|
29
|
+
const projectRoot = __dirname$1.split("/src/")[0];
|
|
30
|
+
return join(
|
|
31
|
+
projectRoot,
|
|
32
|
+
"dist",
|
|
33
|
+
"secure-enclave-signer.app",
|
|
34
|
+
"Contents",
|
|
35
|
+
"MacOS",
|
|
36
|
+
"secure-enclave-signer"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return bundlePath;
|
|
21
40
|
}
|
|
41
|
+
const SOCKET_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.sock`;
|
|
42
|
+
const PID_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.pid`;
|
|
22
43
|
function isSecureEnclaveAvailable() {
|
|
23
44
|
return platform() === "darwin";
|
|
24
45
|
}
|
|
25
|
-
async function
|
|
46
|
+
async function sendDaemonRequest(request) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const client = createConnection({ path: SOCKET_PATH }, () => {
|
|
49
|
+
const data = JSON.stringify(request);
|
|
50
|
+
client.end(data);
|
|
51
|
+
});
|
|
52
|
+
let response = "";
|
|
53
|
+
client.on("data", (chunk) => {
|
|
54
|
+
response += chunk.toString();
|
|
55
|
+
});
|
|
56
|
+
client.on("end", () => {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(response.trim());
|
|
59
|
+
if (parsed.success) {
|
|
60
|
+
resolve(parsed.result);
|
|
61
|
+
} else {
|
|
62
|
+
reject(new Error(`SecureEnclave [${parsed.error}]: ${parsed.message}`));
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
reject(new Error(`Invalid daemon response: ${response}`));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
client.on("error", (err) => {
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
71
|
+
client.setTimeout(3e4, () => {
|
|
72
|
+
client.destroy();
|
|
73
|
+
reject(new Error("Daemon request timed out"));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function isDaemonRunning() {
|
|
78
|
+
try {
|
|
79
|
+
await sendDaemonRequest({ command: "ping" });
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function startDaemon() {
|
|
86
|
+
const binaryPath = getBinaryPath();
|
|
87
|
+
const child = spawn(binaryPath, ["daemon"], {
|
|
88
|
+
detached: true,
|
|
89
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
let stdout = "";
|
|
94
|
+
let stderr = "";
|
|
95
|
+
child.stdout.on("data", (chunk) => {
|
|
96
|
+
stdout += chunk.toString();
|
|
97
|
+
try {
|
|
98
|
+
const ready = JSON.parse(stdout.trim());
|
|
99
|
+
if (ready.status === "ready") {
|
|
100
|
+
child.stdout.removeAllListeners();
|
|
101
|
+
child.stderr.removeAllListeners();
|
|
102
|
+
resolve({ pid: ready.pid, socket: ready.socket });
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
child.stderr.on("data", (chunk) => {
|
|
108
|
+
stderr += chunk.toString();
|
|
109
|
+
});
|
|
110
|
+
child.on("error", (err) => {
|
|
111
|
+
reject(new Error(`Failed to start daemon: ${err.message}`));
|
|
112
|
+
});
|
|
113
|
+
child.on("exit", (code) => {
|
|
114
|
+
if (code !== 0) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(stderr.trim());
|
|
117
|
+
reject(new Error(`Daemon exited: [${parsed.error}] ${parsed.message}`));
|
|
118
|
+
} catch {
|
|
119
|
+
reject(new Error(`Daemon exited with code ${code}: ${stderr}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
setTimeout(() => reject(new Error("Daemon startup timed out")), 15e3);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function stopDaemon() {
|
|
127
|
+
try {
|
|
128
|
+
const pidStr = await readFile(PID_PATH, "utf-8");
|
|
129
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
130
|
+
process.kill(pid, "SIGTERM");
|
|
131
|
+
} catch {
|
|
132
|
+
try {
|
|
133
|
+
await unlink(SOCKET_PATH);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await unlink(PID_PATH);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function runCommandDirect(args) {
|
|
26
143
|
const binaryPath = getBinaryPath();
|
|
27
144
|
try {
|
|
28
145
|
const { stdout } = await execFileAsync(binaryPath, args, {
|
|
@@ -45,6 +162,24 @@ async function runCommand(args) {
|
|
|
45
162
|
throw new Error(`SecureEnclave command failed: ${error.message ?? "Unknown error"}`);
|
|
46
163
|
}
|
|
47
164
|
}
|
|
165
|
+
async function runCommand(args) {
|
|
166
|
+
try {
|
|
167
|
+
const command = args[0];
|
|
168
|
+
const request = { command };
|
|
169
|
+
if (command === "sign") {
|
|
170
|
+
const tagIdx = args.indexOf("--tag");
|
|
171
|
+
const payloadIdx = args.indexOf("--payload");
|
|
172
|
+
if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
|
|
173
|
+
if (payloadIdx >= 0 && payloadIdx + 1 < args.length) request.payload = args[payloadIdx + 1];
|
|
174
|
+
} else if (command === "get-public-key") {
|
|
175
|
+
const tagIdx = args.indexOf("--tag");
|
|
176
|
+
if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
|
|
177
|
+
}
|
|
178
|
+
return await sendDaemonRequest(request);
|
|
179
|
+
} catch {
|
|
180
|
+
return await runCommandDirect(args);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
48
183
|
async function generateKey() {
|
|
49
184
|
const result = await runCommand(["generate"]);
|
|
50
185
|
return {
|
|
@@ -3220,22 +3355,26 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
|
|
|
3220
3355
|
delegateAddress,
|
|
3221
3356
|
scope
|
|
3222
3357
|
});
|
|
3223
|
-
console.log("Signing subdelegation...");
|
|
3358
|
+
console.log("Signing subdelegation... ", JSON.stringify(subdelegation, null, 4));
|
|
3224
3359
|
const signature = await smartAccount.signDelegation({
|
|
3225
3360
|
delegation: subdelegation
|
|
3226
3361
|
});
|
|
3362
|
+
console.log("Signed subdelegation");
|
|
3227
3363
|
let sig = signature;
|
|
3228
3364
|
const chain = resolveChainInput(config.chain);
|
|
3229
3365
|
const publicClient = createPublicClient(chain);
|
|
3366
|
+
console.log("Getting code...");
|
|
3230
3367
|
const code = await publicClient.getCode({ address: smartAccount.address });
|
|
3231
3368
|
const isDeployed = !!(code && code !== "0x");
|
|
3232
3369
|
if (!isDeployed) {
|
|
3370
|
+
console.log("Getting factory args...");
|
|
3233
3371
|
const factoryArgs = await smartAccount.getFactoryArgs();
|
|
3234
3372
|
sig = serializeErc6492Signature({
|
|
3235
3373
|
signature,
|
|
3236
3374
|
address: factoryArgs.factory,
|
|
3237
3375
|
data: factoryArgs.factoryData
|
|
3238
3376
|
});
|
|
3377
|
+
console.log("Serialized 6492 sig");
|
|
3239
3378
|
}
|
|
3240
3379
|
const signedSubdelegation = { ...subdelegation, signature: sig };
|
|
3241
3380
|
console.log("Sending signed delegation...");
|
|
@@ -3257,4 +3396,44 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
|
|
|
3257
3396
|
process.exit(1);
|
|
3258
3397
|
}
|
|
3259
3398
|
});
|
|
3399
|
+
const signerDaemon = program.command("signer-daemon").description("Manage the Secure Enclave signing daemon");
|
|
3400
|
+
signerDaemon.command("start").description("Start the signing daemon (authenticates via Touch ID / password once)").action(async () => {
|
|
3401
|
+
try {
|
|
3402
|
+
const running = await isDaemonRunning();
|
|
3403
|
+
if (running) {
|
|
3404
|
+
console.log("Signing daemon is already running.");
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
console.log("Starting signing daemon (authenticate when prompted)...");
|
|
3408
|
+
const { pid, socket } = await startDaemon();
|
|
3409
|
+
console.log(`Signing daemon started.`);
|
|
3410
|
+
console.log(`PID: ${pid}`);
|
|
3411
|
+
console.log(`Socket: ${socket}`);
|
|
3412
|
+
} catch (err) {
|
|
3413
|
+
console.error(`Failed to start daemon: ${err.message}`);
|
|
3414
|
+
process.exit(1);
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
signerDaemon.command("stop").description("Stop the signing daemon").action(async () => {
|
|
3418
|
+
try {
|
|
3419
|
+
const running = await isDaemonRunning();
|
|
3420
|
+
if (!running) {
|
|
3421
|
+
console.log("Signing daemon is not running.");
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
await stopDaemon();
|
|
3425
|
+
console.log("Signing daemon stopped.");
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
console.error(`Failed to stop daemon: ${err.message}`);
|
|
3428
|
+
process.exit(1);
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
signerDaemon.command("status").description("Check if the signing daemon is running").action(async () => {
|
|
3432
|
+
const running = await isDaemonRunning();
|
|
3433
|
+
if (running) {
|
|
3434
|
+
console.log("Signing daemon is running.");
|
|
3435
|
+
} else {
|
|
3436
|
+
console.log("Signing daemon is not running.");
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3260
3439
|
program.parse();
|
|
Binary file
|