thaifi-wallet-cli 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/login.js +93 -25
- package/dist/store.js +15 -0
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
/** `thaifi login` — pair this CLI with a wallet.thaifi.com account.
|
|
1
|
+
/** `thaifi login` — pair this CLI with a wallet.thaifi.com account.
|
|
2
|
+
*
|
|
3
|
+
* Reliability guarantees (0.3.6+):
|
|
4
|
+
* - The approval URL is always written to ~/.thaifi/pair.log (chmod 600), so
|
|
5
|
+
* background launchers that lose stdout can recover it.
|
|
6
|
+
* - A pairing created but not yet approved is persisted in the store; re-running
|
|
7
|
+
* `thaifi login` within the poll window REUSES it (same URL, same key) instead
|
|
8
|
+
* of creating throwaway pairings on the server.
|
|
9
|
+
*/
|
|
2
10
|
import { hostname } from "node:os";
|
|
3
11
|
import { generatePrivateKey } from "viem/accounts";
|
|
4
12
|
import { Account } from "viem/tempo";
|
|
5
13
|
import { CONFIG } from "../config.js";
|
|
6
|
-
import { loadStore, saveStore, storePath } from "../store.js";
|
|
14
|
+
import { loadStore, saveStore, storePath, writePairLog, pairLogPath } from "../store.js";
|
|
7
15
|
const signatureType = "p256";
|
|
8
16
|
async function openBrowser(url) {
|
|
9
17
|
const { exec } = await import("node:child_process");
|
|
@@ -14,44 +22,97 @@ async function openBrowser(url) {
|
|
|
14
22
|
: `xdg-open "${url}"`;
|
|
15
23
|
exec(cmd, () => undefined);
|
|
16
24
|
}
|
|
25
|
+
function pairLogContent(p, status) {
|
|
26
|
+
return [
|
|
27
|
+
"# ThaiFi CLI pairing",
|
|
28
|
+
"created=" + new Date(p.createdAt).toISOString(),
|
|
29
|
+
"PAIR_URL=" + p.url,
|
|
30
|
+
"PAIR_CODE=" + p.code,
|
|
31
|
+
"KEY_ID=" + p.keyId,
|
|
32
|
+
"MACHINE=" + p.name,
|
|
33
|
+
"STATUS=" + status,
|
|
34
|
+
"",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
17
37
|
export async function login(options = {}) {
|
|
18
|
-
|
|
38
|
+
const existing = loadStore();
|
|
39
|
+
if (existing?.userAddress) {
|
|
19
40
|
console.error("Already paired (found " + storePath + "). Run `thaifi logout` first to re-pair.");
|
|
20
41
|
process.exitCode = 1;
|
|
21
42
|
return;
|
|
22
43
|
}
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
// Reuse a still-fresh pending pairing instead of creating a new one —
|
|
45
|
+
// a re-run of `thaifi login` (crashed launcher, lost stdout, impatient
|
|
46
|
+
// retry) must never pile up throwaway pairings on the server.
|
|
47
|
+
const pending = existing?.pending;
|
|
48
|
+
let reused = false;
|
|
49
|
+
let privateKey;
|
|
50
|
+
let name;
|
|
51
|
+
let keyId;
|
|
52
|
+
let pairingId;
|
|
53
|
+
let code;
|
|
54
|
+
let url;
|
|
55
|
+
if (pending && Date.now() - pending.createdAt < CONFIG.pollTimeoutMs) {
|
|
56
|
+
reused = true;
|
|
57
|
+
privateKey = pending.privateKey;
|
|
58
|
+
keyId = pending.keyId;
|
|
59
|
+
name = pending.name;
|
|
60
|
+
pairingId = pending.pairingId;
|
|
61
|
+
code = pending.code;
|
|
62
|
+
url = pending.url;
|
|
63
|
+
console.log("Pairing already waiting for approval (started " +
|
|
64
|
+
Math.round((Date.now() - pending.createdAt) / 60000) +
|
|
65
|
+
" min ago) — reusing it, no new pairing created.");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// 1. Generate the agent access key (P256 — never leaves this machine).
|
|
69
|
+
privateKey = generatePrivateKey();
|
|
70
|
+
name = hostname();
|
|
71
|
+
keyId = Account.fromP256(privateKey).address;
|
|
72
|
+
console.log("Agent key: " + keyId);
|
|
73
|
+
console.log("Machine: " + name);
|
|
74
|
+
console.log("");
|
|
75
|
+
// 2. Start pairing with the web wallet.
|
|
76
|
+
const startRes = await fetch(CONFIG.walletUrl + "/api/agent/pair/start", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ keyId, name, signatureType }),
|
|
80
|
+
});
|
|
81
|
+
if (!startRes.ok) {
|
|
82
|
+
console.error("Pairing failed to start: HTTP " + startRes.status + " " + (await startRes.text()));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
({ pairingId, code } = (await startRes.json()));
|
|
87
|
+
url = `${CONFIG.walletUrl}/pair?id=${pairingId}&code=${code}`;
|
|
88
|
+
// Persist the pending pairing BEFORE printing anything — if this process
|
|
89
|
+
// dies, a re-run recovers the same pairing instead of orphaning it.
|
|
90
|
+
saveStore({
|
|
91
|
+
privateKey,
|
|
92
|
+
keyId,
|
|
93
|
+
name,
|
|
94
|
+
walletUrl: CONFIG.walletUrl,
|
|
95
|
+
rpcUrl: CONFIG.rpcUrl,
|
|
96
|
+
chainId: CONFIG.chainId,
|
|
97
|
+
pending: { pairingId, code, url, keyId, name, privateKey, createdAt: Date.now() },
|
|
98
|
+
});
|
|
40
99
|
}
|
|
41
|
-
|
|
42
|
-
const url = `${CONFIG.walletUrl}/pair?id=${pairingId}&code=${code}`;
|
|
100
|
+
// 3. Print + persist the approval URL (both routes land here).
|
|
43
101
|
console.log("Authorize this CLI in the ThaiFi Wallet:");
|
|
44
102
|
console.log("");
|
|
45
103
|
console.log(" " + url);
|
|
46
104
|
console.log("");
|
|
105
|
+
console.log("PAIR_URL=" + url);
|
|
47
106
|
console.log("Pairing code: " + code);
|
|
107
|
+
writePairLog(pairLogContent({ pairingId, code, url, keyId, name, privateKey, createdAt: pending?.createdAt ?? Date.now() }, reused ? "waiting-for-approval (reused)" : "waiting-for-approval"));
|
|
108
|
+
console.log("(approval link saved to " + pairLogPath + ")");
|
|
48
109
|
if (options.browser !== false) {
|
|
49
110
|
await openBrowser(url);
|
|
50
111
|
console.log("(opened in your browser — use --no-browser on a headless machine)");
|
|
51
112
|
}
|
|
52
113
|
console.log("");
|
|
53
114
|
console.log("Waiting for approval (timeout " + CONFIG.pollTimeoutMs / 60000 + " min)…");
|
|
54
|
-
//
|
|
115
|
+
// 4. Poll until approved.
|
|
55
116
|
const deadline = Date.now() + CONFIG.pollTimeoutMs;
|
|
56
117
|
let status = null;
|
|
57
118
|
while (Date.now() < deadline) {
|
|
@@ -64,17 +125,22 @@ export async function login(options = {}) {
|
|
|
64
125
|
break;
|
|
65
126
|
if (status.status === "rejected" || status.status === "claimed") {
|
|
66
127
|
console.error("Pairing was " + status.status + " on the web wallet.");
|
|
128
|
+
// Clear the dead pending pairing so the next login starts fresh.
|
|
129
|
+
if (existing)
|
|
130
|
+
saveStore({ ...existing, pending: undefined });
|
|
67
131
|
process.exitCode = 1;
|
|
68
132
|
return;
|
|
69
133
|
}
|
|
70
134
|
process.stdout.write(".");
|
|
71
135
|
}
|
|
72
136
|
if (status?.status !== "approved") {
|
|
73
|
-
console.error("\nPairing timed out."
|
|
137
|
+
console.error("\nPairing timed out. The approval link is still in " +
|
|
138
|
+
pairLogPath +
|
|
139
|
+
" — run `thaifi login` again to reuse it (recommending the user approve) or wait for it to expire server-side.");
|
|
74
140
|
process.exitCode = 1;
|
|
75
141
|
return;
|
|
76
142
|
}
|
|
77
|
-
//
|
|
143
|
+
// 5. Persist the completed pairing (and clear pending).
|
|
78
144
|
saveStore({
|
|
79
145
|
privateKey,
|
|
80
146
|
keyId,
|
|
@@ -87,7 +153,9 @@ export async function login(options = {}) {
|
|
|
87
153
|
limitAmount: status.limitAmount,
|
|
88
154
|
limitPeriod: status.limitPeriod,
|
|
89
155
|
keyExpiry: status.expiry,
|
|
156
|
+
pending: undefined,
|
|
90
157
|
});
|
|
158
|
+
writePairLog(pairLogContent({ pairingId, code, url, keyId, name, privateKey, createdAt: pending?.createdAt ?? Date.now() }, "approved"));
|
|
91
159
|
console.log("");
|
|
92
160
|
console.log("✓ Paired!");
|
|
93
161
|
console.log(" Wallet account: " + status.userAddress);
|
package/dist/store.js
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
const DIR = join(homedir(), ".thaifi");
|
|
8
8
|
const FILE = join(DIR, "store.json");
|
|
9
|
+
const PAIR_LOG = join(DIR, "pair.log");
|
|
9
10
|
export function loadStore() {
|
|
10
11
|
if (!existsSync(FILE))
|
|
11
12
|
return null;
|
|
@@ -31,3 +32,17 @@ export function clearStore() {
|
|
|
31
32
|
rmSync(FILE);
|
|
32
33
|
}
|
|
33
34
|
export const storePath = FILE;
|
|
35
|
+
export const pairLogPath = PAIR_LOG;
|
|
36
|
+
/** Writes ~/.thaifi/pair.log (chmod 600) — the durable copy of the approval
|
|
37
|
+
* URL. Background launchers that lose stdout can always recover the link
|
|
38
|
+
* from this file. Overwritten on every `thaifi login`. */
|
|
39
|
+
export function writePairLog(content) {
|
|
40
|
+
mkdirSync(DIR, { recursive: true });
|
|
41
|
+
writeFileSync(PAIR_LOG, content, { mode: 0o600 });
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(PAIR_LOG, 0o600);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// best-effort on platforms without chmod
|
|
47
|
+
}
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thaifi-wallet-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "ThaiFi Wallet CLI for AI agents — pair with wallet.thaifi.com, sign with on-chain access keys (spending limits enforced on-chain), x402/MPP payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|