@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,365 +0,0 @@
1
- /**
2
- * OWS Signer unit tests.
3
- *
4
- * Uses a mock OWS module — no real @open-wallet-standard/core needed.
5
- * Tests construction, EIP-712 typed data building, signature concatenation,
6
- * BigInt serialization, error paths, and security (no key leakage).
7
- */
8
-
9
- import { describe, it } from "node:test";
10
- import assert from "node:assert/strict";
11
- import { OwsSigner } from "../src/ows-signer.js";
12
-
13
- // ── Mock OWS module ──────────────────────────────────────────────────
14
-
15
- interface SignCall {
16
- wallet: string;
17
- chain: string;
18
- json: string;
19
- passphrase?: string;
20
- }
21
-
22
- function createMockOws(options?: {
23
- accounts?: Array<{ chainId: string; address: string; derivationPath: string }>;
24
- signature?: string;
25
- recoveryId?: number;
26
- }) {
27
- const calls: SignCall[] = [];
28
- const accounts = options?.accounts ?? [
29
- {
30
- chainId: "eip155:8453",
31
- address: "0x1234567890abcdef1234567890abcdef12345678",
32
- derivationPath: "m/44'/60'/0'/0/0",
33
- },
34
- ];
35
-
36
- return {
37
- calls,
38
- getWallet(nameOrId: string) {
39
- return {
40
- id: `id-${nameOrId}`,
41
- name: nameOrId,
42
- accounts,
43
- createdAt: "2026-04-01T00:00:00Z",
44
- };
45
- },
46
- signTypedData(
47
- wallet: string,
48
- chain: string,
49
- typedDataJson: string,
50
- passphrase?: string,
51
- ) {
52
- calls.push({ wallet, chain, json: typedDataJson, passphrase });
53
- return {
54
- signature: options?.signature ?? "aa".repeat(64),
55
- recoveryId: options?.recoveryId ?? 0,
56
- };
57
- },
58
- };
59
- }
60
-
61
- // ── Construction ─────────────────────────────────────────────────────
62
-
63
- describe("OwsSigner.create", () => {
64
- it("creates signer with eip155 chain id", async () => {
65
- const mock = createMockOws();
66
- const signer = await OwsSigner.create({
67
- walletId: "pay-test",
68
- _owsModule: mock,
69
- });
70
- assert.equal(
71
- signer.address,
72
- "0x1234567890abcdef1234567890abcdef12345678",
73
- );
74
- });
75
-
76
- it("creates signer with evm chain id", async () => {
77
- const mock = createMockOws({
78
- accounts: [
79
- {
80
- chainId: "evm",
81
- address: "0xcafe",
82
- derivationPath: "m/44'/60'/0'/0/0",
83
- },
84
- ],
85
- });
86
- const signer = await OwsSigner.create({
87
- walletId: "pay-evm",
88
- _owsModule: mock,
89
- });
90
- assert.equal(signer.address, "0xcafe");
91
- });
92
-
93
- it("throws when no EVM account found", async () => {
94
- const mock = createMockOws({
95
- accounts: [
96
- {
97
- chainId: "solana",
98
- address: "Sol123",
99
- derivationPath: "m/44'/501'/0'/0'",
100
- },
101
- ],
102
- });
103
- await assert.rejects(
104
- () => OwsSigner.create({ walletId: "pay-sol", _owsModule: mock }),
105
- { message: /No EVM account found/ },
106
- );
107
- });
108
-
109
- it("throws when no accounts at all", async () => {
110
- const mock = createMockOws({ accounts: [] });
111
- await assert.rejects(
112
- () => OwsSigner.create({ walletId: "pay-empty", _owsModule: mock }),
113
- { message: /No EVM account found/ },
114
- );
115
- });
116
-
117
- it("throws clear error when OWS not installed or wallet missing", async () => {
118
- // Without _owsModule, create() will try dynamic import.
119
- // If OWS is installed (e.g. peer dep in CI), it throws "wallet not found".
120
- // If OWS is NOT installed, it throws "not installed".
121
- await assert.rejects(
122
- () => OwsSigner.create({ walletId: "pay-missing" }),
123
- { message: /not installed|wallet not found/ },
124
- );
125
- });
126
- });
127
-
128
- // ── signTypedData ────────────────────────────────────────────────────
129
-
130
- describe("OwsSigner.signTypedData", () => {
131
- const domain = {
132
- name: "Pay",
133
- version: "1",
134
- chainId: 8453,
135
- verifyingContract: "0xrouter" as const,
136
- };
137
-
138
- const types = {
139
- Request: [
140
- { name: "method", type: "string" },
141
- { name: "path", type: "string" },
142
- ],
143
- };
144
-
145
- const value = { method: "POST", path: "/api/v1/direct" };
146
-
147
- it("builds EIP-712 JSON with EIP712Domain injected", async () => {
148
- const mock = createMockOws();
149
- const signer = await OwsSigner.create({
150
- walletId: "pay-test",
151
- _owsModule: mock,
152
- });
153
-
154
- await signer.signTypedData(domain, types, value);
155
-
156
- assert.equal(mock.calls.length, 1);
157
- const parsed = JSON.parse(mock.calls[0].json);
158
-
159
- // EIP712Domain should be auto-generated from domain fields
160
- assert.deepStrictEqual(parsed.types.EIP712Domain, [
161
- { name: "name", type: "string" },
162
- { name: "version", type: "string" },
163
- { name: "chainId", type: "uint256" },
164
- { name: "verifyingContract", type: "address" },
165
- ]);
166
-
167
- // Original types preserved
168
- assert.deepStrictEqual(parsed.types.Request, types.Request);
169
-
170
- // Primary type derived from first non-EIP712Domain key
171
- assert.equal(parsed.primaryType, "Request");
172
-
173
- // Domain and message passed through
174
- assert.deepStrictEqual(parsed.domain, domain);
175
- assert.deepStrictEqual(parsed.message, value);
176
- });
177
-
178
- it("passes chain as evm always", async () => {
179
- const mock = createMockOws();
180
- const signer = await OwsSigner.create({
181
- walletId: "pay-test",
182
- _owsModule: mock,
183
- });
184
-
185
- await signer.signTypedData(domain, types, value);
186
- assert.equal(mock.calls[0].chain, "evm");
187
- });
188
-
189
- it("passes API key as passphrase", async () => {
190
- const mock = createMockOws();
191
- const signer = await OwsSigner.create({
192
- walletId: "pay-test",
193
- owsApiKey: "ows_key_secret123",
194
- _owsModule: mock,
195
- });
196
-
197
- await signer.signTypedData(domain, types, value);
198
- assert.equal(mock.calls[0].passphrase, "ows_key_secret123");
199
-
200
- // API key must NOT appear in the JSON payload
201
- assert.ok(!mock.calls[0].json.includes("ows_key_secret123"));
202
- });
203
-
204
- it("only includes domain fields that are present", async () => {
205
- const mock = createMockOws();
206
- const signer = await OwsSigner.create({
207
- walletId: "pay-test",
208
- _owsModule: mock,
209
- });
210
-
211
- // Domain with only name and chainId
212
- await signer.signTypedData(
213
- { name: "Pay", chainId: 8453 },
214
- types,
215
- value,
216
- );
217
-
218
- const parsed = JSON.parse(mock.calls[0].json);
219
- assert.deepStrictEqual(parsed.types.EIP712Domain, [
220
- { name: "name", type: "string" },
221
- { name: "chainId", type: "uint256" },
222
- ]);
223
- });
224
-
225
- it("serializes BigInt values to strings", async () => {
226
- const mock = createMockOws();
227
- const signer = await OwsSigner.create({
228
- walletId: "pay-test",
229
- _owsModule: mock,
230
- });
231
-
232
- await signer.signTypedData(domain, types, {
233
- method: "POST",
234
- path: "/direct",
235
- amount: 5000000n,
236
- });
237
-
238
- const parsed = JSON.parse(mock.calls[0].json);
239
- assert.equal(parsed.message.amount, "5000000");
240
- });
241
- });
242
-
243
- // ── Signature concatenation ──────────────────────────────────────────
244
-
245
- describe("signature concatenation", () => {
246
- it("appends v=27 when recoveryId=0", async () => {
247
- const mock = createMockOws({
248
- signature: "aa".repeat(64), // 128 hex = r+s only
249
- recoveryId: 0,
250
- });
251
- const signer = await OwsSigner.create({
252
- walletId: "pay-test",
253
- _owsModule: mock,
254
- });
255
-
256
- const sig = await signer.signTypedData({}, { R: [] }, {});
257
- assert.ok(sig.startsWith("0x"));
258
- assert.equal(sig.length, 2 + 130); // 0x + 64r + 64s + 2v
259
- assert.equal(sig.slice(-2), "1b"); // 27 = 0x1b
260
- });
261
-
262
- it("appends v=28 when recoveryId=1", async () => {
263
- const mock = createMockOws({
264
- signature: "bb".repeat(64),
265
- recoveryId: 1,
266
- });
267
- const signer = await OwsSigner.create({
268
- walletId: "pay-test",
269
- _owsModule: mock,
270
- });
271
-
272
- const sig = await signer.signTypedData({}, { R: [] }, {});
273
- assert.equal(sig.slice(-2), "1c"); // 28 = 0x1c
274
- });
275
-
276
- it("passes through 130-char RSV signature as-is", async () => {
277
- const rsv = "cc".repeat(65); // 130 hex = already has v
278
- const mock = createMockOws({ signature: rsv });
279
- const signer = await OwsSigner.create({
280
- walletId: "pay-test",
281
- _owsModule: mock,
282
- });
283
-
284
- const sig = await signer.signTypedData({}, { R: [] }, {});
285
- assert.equal(sig, `0x${rsv}`);
286
- });
287
-
288
- it("strips 0x prefix from OWS signature", async () => {
289
- const mock = createMockOws({
290
- signature: "0x" + "dd".repeat(64),
291
- recoveryId: 0,
292
- });
293
- const signer = await OwsSigner.create({
294
- walletId: "pay-test",
295
- _owsModule: mock,
296
- });
297
-
298
- const sig = await signer.signTypedData({}, { R: [] }, {});
299
- assert.ok(!sig.includes("0x0x")); // no double prefix
300
- assert.equal(sig.length, 2 + 130);
301
- });
302
-
303
- it("defaults recoveryId to 0 when undefined", async () => {
304
- const mock = createMockOws({
305
- signature: "ee".repeat(64),
306
- recoveryId: undefined,
307
- });
308
- const signer = await OwsSigner.create({
309
- walletId: "pay-test",
310
- _owsModule: mock,
311
- });
312
-
313
- const sig = await signer.signTypedData({}, { R: [] }, {});
314
- assert.equal(sig.slice(-2), "1b"); // v=27
315
- });
316
- });
317
-
318
- // ── sign() rejection ─────────────────────────────────────────────────
319
-
320
- describe("sign() raw hash rejection", () => {
321
- it("throws because OWS only supports EIP-712", async () => {
322
- const mock = createMockOws();
323
- const signer = await OwsSigner.create({
324
- walletId: "pay-test",
325
- _owsModule: mock,
326
- });
327
-
328
- assert.throws(
329
- () => signer.sign(new Uint8Array(32)),
330
- { message: /does not support raw hash signing/ },
331
- );
332
- });
333
- });
334
-
335
- // ── Security ─────────────────────────────────────────────────────────
336
-
337
- describe("security", () => {
338
- it("toJSON does not expose API key", async () => {
339
- const mock = createMockOws();
340
- const signer = await OwsSigner.create({
341
- walletId: "pay-test",
342
- owsApiKey: "ows_key_topsecret",
343
- _owsModule: mock,
344
- });
345
-
346
- const json = signer.toJSON();
347
- assert.ok(!JSON.stringify(json).includes("topsecret"));
348
- assert.equal(json.walletId, "pay-test");
349
- assert.ok(json.address);
350
- });
351
-
352
- it("inspect does not expose API key", async () => {
353
- const mock = createMockOws();
354
- const signer = await OwsSigner.create({
355
- walletId: "pay-test",
356
- owsApiKey: "ows_key_topsecret",
357
- _owsModule: mock,
358
- });
359
-
360
- const inspectFn = signer[Symbol.for("nodejs.util.inspect.custom")] as () => string;
361
- const output = inspectFn.call(signer);
362
- assert.ok(!output.includes("topsecret"));
363
- assert.ok(output.includes("pay-test"));
364
- });
365
- });
@@ -1,47 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- CallbackSigner,
5
- CliSigner,
6
- RawKeySigner,
7
- createSigner,
8
- } from "../src/signer.js";
9
-
10
- describe("CallbackSigner", () => {
11
- it("delegates to callback", () => {
12
- const sig = new Uint8Array(65).fill(1);
13
- const signer = new CallbackSigner(() => sig);
14
- const result = signer.sign(new Uint8Array(32));
15
- assert.deepEqual(result, sig);
16
- });
17
- });
18
-
19
- describe("createSigner", () => {
20
- it("creates CliSigner for cli mode", () => {
21
- const signer = createSigner("cli");
22
- assert.ok(signer instanceof CliSigner);
23
- });
24
-
25
- it("throws for raw mode without key", () => {
26
- const originalKey = process.env.PAYSKILL_KEY;
27
- delete process.env.PAYSKILL_KEY;
28
- assert.throws(() => createSigner("raw"), /No key/);
29
- if (originalKey) process.env.PAYSKILL_KEY = originalKey;
30
- });
31
-
32
- it("creates RawKeySigner with key", () => {
33
- const signer = createSigner("raw", { key: "0x" + "ab".repeat(32) });
34
- assert.ok(signer instanceof RawKeySigner);
35
- });
36
-
37
- it("creates CallbackSigner for custom mode", () => {
38
- const signer = createSigner("custom", {
39
- callback: () => new Uint8Array(65),
40
- });
41
- assert.ok(signer instanceof CallbackSigner);
42
- });
43
-
44
- it("throws for custom mode without callback", () => {
45
- assert.throws(() => createSigner("custom"), /callback/);
46
- });
47
- });
@@ -1,66 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { PayClient } from "../src/client.js";
4
- import { PayValidationError } from "../src/errors.js";
5
- import { CallbackSigner } from "../src/signer.js";
6
-
7
- const VALID_ADDR = "0x" + "a1".repeat(20);
8
- const dummySigner = new CallbackSigner(() => new Uint8Array(65));
9
-
10
- const client = new PayClient({
11
- apiUrl: "http://localhost:9999",
12
- signer: dummySigner,
13
- });
14
-
15
- describe("payDirect validation", () => {
16
- it("rejects invalid address", async () => {
17
- await assert.rejects(
18
- () => client.payDirect("not-an-address", 1_000_000),
19
- PayValidationError
20
- );
21
- });
22
-
23
- it("rejects amount below minimum", async () => {
24
- await assert.rejects(
25
- () => client.payDirect(VALID_ADDR, 500_000),
26
- PayValidationError
27
- );
28
- });
29
-
30
- it("accepts valid inputs (fails on network, not validation)", async () => {
31
- // This should fail with a network error, not validation
32
- await assert.rejects(
33
- () => client.payDirect(VALID_ADDR, 1_000_000),
34
- (err: Error) => {
35
- assert.ok(!(err instanceof PayValidationError));
36
- return true;
37
- }
38
- );
39
- });
40
- });
41
-
42
-
43
- describe("openTab validation", () => {
44
- it("rejects amount below $5 minimum", async () => {
45
- await assert.rejects(
46
- () =>
47
- client.openTab(VALID_ADDR, 1_000_000, { maxChargePerCall: 100_000 }),
48
- PayValidationError
49
- );
50
- });
51
-
52
- it("rejects zero maxChargePerCall", async () => {
53
- await assert.rejects(
54
- () => client.openTab(VALID_ADDR, 10_000_000, { maxChargePerCall: 0 }),
55
- PayValidationError
56
- );
57
- });
58
-
59
- it("rejects invalid provider address", async () => {
60
- await assert.rejects(
61
- () =>
62
- client.openTab("bad-addr", 10_000_000, { maxChargePerCall: 500_000 }),
63
- PayValidationError
64
- );
65
- });
66
- });