@pay-skill/sdk 0.1.8 → 0.1.11

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 (58) hide show
  1. package/README.md +80 -91
  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 +136 -104
  19. package/dist/wallet.d.ts.map +1 -1
  20. package/dist/wallet.js +658 -275
  21. package/dist/wallet.js.map +1 -1
  22. package/jsr.json +1 -1
  23. package/package.json +5 -2
  24. package/src/auth.ts +28 -18
  25. package/src/errors.ts +10 -3
  26. package/src/index.ts +12 -39
  27. package/src/keychain.ts +18 -0
  28. package/src/wallet.ts +1054 -355
  29. package/tests/test_auth_rejection.ts +43 -95
  30. package/tests/test_crypto.ts +59 -172
  31. package/tests/test_e2e.ts +46 -105
  32. package/tests/test_errors.ts +9 -1
  33. package/tests/test_ows.ts +153 -0
  34. package/tests/test_wallet.ts +194 -0
  35. package/dist/client.d.ts +0 -94
  36. package/dist/client.d.ts.map +0 -1
  37. package/dist/client.js +0 -443
  38. package/dist/client.js.map +0 -1
  39. package/dist/models.d.ts +0 -78
  40. package/dist/models.d.ts.map +0 -1
  41. package/dist/models.js +0 -2
  42. package/dist/models.js.map +0 -1
  43. package/dist/ows-signer.d.ts +0 -75
  44. package/dist/ows-signer.d.ts.map +0 -1
  45. package/dist/ows-signer.js +0 -130
  46. package/dist/ows-signer.js.map +0 -1
  47. package/dist/signer.d.ts +0 -46
  48. package/dist/signer.d.ts.map +0 -1
  49. package/dist/signer.js +0 -111
  50. package/dist/signer.js.map +0 -1
  51. package/src/client.ts +0 -644
  52. package/src/models.ts +0 -77
  53. package/src/ows-signer.ts +0 -223
  54. package/src/signer.ts +0 -147
  55. package/tests/test_ows_integration.ts +0 -92
  56. package/tests/test_ows_signer.ts +0 -365
  57. package/tests/test_signer.ts +0 -47
  58. package/tests/test_validation.ts +0 -66
@@ -1,33 +1,46 @@
1
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
2
+ * Auth rejection tests — proves that the Wallet sends valid auth headers
3
+ * and that servers can reject invalid auth.
6
4
  */
7
5
 
8
6
  import { describe, it, beforeEach, afterEach } from "node:test";
9
7
  import assert from "node:assert/strict";
10
- import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
8
+ import {
9
+ createServer,
10
+ type Server,
11
+ type IncomingMessage,
12
+ type ServerResponse,
13
+ } from "node:http";
11
14
  import { once } from "node:events";
12
-
13
- import { PayClient, PayServerError, CallbackSigner, RawKeySigner } from "../src/index.js";
14
15
  import type { Hex, Address } from "viem";
16
+ import { Wallet, PayServerError } from "../src/index.js";
15
17
 
16
18
  const ANVIL_PK =
17
19
  "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
20
 
25
21
  /**
26
22
  * Minimal HTTP server that enforces X-Pay-* auth headers.
27
- * Returns 401 if any required header is missing, 200 otherwise.
23
+ * /contracts is public (no auth).
24
+ * Everything else requires valid auth headers.
28
25
  */
29
26
  function createAuthServer(): Server {
30
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
+
31
44
  const agent = req.headers["x-pay-agent"];
32
45
  const sig = req.headers["x-pay-signature"];
33
46
  const ts = req.headers["x-pay-timestamp"];
@@ -39,7 +52,6 @@ function createAuthServer(): Server {
39
52
  return;
40
53
  }
41
54
 
42
- // Check that sig looks like a real 65-byte hex signature
43
55
  const sigStr = Array.isArray(sig) ? sig[0] : sig;
44
56
  if (!sigStr.startsWith("0x") || sigStr.length !== 132) {
45
57
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -47,21 +59,15 @@ function createAuthServer(): Server {
47
59
  return;
48
60
  }
49
61
 
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
62
+ // Auth passed
58
63
  res.writeHead(200, { "Content-Type": "application/json" });
59
64
  res.end(
60
65
  JSON.stringify({
61
- address: agent,
62
- balance: 100_000_000,
63
- open_tabs: [],
64
- })
66
+ wallet: agent,
67
+ balance_usdc: "100000000",
68
+ open_tabs: 0,
69
+ total_locked: 0,
70
+ }),
65
71
  );
66
72
  });
67
73
  }
@@ -69,14 +75,14 @@ function createAuthServer(): Server {
69
75
  let server: Server;
70
76
  let baseUrl: string;
71
77
 
72
- describe("Auth rejection", () => {
78
+ describe("Auth with new Wallet class", () => {
73
79
  beforeEach(async () => {
74
80
  server = createAuthServer();
75
- server.listen(0); // random port
81
+ server.listen(0);
76
82
  await once(server, "listening");
77
83
  const addr = server.address();
78
84
  if (typeof addr === "object" && addr) {
79
- baseUrl = `http://127.0.0.1:${addr.port}`;
85
+ baseUrl = `http://127.0.0.1:${addr.port}/api/v1`;
80
86
  }
81
87
  });
82
88
 
@@ -85,70 +91,12 @@ describe("Auth rejection", () => {
85
91
  await once(server, "close");
86
92
  });
87
93
 
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
- }
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;
153
101
  });
154
102
  });
@@ -1,149 +1,61 @@
1
1
  /**
2
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
3
+ * 1. Address derivation uses real secp256k1
4
+ * 2. buildAuthHeaders produces valid recoverable signatures
6
5
  */
7
6
 
8
7
  import { describe, it } from "node:test";
9
8
  import assert from "node:assert/strict";
10
- import { privateKeyToAccount } from "viem/accounts";
11
9
  import { recoverAddress, type Hex, type Address } from "viem";
12
10
 
13
- import { PrivateKeySigner, Wallet } from "../src/wallet.js";
14
- import { RawKeySigner } from "../src/signer.js";
15
- import { buildAuthHeaders, computeEip712Hash } from "../src/auth.js";
11
+ import { Wallet } from "../src/wallet.js";
12
+ import { buildAuthHeaders, buildAuthHeadersSigned, computeEip712Hash } from "../src/auth.js";
16
13
 
17
14
  // Anvil account #0 — well-known test key
18
15
  const ANVIL_PK =
19
16
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
20
- const ANVIL_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address;
17
+ const ANVIL_ADDRESS =
18
+ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address;
21
19
 
22
- const TEST_ROUTER = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
20
+ const TEST_ROUTER =
21
+ "0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
23
22
  const TEST_CHAIN_ID = 8453;
24
23
 
25
24
  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
- });
25
+ it("derives correct address from Anvil #0 private key", () => {
26
+ const wallet = new Wallet({ privateKey: ANVIL_PK });
33
27
  assert.equal(wallet.address, ANVIL_ADDRESS);
34
28
  });
35
29
 
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
30
  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
- );
31
+ const wallet = new Wallet({ privateKey: ANVIL_PK.slice(2) });
32
+ assert.equal(wallet.address, ANVIL_ADDRESS);
105
33
  });
106
34
  });
107
35
 
108
36
  describe("buildAuthHeaders", () => {
109
37
  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"
38
+ const headers = await buildAuthHeaders(
39
+ ANVIL_PK,
40
+ "POST",
41
+ "/api/v1/direct",
42
+ { chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
137
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);
138
50
  });
139
51
 
140
52
  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
53
+ const headers = await buildAuthHeaders(
54
+ ANVIL_PK,
55
+ "POST",
56
+ "/api/v1/direct",
57
+ { chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
58
+ );
147
59
  const { hashTypedData } = await import("viem");
148
60
  const hash = hashTypedData({
149
61
  domain: {
@@ -168,83 +80,58 @@ describe("buildAuthHeaders", () => {
168
80
  nonce: headers["X-Pay-Nonce"] as Hex,
169
81
  },
170
82
  });
171
-
172
83
  const recovered = await recoverAddress({
173
84
  hash,
174
85
  signature: headers["X-Pay-Signature"] as Hex,
175
86
  });
87
+ assert.equal(recovered.toLowerCase(), ANVIL_ADDRESS.toLowerCase());
88
+ });
89
+ });
176
90
 
177
- assert.equal(
178
- recovered.toLowerCase(),
179
- ANVIL_ADDRESS.toLowerCase(),
180
- "recovered address must match signer"
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 },
181
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);
182
108
  });
183
109
  });
184
110
 
185
111
  describe("computeEip712Hash", () => {
112
+ const nonce =
113
+ "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
114
+
186
115
  it("produces deterministic output", () => {
187
- const nonce =
188
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
189
116
  const h1 = computeEip712Hash(
190
- "POST",
191
- "/api/v1/direct",
192
- BigInt(1741400000),
193
- nonce,
194
- TEST_CHAIN_ID,
195
- TEST_ROUTER
117
+ "POST", "/api/v1/direct", BigInt(1741400000),
118
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
196
119
  );
197
120
  const h2 = computeEip712Hash(
198
- "POST",
199
- "/api/v1/direct",
200
- BigInt(1741400000),
201
- nonce,
202
- TEST_CHAIN_ID,
203
- TEST_ROUTER
121
+ "POST", "/api/v1/direct", BigInt(1741400000),
122
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
204
123
  );
205
124
  assert.deepEqual(h1, h2);
206
125
  });
207
126
 
208
127
  it("different methods produce different hashes", () => {
209
- const nonce =
210
- "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
211
128
  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
129
+ "POST", "/api/v1/direct", BigInt(1741400000),
130
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
240
131
  );
241
132
  const h2 = computeEip712Hash(
242
- "POST",
243
- "/api/v1/direct",
244
- BigInt(1741400000),
245
- nonce,
246
- 84531,
247
- TEST_ROUTER
133
+ "GET", "/api/v1/direct", BigInt(1741400000),
134
+ nonce, TEST_CHAIN_ID, TEST_ROUTER,
248
135
  );
249
136
  assert.notDeepEqual(h1, h2);
250
137
  });