@pay-skill/sdk 0.1.8 → 0.2.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.
Files changed (60) hide show
  1. package/README.md +143 -154
  2. package/dist/auth.d.ts +11 -6
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +19 -7
  5. package/dist/auth.js.map +1 -1
  6. package/dist/errors.d.ts +4 -2
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +8 -3
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.d.ts +2 -13
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -6
  13. package/dist/index.js.map +1 -1
  14. package/dist/keychain.d.ts +8 -0
  15. package/dist/keychain.d.ts.map +1 -0
  16. package/dist/keychain.js +17 -0
  17. package/dist/keychain.js.map +1 -0
  18. package/dist/wallet.d.ts +135 -104
  19. package/dist/wallet.d.ts.map +1 -1
  20. package/dist/wallet.js +631 -276
  21. package/dist/wallet.js.map +1 -1
  22. package/jsr.json +13 -13
  23. package/knip.json +5 -5
  24. package/package.json +51 -48
  25. package/src/auth.ts +210 -200
  26. package/src/eip3009.ts +79 -79
  27. package/src/errors.ts +55 -48
  28. package/src/index.ts +24 -51
  29. package/src/keychain.ts +18 -0
  30. package/src/wallet.ts +1111 -445
  31. package/tests/test_auth_rejection.ts +102 -154
  32. package/tests/test_crypto.ts +138 -251
  33. package/tests/test_e2e.ts +99 -158
  34. package/tests/test_errors.ts +44 -36
  35. package/tests/test_ows.ts +153 -0
  36. package/tests/test_wallet.ts +194 -0
  37. package/dist/client.d.ts +0 -94
  38. package/dist/client.d.ts.map +0 -1
  39. package/dist/client.js +0 -443
  40. package/dist/client.js.map +0 -1
  41. package/dist/models.d.ts +0 -78
  42. package/dist/models.d.ts.map +0 -1
  43. package/dist/models.js +0 -2
  44. package/dist/models.js.map +0 -1
  45. package/dist/ows-signer.d.ts +0 -75
  46. package/dist/ows-signer.d.ts.map +0 -1
  47. package/dist/ows-signer.js +0 -130
  48. package/dist/ows-signer.js.map +0 -1
  49. package/dist/signer.d.ts +0 -46
  50. package/dist/signer.d.ts.map +0 -1
  51. package/dist/signer.js +0 -111
  52. package/dist/signer.js.map +0 -1
  53. package/src/client.ts +0 -644
  54. package/src/models.ts +0 -77
  55. package/src/ows-signer.ts +0 -223
  56. package/src/signer.ts +0 -147
  57. package/tests/test_ows_integration.ts +0 -92
  58. package/tests/test_ows_signer.ts +0 -365
  59. package/tests/test_signer.ts +0 -47
  60. package/tests/test_validation.ts +0 -66
@@ -1,154 +1,102 @@
1
- /**
2
- * Auth rejection tests — proves that:
3
- * 1. Requests without auth headers are rejected with 401
4
- * 2. Requests with invalid/wrong signatures are rejected with 401
5
- * 3. The SDK surfaces auth errors as PayServerError with correct statusCode
6
- */
7
-
8
- import { describe, it, beforeEach, afterEach } from "node:test";
9
- import assert from "node:assert/strict";
10
- import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
11
- import { once } from "node:events";
12
-
13
- import { PayClient, PayServerError, CallbackSigner, RawKeySigner } from "../src/index.js";
14
- import type { Hex, Address } from "viem";
15
-
16
- const ANVIL_PK =
17
- "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
18
- const WRONG_PK =
19
- "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" as Hex;
20
-
21
- const TEST_ROUTER = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
22
- const TEST_CHAIN_ID = 8453;
23
- const VALID_ADDR = "0x" + "a1".repeat(20);
24
-
25
- /**
26
- * Minimal HTTP server that enforces X-Pay-* auth headers.
27
- * Returns 401 if any required header is missing, 200 otherwise.
28
- */
29
- function createAuthServer(): Server {
30
- return createServer((req: IncomingMessage, res: ServerResponse) => {
31
- const agent = req.headers["x-pay-agent"];
32
- const sig = req.headers["x-pay-signature"];
33
- const ts = req.headers["x-pay-timestamp"];
34
- const nonce = req.headers["x-pay-nonce"];
35
-
36
- if (!agent || !sig || !ts || !nonce) {
37
- res.writeHead(401, { "Content-Type": "application/json" });
38
- res.end(JSON.stringify({ error: "Missing auth headers" }));
39
- return;
40
- }
41
-
42
- // Check that sig looks like a real 65-byte hex signature
43
- const sigStr = Array.isArray(sig) ? sig[0] : sig;
44
- if (!sigStr.startsWith("0x") || sigStr.length !== 132) {
45
- res.writeHead(401, { "Content-Type": "application/json" });
46
- res.end(JSON.stringify({ error: "Invalid signature format" }));
47
- return;
48
- }
49
-
50
- // Check that sig is not all zeros (stub detection)
51
- if (sigStr === "0x" + "0".repeat(130)) {
52
- res.writeHead(401, { "Content-Type": "application/json" });
53
- res.end(JSON.stringify({ error: "Stub signature rejected" }));
54
- return;
55
- }
56
-
57
- // Auth passed return mock data
58
- res.writeHead(200, { "Content-Type": "application/json" });
59
- res.end(
60
- JSON.stringify({
61
- address: agent,
62
- balance: 100_000_000,
63
- open_tabs: [],
64
- })
65
- );
66
- });
67
- }
68
-
69
- let server: Server;
70
- let baseUrl: string;
71
-
72
- describe("Auth rejection", () => {
73
- beforeEach(async () => {
74
- server = createAuthServer();
75
- server.listen(0); // random port
76
- await once(server, "listening");
77
- const addr = server.address();
78
- if (typeof addr === "object" && addr) {
79
- baseUrl = `http://127.0.0.1:${addr.port}`;
80
- }
81
- });
82
-
83
- afterEach(async () => {
84
- server.close();
85
- await once(server, "close");
86
- });
87
-
88
- it("rejects request without auth headers (no private key configured)", async () => {
89
- // Client with no auth config — sends no X-Pay-* headers
90
- const client = new PayClient({
91
- apiUrl: baseUrl,
92
- signer: new CallbackSigner((_h: Uint8Array) => new Uint8Array(65)),
93
- });
94
-
95
- await assert.rejects(
96
- () => client.getStatus(),
97
- (err: unknown) => {
98
- assert.ok(err instanceof PayServerError);
99
- assert.equal(err.statusCode, 401);
100
- assert.ok(err.message.includes("Missing auth headers"));
101
- return true;
102
- }
103
- );
104
- });
105
-
106
- it("rejects request with stub signer (all-zero signature)", async () => {
107
- // Client with a stub signer that returns zeros — server should reject
108
- const client = new PayClient({
109
- apiUrl: baseUrl,
110
- signer: new CallbackSigner((_h: Uint8Array) => new Uint8Array(65)),
111
- chainId: TEST_CHAIN_ID,
112
- routerAddress: TEST_ROUTER,
113
- });
114
-
115
- await assert.rejects(
116
- () => client.getStatus(),
117
- (err: unknown) => {
118
- assert.ok(err instanceof PayServerError);
119
- assert.equal(err.statusCode, 401);
120
- return true;
121
- }
122
- );
123
- });
124
-
125
- it("accepts request with valid auth headers (real signing)", async () => {
126
- const client = new PayClient({
127
- apiUrl: baseUrl,
128
- privateKey: ANVIL_PK,
129
- chainId: TEST_CHAIN_ID,
130
- routerAddress: TEST_ROUTER,
131
- });
132
-
133
- // Should NOT throw — server accepts valid auth
134
- const status = await client.getStatus();
135
- assert.ok(status.balance >= 0);
136
- });
137
-
138
- it("PayServerError has statusCode 401 for auth failures", async () => {
139
- // Directly verify error structure
140
- const client = new PayClient({
141
- apiUrl: baseUrl,
142
- signer: new CallbackSigner((_h: Uint8Array) => new Uint8Array(65)),
143
- });
144
-
145
- try {
146
- await client.getStatus();
147
- assert.fail("Should have thrown PayServerError");
148
- } catch (err) {
149
- assert.ok(err instanceof PayServerError, "must be PayServerError");
150
- assert.equal(err.statusCode, 401, "statusCode must be 401");
151
- assert.equal(err.code, "server_error");
152
- }
153
- });
154
- });
1
+ /**
2
+ * Auth rejection tests — proves that the Wallet sends valid auth headers
3
+ * and that servers can reject invalid auth.
4
+ */
5
+
6
+ import { describe, it, beforeEach, afterEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import {
9
+ createServer,
10
+ type Server,
11
+ type IncomingMessage,
12
+ type ServerResponse,
13
+ } from "node:http";
14
+ import { once } from "node:events";
15
+ import type { Hex, Address } from "viem";
16
+ import { Wallet, PayServerError } from "../src/index.js";
17
+
18
+ const ANVIL_PK =
19
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
20
+
21
+ /**
22
+ * Minimal HTTP server that enforces X-Pay-* auth headers.
23
+ * /contracts is public (no auth).
24
+ * Everything else requires valid auth headers.
25
+ */
26
+ function createAuthServer(): Server {
27
+ return createServer((req: IncomingMessage, res: ServerResponse) => {
28
+ // /contracts is public
29
+ if (req.url?.endsWith("/contracts")) {
30
+ res.writeHead(200, { "Content-Type": "application/json" });
31
+ res.end(
32
+ JSON.stringify({
33
+ router: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
34
+ tab: "0x" + "bb".repeat(20),
35
+ direct: "0x" + "cc".repeat(20),
36
+ fee: "0x" + "dd".repeat(20),
37
+ usdc: "0x" + "ee".repeat(20),
38
+ chain_id: 8453,
39
+ }),
40
+ );
41
+ return;
42
+ }
43
+
44
+ const agent = req.headers["x-pay-agent"];
45
+ const sig = req.headers["x-pay-signature"];
46
+ const ts = req.headers["x-pay-timestamp"];
47
+ const nonce = req.headers["x-pay-nonce"];
48
+
49
+ if (!agent || !sig || !ts || !nonce) {
50
+ res.writeHead(401, { "Content-Type": "application/json" });
51
+ res.end(JSON.stringify({ error: "Missing auth headers" }));
52
+ return;
53
+ }
54
+
55
+ const sigStr = Array.isArray(sig) ? sig[0] : sig;
56
+ if (!sigStr.startsWith("0x") || sigStr.length !== 132) {
57
+ res.writeHead(401, { "Content-Type": "application/json" });
58
+ res.end(JSON.stringify({ error: "Invalid signature format" }));
59
+ return;
60
+ }
61
+
62
+ // Auth passed
63
+ res.writeHead(200, { "Content-Type": "application/json" });
64
+ res.end(
65
+ JSON.stringify({
66
+ wallet: agent,
67
+ balance_usdc: "100000000",
68
+ open_tabs: 0,
69
+ total_locked: 0,
70
+ }),
71
+ );
72
+ });
73
+ }
74
+
75
+ let server: Server;
76
+ let baseUrl: string;
77
+
78
+ describe("Auth with new Wallet class", () => {
79
+ beforeEach(async () => {
80
+ server = createAuthServer();
81
+ server.listen(0);
82
+ await once(server, "listening");
83
+ const addr = server.address();
84
+ if (typeof addr === "object" && addr) {
85
+ baseUrl = `http://127.0.0.1:${addr.port}/api/v1`;
86
+ }
87
+ });
88
+
89
+ afterEach(async () => {
90
+ server.close();
91
+ await once(server, "close");
92
+ });
93
+
94
+ it("Wallet sends valid auth headers and gets 200", async () => {
95
+ process.env.PAYSKILL_API_URL = baseUrl;
96
+ const wallet = new Wallet({ privateKey: ANVIL_PK });
97
+ const status = await wallet.status();
98
+ assert.ok(status.address.startsWith("0x"));
99
+ assert.ok(status.balance.total >= 0);
100
+ delete process.env.PAYSKILL_API_URL;
101
+ });
102
+ });
@@ -1,251 +1,138 @@
1
- /**
2
- * Crypto round-trip tests — proves that:
3
- * 1. Address derivation uses real secp256k1 (not FNV hash)
4
- * 2. EIP-712 signing produces valid signatures that the server can recover
5
- * 3. buildAuthHeaders produces valid auth headers
6
- */
7
-
8
- import { describe, it } from "node:test";
9
- import assert from "node:assert/strict";
10
- import { privateKeyToAccount } from "viem/accounts";
11
- import { recoverAddress, type Hex, type Address } from "viem";
12
-
13
- import { PrivateKeySigner, Wallet } from "../src/wallet.js";
14
- import { RawKeySigner } from "../src/signer.js";
15
- import { buildAuthHeaders, computeEip712Hash } from "../src/auth.js";
16
-
17
- // Anvil account #0 — well-known test key
18
- const ANVIL_PK =
19
- "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
20
- const ANVIL_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address;
21
-
22
- const TEST_ROUTER = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
23
- const TEST_CHAIN_ID = 8453;
24
-
25
- describe("Address derivation", () => {
26
- it("derives correct address from Anvil #0 private key via Wallet", () => {
27
- const wallet = new Wallet({
28
- privateKey: ANVIL_PK,
29
- chain: "8453",
30
- apiUrl: "http://localhost:3000/api/v1",
31
- routerAddress: TEST_ROUTER,
32
- });
33
- assert.equal(wallet.address, ANVIL_ADDRESS);
34
- });
35
-
36
- it("derives correct address via PrivateKeySigner", () => {
37
- const signer = new PrivateKeySigner(ANVIL_PK);
38
- assert.equal(signer.address, ANVIL_ADDRESS);
39
- });
40
-
41
- it("derives correct address via RawKeySigner", () => {
42
- const signer = new RawKeySigner(ANVIL_PK);
43
- assert.equal(signer.address, ANVIL_ADDRESS);
44
- });
45
-
46
- it("works without 0x prefix", () => {
47
- const signer = new PrivateKeySigner(ANVIL_PK.slice(2));
48
- assert.equal(signer.address, ANVIL_ADDRESS);
49
- });
50
- });
51
-
52
- describe("EIP-712 signing round-trip", () => {
53
- it("PrivateKeySigner produces recoverable EIP-712 signature", async () => {
54
- const signer = new PrivateKeySigner(ANVIL_PK);
55
-
56
- const domain = {
57
- name: "pay",
58
- version: "0.1",
59
- chainId: BigInt(TEST_CHAIN_ID),
60
- verifyingContract: TEST_ROUTER,
61
- };
62
- const types = {
63
- APIRequest: [
64
- { name: "method", type: "string" },
65
- { name: "path", type: "string" },
66
- { name: "timestamp", type: "uint256" },
67
- { name: "nonce", type: "bytes32" },
68
- ],
69
- };
70
- const nonce =
71
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
72
- const message = {
73
- method: "POST",
74
- path: "/api/v1/direct",
75
- timestamp: BigInt(1741400000),
76
- nonce,
77
- };
78
-
79
- const signature = await signer.signTypedData(domain, types, message);
80
-
81
- assert.ok(signature.startsWith("0x"), "signature should be hex");
82
- assert.equal(signature.length, 132, "signature should be 65 bytes hex");
83
- assert.notEqual(
84
- signature,
85
- "0x" + "0".repeat(130),
86
- "signature must not be zeros (stub)"
87
- );
88
-
89
- // Recover the signer address from the signature
90
- const recovered = await recoverAddress({
91
- hash: (await import("viem")).hashTypedData({
92
- domain,
93
- types,
94
- primaryType: "APIRequest",
95
- message,
96
- }),
97
- signature: signature as Hex,
98
- });
99
-
100
- assert.equal(
101
- recovered.toLowerCase(),
102
- ANVIL_ADDRESS.toLowerCase(),
103
- "recovered address must match signer"
104
- );
105
- });
106
- });
107
-
108
- describe("buildAuthHeaders", () => {
109
- it("produces valid auth headers with correct address", async () => {
110
- const headers = await buildAuthHeaders(ANVIL_PK, "POST", "/api/v1/direct", {
111
- chainId: TEST_CHAIN_ID,
112
- routerAddress: TEST_ROUTER,
113
- });
114
-
115
- assert.equal(headers["X-Pay-Agent"], ANVIL_ADDRESS);
116
- assert.ok(
117
- headers["X-Pay-Signature"].startsWith("0x"),
118
- "signature should be hex"
119
- );
120
- assert.equal(
121
- headers["X-Pay-Signature"].length,
122
- 132,
123
- "signature should be 65 bytes"
124
- );
125
- assert.ok(
126
- Number(headers["X-Pay-Timestamp"]) > 0,
127
- "timestamp should be positive"
128
- );
129
- assert.ok(
130
- headers["X-Pay-Nonce"].startsWith("0x"),
131
- "nonce should be hex"
132
- );
133
- assert.equal(
134
- headers["X-Pay-Nonce"].length,
135
- 66,
136
- "nonce should be 32 bytes hex"
137
- );
138
- });
139
-
140
- it("signature recovers to the correct address", async () => {
141
- const headers = await buildAuthHeaders(ANVIL_PK, "POST", "/api/v1/direct", {
142
- chainId: TEST_CHAIN_ID,
143
- routerAddress: TEST_ROUTER,
144
- });
145
-
146
- // Recompute the hash and recover
147
- const { hashTypedData } = await import("viem");
148
- const hash = hashTypedData({
149
- domain: {
150
- name: "pay",
151
- version: "0.1",
152
- chainId: TEST_CHAIN_ID,
153
- verifyingContract: TEST_ROUTER,
154
- },
155
- types: {
156
- APIRequest: [
157
- { name: "method", type: "string" },
158
- { name: "path", type: "string" },
159
- { name: "timestamp", type: "uint256" },
160
- { name: "nonce", type: "bytes32" },
161
- ],
162
- },
163
- primaryType: "APIRequest",
164
- message: {
165
- method: "POST",
166
- path: "/api/v1/direct",
167
- timestamp: BigInt(headers["X-Pay-Timestamp"]),
168
- nonce: headers["X-Pay-Nonce"] as Hex,
169
- },
170
- });
171
-
172
- const recovered = await recoverAddress({
173
- hash,
174
- signature: headers["X-Pay-Signature"] as Hex,
175
- });
176
-
177
- assert.equal(
178
- recovered.toLowerCase(),
179
- ANVIL_ADDRESS.toLowerCase(),
180
- "recovered address must match signer"
181
- );
182
- });
183
- });
184
-
185
- describe("computeEip712Hash", () => {
186
- it("produces deterministic output", () => {
187
- const nonce =
188
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
189
- const h1 = computeEip712Hash(
190
- "POST",
191
- "/api/v1/direct",
192
- BigInt(1741400000),
193
- nonce,
194
- TEST_CHAIN_ID,
195
- TEST_ROUTER
196
- );
197
- const h2 = computeEip712Hash(
198
- "POST",
199
- "/api/v1/direct",
200
- BigInt(1741400000),
201
- nonce,
202
- TEST_CHAIN_ID,
203
- TEST_ROUTER
204
- );
205
- assert.deepEqual(h1, h2);
206
- });
207
-
208
- it("different methods produce different hashes", () => {
209
- const nonce =
210
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
211
- const h1 = computeEip712Hash(
212
- "POST",
213
- "/api/v1/direct",
214
- BigInt(1741400000),
215
- nonce,
216
- TEST_CHAIN_ID,
217
- TEST_ROUTER
218
- );
219
- const h2 = computeEip712Hash(
220
- "GET",
221
- "/api/v1/direct",
222
- BigInt(1741400000),
223
- nonce,
224
- TEST_CHAIN_ID,
225
- TEST_ROUTER
226
- );
227
- assert.notDeepEqual(h1, h2);
228
- });
229
-
230
- it("different chain IDs produce different hashes", () => {
231
- const nonce =
232
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
233
- const h1 = computeEip712Hash(
234
- "POST",
235
- "/api/v1/direct",
236
- BigInt(1741400000),
237
- nonce,
238
- 8453,
239
- TEST_ROUTER
240
- );
241
- const h2 = computeEip712Hash(
242
- "POST",
243
- "/api/v1/direct",
244
- BigInt(1741400000),
245
- nonce,
246
- 84531,
247
- TEST_ROUTER
248
- );
249
- assert.notDeepEqual(h1, h2);
250
- });
251
- });
1
+ /**
2
+ * Crypto round-trip tests — proves that:
3
+ * 1. Address derivation uses real secp256k1
4
+ * 2. buildAuthHeaders produces valid recoverable signatures
5
+ */
6
+
7
+ import { describe, it } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { recoverAddress, type Hex, type Address } from "viem";
10
+
11
+ import { Wallet } from "../src/wallet.js";
12
+ import { buildAuthHeaders, buildAuthHeadersSigned, computeEip712Hash } from "../src/auth.js";
13
+
14
+ // Anvil account #0 well-known test key
15
+ const ANVIL_PK =
16
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
17
+ const ANVIL_ADDRESS =
18
+ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address;
19
+
20
+ const TEST_ROUTER =
21
+ "0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
22
+ const TEST_CHAIN_ID = 8453;
23
+
24
+ describe("Address derivation", () => {
25
+ it("derives correct address from Anvil #0 private key", () => {
26
+ const wallet = new Wallet({ privateKey: ANVIL_PK });
27
+ assert.equal(wallet.address, ANVIL_ADDRESS);
28
+ });
29
+
30
+ it("works without 0x prefix", () => {
31
+ const wallet = new Wallet({ privateKey: ANVIL_PK.slice(2) });
32
+ assert.equal(wallet.address, ANVIL_ADDRESS);
33
+ });
34
+ });
35
+
36
+ describe("buildAuthHeaders", () => {
37
+ it("produces valid auth headers with correct address", async () => {
38
+ const headers = await buildAuthHeaders(
39
+ ANVIL_PK,
40
+ "POST",
41
+ "/api/v1/direct",
42
+ { chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
43
+ );
44
+ assert.equal(headers["X-Pay-Agent"], ANVIL_ADDRESS);
45
+ assert.ok(headers["X-Pay-Signature"].startsWith("0x"));
46
+ assert.equal(headers["X-Pay-Signature"].length, 132);
47
+ assert.ok(Number(headers["X-Pay-Timestamp"]) > 0);
48
+ assert.ok(headers["X-Pay-Nonce"].startsWith("0x"));
49
+ assert.equal(headers["X-Pay-Nonce"].length, 66);
50
+ });
51
+
52
+ it("signature recovers to the correct address", async () => {
53
+ const headers = await buildAuthHeaders(
54
+ ANVIL_PK,
55
+ "POST",
56
+ "/api/v1/direct",
57
+ { chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
58
+ );
59
+ const { hashTypedData } = await import("viem");
60
+ const hash = hashTypedData({
61
+ domain: {
62
+ name: "pay",
63
+ version: "0.1",
64
+ chainId: TEST_CHAIN_ID,
65
+ verifyingContract: TEST_ROUTER,
66
+ },
67
+ types: {
68
+ APIRequest: [
69
+ { name: "method", type: "string" },
70
+ { name: "path", type: "string" },
71
+ { name: "timestamp", type: "uint256" },
72
+ { name: "nonce", type: "bytes32" },
73
+ ],
74
+ },
75
+ primaryType: "APIRequest",
76
+ message: {
77
+ method: "POST",
78
+ path: "/api/v1/direct",
79
+ timestamp: BigInt(headers["X-Pay-Timestamp"]),
80
+ nonce: headers["X-Pay-Nonce"] as Hex,
81
+ },
82
+ });
83
+ const recovered = await recoverAddress({
84
+ hash,
85
+ signature: headers["X-Pay-Signature"] as Hex,
86
+ });
87
+ assert.equal(recovered.toLowerCase(), ANVIL_ADDRESS.toLowerCase());
88
+ });
89
+ });
90
+
91
+ describe("buildAuthHeadersSigned", () => {
92
+ it("produces same-structure headers as buildAuthHeaders", async () => {
93
+ const { privateKeyToAccount } = await import("viem/accounts");
94
+ const account = privateKeyToAccount(ANVIL_PK);
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ const signFn = (p: any) => account.signTypedData(p);
97
+
98
+ const headers = await buildAuthHeadersSigned(
99
+ ANVIL_ADDRESS,
100
+ signFn,
101
+ "GET",
102
+ "/api/v1/status",
103
+ { chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
104
+ );
105
+ assert.equal(headers["X-Pay-Agent"], ANVIL_ADDRESS);
106
+ assert.ok(headers["X-Pay-Signature"].startsWith("0x"));
107
+ assert.equal(headers["X-Pay-Signature"].length, 132);
108
+ });
109
+ });
110
+
111
+ describe("computeEip712Hash", () => {
112
+ const nonce =
113
+ "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
114
+
115
+ it("produces deterministic output", () => {
116
+ const h1 = computeEip712Hash(
117
+ "POST", "/api/v1/direct", BigInt(1741400000),
118
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
119
+ );
120
+ const h2 = computeEip712Hash(
121
+ "POST", "/api/v1/direct", BigInt(1741400000),
122
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
123
+ );
124
+ assert.deepEqual(h1, h2);
125
+ });
126
+
127
+ it("different methods produce different hashes", () => {
128
+ const h1 = computeEip712Hash(
129
+ "POST", "/api/v1/direct", BigInt(1741400000),
130
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
131
+ );
132
+ const h2 = computeEip712Hash(
133
+ "GET", "/api/v1/direct", BigInt(1741400000),
134
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
135
+ );
136
+ assert.notDeepEqual(h1, h2);
137
+ });
138
+ });