thaifi-wallet-cli 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agentAuth.js +55 -0
- package/dist/commands/request.js +43 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Set `THAIFI_WALLET_URL` / `THAIFI_RPC_URL` to target other deployments.
|
|
|
44
44
|
| `tokens` | List supported TIP-20 tokens (symbol, decimals, address) |
|
|
45
45
|
| `balance [--token <symbol\|addr>]` | Balances (default: all supported tokens) |
|
|
46
46
|
| `transfer <to> <amount> [--token <symbol\|addr>]` | Send tokens (default: pathUSD; limit enforced on-chain) |
|
|
47
|
-
| `request <url> [-X -H -d --json --dry-run --max-spend]` | HTTP with automatic x402/MPP payment |
|
|
47
|
+
| `request <url> [-X -H -d --json --dry-run --max-spend]` | HTTP with automatic x402/MPP payment **and ThaiFiAgent identity auth (401/403 answered by signing the challenge)** |
|
|
48
48
|
| `services [--search q]` | Discover services from the MPP registry (`THAIFI_MPP_REGISTRY`) |
|
|
49
49
|
| `logout` | Remove the local key (revoke on-chain from the web wallet) |
|
|
50
50
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThaiFi Agent Auth (TAA) — client-side helper.
|
|
3
|
+
*
|
|
4
|
+
* Signs a server challenge with the agent access key (P256, keychain
|
|
5
|
+
* envelope binding the wallet account) so the server can verify on-chain
|
|
6
|
+
* that this key is authorized (AccountKeychain, TIP-1011). Server-side
|
|
7
|
+
* reference implementation: thaifi/mpp repo, src/agentAuth.ts.
|
|
8
|
+
*
|
|
9
|
+
* Signed hash (must match the server byte-for-byte):
|
|
10
|
+
* keccak256(tag4("thaifi-agent-auth") ‖ 0x01 ‖ keccak256(realm)[0:10]
|
|
11
|
+
* ‖ nonceBytes ‖ utf8(METHOD) ‖ utf8(pathWithQuery)
|
|
12
|
+
* ‖ sha256(body) | 0x00*32 ‖ uint64be(timestamp))
|
|
13
|
+
*/
|
|
14
|
+
import { concatHex, keccak256, sha256, slice, stringToBytes, stringToHex, toHex } from "viem";
|
|
15
|
+
import { Account } from "viem/tempo";
|
|
16
|
+
const AUTH_TAG = slice(keccak256(stringToBytes("thaifi-agent-auth")), 0, 4);
|
|
17
|
+
const ZERO_BODY_HASH = `0x${"00".repeat(32)}`;
|
|
18
|
+
function b64uDecodeBytes(value) {
|
|
19
|
+
return new Uint8Array(Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64"));
|
|
20
|
+
}
|
|
21
|
+
export function agentAuthHash(params) {
|
|
22
|
+
const bodyHash = params.body && params.body.length > 0 ? sha256(stringToBytes(params.body)) : ZERO_BODY_HASH;
|
|
23
|
+
return keccak256(concatHex([
|
|
24
|
+
AUTH_TAG,
|
|
25
|
+
"0x01",
|
|
26
|
+
slice(keccak256(stringToBytes(params.realm)), 0, 10),
|
|
27
|
+
params.nonce,
|
|
28
|
+
stringToHex(params.method.toUpperCase()),
|
|
29
|
+
stringToHex(params.path),
|
|
30
|
+
bodyHash,
|
|
31
|
+
toHex(BigInt(params.ts), { size: 8 }),
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
/** Sign the ThaiFiAgent challenge → the b64u credential for the retry header. */
|
|
35
|
+
export async function buildAgentAuthCredential(store, challenge, req) {
|
|
36
|
+
if (!store.userAddress)
|
|
37
|
+
throw new Error("Not paired.");
|
|
38
|
+
const url = new URL(req.url);
|
|
39
|
+
const path = url.pathname + url.search;
|
|
40
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
41
|
+
const nonce = toHex(b64uDecodeBytes(challenge.nonce));
|
|
42
|
+
const hash = agentAuthHash({
|
|
43
|
+
realm: challenge.realm,
|
|
44
|
+
nonce,
|
|
45
|
+
method: req.method,
|
|
46
|
+
path,
|
|
47
|
+
body: req.body ?? null,
|
|
48
|
+
ts,
|
|
49
|
+
});
|
|
50
|
+
const account = Account.fromP256(store.privateKey, {
|
|
51
|
+
access: store.userAddress,
|
|
52
|
+
});
|
|
53
|
+
const signature = await account.sign({ hash });
|
|
54
|
+
return { v: 1, ts, nonce: challenge.nonce, signature };
|
|
55
|
+
}
|
package/dist/commands/request.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { concatHex, keccak256, slice, stringToBytes } from "viem";
|
|
3
3
|
import { loadStore } from "../store.js";
|
|
4
4
|
import { createAgentClient } from "../chain.js";
|
|
5
|
+
import { buildAgentAuthCredential } from "../agentAuth.js";
|
|
5
6
|
import { CONFIG, PATHUSD_DECIMALS } from "../config.js";
|
|
6
7
|
function b64uDecode(s) {
|
|
7
8
|
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
@@ -51,6 +52,26 @@ export function parseMppChallenge(header) {
|
|
|
51
52
|
challenge.expires = params.expires;
|
|
52
53
|
return challenge;
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a `WWW-Authenticate: ThaiFiAgent realm="…", challenge="<b64u>"`
|
|
57
|
+
* header — ThaiFi Agent Auth (identity proof, no payment). The server
|
|
58
|
+
* verifies our signature against the AccountKeychain precompile.
|
|
59
|
+
*/
|
|
60
|
+
function parseAgentChallenge(header) {
|
|
61
|
+
const m = /^ThaiFiAgent\s+(.+)$/i.exec(header.trim());
|
|
62
|
+
if (!m)
|
|
63
|
+
return null;
|
|
64
|
+
const params = {};
|
|
65
|
+
const re = /([a-zA-Z]+)=((?:"((?:[^"\\]|\\.)*)")|[^\s,]+)/g;
|
|
66
|
+
let pm;
|
|
67
|
+
while ((pm = re.exec(m[1]))) {
|
|
68
|
+
const value = pm[3] !== undefined ? pm[3].replace(/\\(.)/g, "$1") : pm[2];
|
|
69
|
+
params[pm[1].toLowerCase()] = value;
|
|
70
|
+
}
|
|
71
|
+
if (!params.realm || !params.challenge)
|
|
72
|
+
return null;
|
|
73
|
+
return { realm: params.realm, nonce: params.challenge };
|
|
74
|
+
}
|
|
54
75
|
/** MPP tempo charge → on-chain TIP-20 transfer (push mode: we broadcast, then send the hash). */
|
|
55
76
|
function mppTempoChallenge(challenge) {
|
|
56
77
|
const req = challenge.request;
|
|
@@ -190,6 +211,28 @@ export async function request(url, options) {
|
|
|
190
211
|
headers: options.headers,
|
|
191
212
|
body: options.body,
|
|
192
213
|
});
|
|
214
|
+
// 401/403 → ThaiFi Agent Auth: sign the challenge with the agent access key
|
|
215
|
+
// (identity proof, no payment) → retry once. The server verifies the key
|
|
216
|
+
// against the AccountKeychain precompile.
|
|
217
|
+
if (res.status === 401 || res.status === 403) {
|
|
218
|
+
const agentChallenge = parseAgentChallenge(res.headers.get("www-authenticate") ?? "");
|
|
219
|
+
if (agentChallenge) {
|
|
220
|
+
console.log("401 — signing agent auth challenge …");
|
|
221
|
+
const credential = await buildAgentAuthCredential(store, agentChallenge, {
|
|
222
|
+
method: options.method,
|
|
223
|
+
url,
|
|
224
|
+
body: options.body,
|
|
225
|
+
});
|
|
226
|
+
res = await fetch(url, {
|
|
227
|
+
method: options.method,
|
|
228
|
+
headers: {
|
|
229
|
+
...options.headers,
|
|
230
|
+
Authorization: "ThaiFiAgent " + b64uEncode(JSON.stringify(credential)),
|
|
231
|
+
},
|
|
232
|
+
body: options.body,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
193
236
|
// 402 → pay on-chain (TIP-20 transfer from the agent's spend limit) → retry once.
|
|
194
237
|
if (res.status === 402) {
|
|
195
238
|
const challenge = await parseChallenge(res, CONFIG.pathUsd);
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const program = new Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name("thaifi")
|
|
16
16
|
.description("ThaiFi Wallet CLI — pair with wallet.thaifi.com and sign with on-chain access keys")
|
|
17
|
-
.version("0.
|
|
17
|
+
.version("0.3.0");
|
|
18
18
|
program
|
|
19
19
|
.command("login")
|
|
20
20
|
.description("Pair this CLI with your ThaiFi Wallet (opens the browser to approve)")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thaifi-wallet-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|