@bsv/sdk 2.0.12 → 2.0.13

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 (77) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/transaction/MerklePath.js +132 -0
  7. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  10. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  11. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  12. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  13. package/dist/esm/src/transaction/MerklePath.js +132 -0
  14. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  17. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  18. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  19. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  20. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  21. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +1 -1
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/storage.md +1 -1
  26. package/docs/reference/transaction.md +40 -0
  27. package/package.json +1 -1
  28. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  29. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  30. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  31. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  32. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  33. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  34. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  35. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  36. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  37. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  38. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  39. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  40. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  41. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  42. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  43. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  44. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  45. package/src/primitives/__tests/Signature.test.ts +333 -0
  46. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  47. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  48. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  49. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  50. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  51. package/src/script/__tests/Script.additional.test.ts +100 -0
  52. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  53. package/src/script/__tests/Spend.additional.test.ts +837 -0
  54. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  55. package/src/transaction/MerklePath.ts +155 -0
  56. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  57. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  58. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  59. package/src/transaction/__tests/MerklePath.test.ts +80 -0
  60. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  61. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  62. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  63. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  64. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  65. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  66. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  67. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  68. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  69. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  70. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  71. package/src/wallet/__tests/WERR.test.ts +212 -0
  72. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  73. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  74. package/src/wallet/__tests/WalletError.test.ts +290 -0
  75. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  76. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  77. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,827 @@
1
+ "use strict";
2
+ /**
3
+ * AuthFetch additional tests.
4
+ *
5
+ * Covers branches not exercised by the primary AuthFetch.test.ts:
6
+ * - fetch(): retryCounter exhaustion, non-auth fallback path, stale-session retry
7
+ * - serializeRequest(): all header/body/URL variants
8
+ * - handleFetchAndValidate(): success, x-bsv header spoofing, non-ok response
9
+ * - handlePaymentAndRetry(): missing/invalid headers, incompatible context regeneration
10
+ * - describeRequestBodyForLogging(): all body types
11
+ * - getMaxPaymentAttempts(): edge cases
12
+ * - getPaymentRetryDelay(): values at different attempt counts
13
+ * - wait(): zero / positive
14
+ * - isPaymentContextCompatible(): match / mismatch branches
15
+ * - consumeReceivedCertificates(): drains the internal buffer
16
+ * - sendCertificateRequest(): creates new peer when none exists
17
+ * - logPaymentAttempt(): all three log levels
18
+ * - createPaymentErrorEntry(): Error vs non-Error values
19
+ * - buildPaymentFailureError(): shapes the error correctly
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const globals_1 = require("@jest/globals");
23
+ const AuthFetch_js_1 = require("../AuthFetch.js");
24
+ const index_js_1 = require("../../../primitives/index.js");
25
+ // ---------------------------------------------------------------------------
26
+ // Module mock for createNonce (matches what primary test does)
27
+ // ---------------------------------------------------------------------------
28
+ globals_1.jest.mock('../../utils/createNonce.js', () => ({
29
+ createNonce: globals_1.jest.fn()
30
+ }));
31
+ const createNonce_js_1 = require("../../utils/createNonce.js");
32
+ const createNonceMock = createNonce_js_1.createNonce;
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+ function buildWallet() {
37
+ const identityKey = new index_js_1.PrivateKey(10).toPublicKey().toString();
38
+ const derivedKey = new index_js_1.PrivateKey(11).toPublicKey().toString();
39
+ return {
40
+ getPublicKey: globals_1.jest.fn(async (opts) => opts?.identityKey === true ? { publicKey: identityKey } : { publicKey: derivedKey }),
41
+ createAction: globals_1.jest.fn(async () => ({
42
+ tx: index_js_1.Utils.toArray('mock-tx', 'utf8')
43
+ })),
44
+ createHmac: globals_1.jest.fn(async () => ({ hmac: new Array(32).fill(0) }))
45
+ };
46
+ }
47
+ function make402Response(overrides = {}) {
48
+ const headers = {
49
+ 'x-bsv-payment-version': '1.0',
50
+ 'x-bsv-payment-satoshis-required': '10',
51
+ 'x-bsv-auth-identity-key': 'srv-key',
52
+ 'x-bsv-payment-derivation-prefix': 'pfx',
53
+ ...overrides
54
+ };
55
+ return new Response('', { status: 402, headers });
56
+ }
57
+ afterEach(() => {
58
+ globals_1.jest.restoreAllMocks();
59
+ createNonceMock.mockReset();
60
+ });
61
+ // ---------------------------------------------------------------------------
62
+ // 1. fetch() – retryCounter exhaustion
63
+ // ---------------------------------------------------------------------------
64
+ describe('AuthFetch.fetch – retryCounter', () => {
65
+ it('throws when retryCounter reaches 0', async () => {
66
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
67
+ await expect(authFetch.fetch('https://example.com', { retryCounter: 0 })).rejects.toThrow('Request failed after maximum number of retries.');
68
+ });
69
+ it('decrements retryCounter before making the request', async () => {
70
+ // Verify that the stale-session retry path calls fetch() again, which means
71
+ // retryCounter gets decremented. We intercept the recursive fetch() call using
72
+ // a spy so a real Peer is never constructed inside the unit-test environment.
73
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
74
+ let fetchCallCount = 0;
75
+ const originalFetch = authFetch.fetch.bind(authFetch);
76
+ globals_1.jest.spyOn(authFetch, 'fetch').mockImplementation(async (url, config) => {
77
+ fetchCallCount++;
78
+ if (fetchCallCount === 1) {
79
+ // First call: run the real code path so the stale-session branch triggers
80
+ return originalFetch(url, config);
81
+ }
82
+ // Subsequent calls (recursive retry after stale-session): throw to prove
83
+ // the retry occurred with a decremented retryCounter.
84
+ throw new Error('second call');
85
+ });
86
+ // Inject a stub peer that throws a stale-session error on toPeer()
87
+ const peerStub = {
88
+ listenForCertificatesReceived: globals_1.jest.fn(),
89
+ listenForCertificatesRequested: globals_1.jest.fn(),
90
+ listenForGeneralMessages: globals_1.jest.fn(() => 1),
91
+ stopListeningForGeneralMessages: globals_1.jest.fn(),
92
+ toPeer: globals_1.jest.fn(async () => {
93
+ throw new Error('Session not found for nonce xyz');
94
+ })
95
+ };
96
+ authFetch.peers['https://example.com'] = {
97
+ peer: peerStub,
98
+ identityKey: 'some-key',
99
+ supportsMutualAuth: true,
100
+ pendingCertificateRequests: []
101
+ };
102
+ // With retryCounter: 2, the stale-session branch retries once; the spy
103
+ // intercepts the recursive call and throws 'second call'.
104
+ await expect(authFetch.fetch('https://example.com/path', { retryCounter: 2 })).rejects.toThrow('second call');
105
+ expect(fetchCallCount).toBe(2);
106
+ });
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // 2. fetch() – supportsMutualAuth === false fallback
110
+ // ---------------------------------------------------------------------------
111
+ describe('AuthFetch.fetch – non-auth fallback (supportsMutualAuth=false)', () => {
112
+ it('falls back to handleFetchAndValidate when peer does not support mutual auth', async () => {
113
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
114
+ const handleFetchSpy = globals_1.jest
115
+ .spyOn(authFetch, 'handleFetchAndValidate')
116
+ .mockResolvedValue(new Response('ok', { status: 200 }));
117
+ const peerStub = {
118
+ peer: { toPeer: globals_1.jest.fn() },
119
+ supportsMutualAuth: false,
120
+ pendingCertificateRequests: []
121
+ };
122
+ authFetch.peers['https://example.com'] = peerStub;
123
+ const result = await authFetch.fetch('https://example.com/resource');
124
+ expect(handleFetchSpy).toHaveBeenCalledTimes(1);
125
+ expect(result.status).toBe(200);
126
+ });
127
+ it('rejects when handleFetchAndValidate throws in non-auth fallback', async () => {
128
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
129
+ globals_1.jest
130
+ .spyOn(authFetch, 'handleFetchAndValidate')
131
+ .mockRejectedValue(new Error('fetch validation failed'));
132
+ const peerStub = {
133
+ peer: { toPeer: globals_1.jest.fn() },
134
+ supportsMutualAuth: false,
135
+ pendingCertificateRequests: []
136
+ };
137
+ authFetch.peers['https://example.com'] = peerStub;
138
+ await expect(authFetch.fetch('https://example.com/resource')).rejects.toThrow('fetch validation failed');
139
+ });
140
+ });
141
+ // ---------------------------------------------------------------------------
142
+ // 3. handleFetchAndValidate
143
+ // ---------------------------------------------------------------------------
144
+ describe('AuthFetch.handleFetchAndValidate (private)', () => {
145
+ it('returns the response when fetch succeeds with no x-bsv headers', async () => {
146
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
147
+ const mockResponse = new Response('body', {
148
+ status: 200,
149
+ headers: { 'Content-Type': 'text/plain' }
150
+ });
151
+ globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
152
+ const peerToUse = { supportsMutualAuth: undefined };
153
+ const response = await authFetch.handleFetchAndValidate('https://example.com', { method: 'GET' }, peerToUse);
154
+ expect(response.status).toBe(200);
155
+ expect(peerToUse.supportsMutualAuth).toBe(false);
156
+ });
157
+ it('throws when response contains an x-bsv header (spoofing detection)', async () => {
158
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
159
+ // The source iterates response.headers.forEach((value, name) => ...)
160
+ // and checks if the VALUE starts with 'x-bsv'. To trigger spoofing
161
+ // detection we need a header whose value starts with 'x-bsv'.
162
+ const mockResponse = new Response('', {
163
+ status: 200,
164
+ headers: { 'x-custom-header': 'x-bsv-auth-identity-key' }
165
+ });
166
+ globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
167
+ await expect(authFetch.handleFetchAndValidate('https://example.com', {}, { supportsMutualAuth: undefined })).rejects.toThrow('The server is trying to claim it has been authenticated');
168
+ });
169
+ it('throws when response is not ok', async () => {
170
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
171
+ const mockResponse = new Response('Not Found', { status: 404 });
172
+ globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
173
+ await expect(authFetch.handleFetchAndValidate('https://example.com', {}, { supportsMutualAuth: undefined })).rejects.toThrow('Request failed with status: 404');
174
+ });
175
+ });
176
+ // ---------------------------------------------------------------------------
177
+ // 4. handlePaymentAndRetry – missing/invalid headers
178
+ // ---------------------------------------------------------------------------
179
+ describe('AuthFetch.handlePaymentAndRetry – header validation', () => {
180
+ it('throws when x-bsv-payment-version header is missing', async () => {
181
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
182
+ const response = new Response('', { status: 402 }); // no payment headers
183
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Unsupported x-bsv-payment-version response header');
184
+ });
185
+ it('throws when x-bsv-payment-version header has wrong value', async () => {
186
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
187
+ const response = make402Response({ 'x-bsv-payment-version': '2.0' });
188
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Unsupported x-bsv-payment-version response header');
189
+ });
190
+ it('throws when x-bsv-payment-satoshis-required header is missing', async () => {
191
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
192
+ const response = make402Response({ 'x-bsv-payment-satoshis-required': '' });
193
+ // Force re-check: create a response without the satoshis header entirely
194
+ const headersRaw = {
195
+ 'x-bsv-payment-version': '1.0',
196
+ 'x-bsv-auth-identity-key': 'srv-key',
197
+ 'x-bsv-payment-derivation-prefix': 'pfx'
198
+ // satoshis-required intentionally omitted
199
+ };
200
+ const respNoSatoshis = new Response('', { status: 402, headers: headersRaw });
201
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, respNoSatoshis)).rejects.toThrow('Missing x-bsv-payment-satoshis-required response header');
202
+ });
203
+ it('throws when satoshis value is NaN', async () => {
204
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
205
+ const response = make402Response({ 'x-bsv-payment-satoshis-required': 'not-a-number' });
206
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Invalid x-bsv-payment-satoshis-required response header value');
207
+ });
208
+ it('throws when satoshis value is zero', async () => {
209
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
210
+ const response = make402Response({ 'x-bsv-payment-satoshis-required': '0' });
211
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Invalid x-bsv-payment-satoshis-required response header value');
212
+ });
213
+ it('throws when x-bsv-auth-identity-key header is missing', async () => {
214
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
215
+ const headersRaw = {
216
+ 'x-bsv-payment-version': '1.0',
217
+ 'x-bsv-payment-satoshis-required': '10',
218
+ 'x-bsv-payment-derivation-prefix': 'pfx'
219
+ // identity-key omitted
220
+ };
221
+ const response = new Response('', { status: 402, headers: headersRaw });
222
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-auth-identity-key response header');
223
+ });
224
+ it('throws when x-bsv-payment-derivation-prefix header is missing', async () => {
225
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
226
+ const headersRaw = {
227
+ 'x-bsv-payment-version': '1.0',
228
+ 'x-bsv-payment-satoshis-required': '10',
229
+ 'x-bsv-auth-identity-key': 'srv-key'
230
+ // derivation-prefix omitted
231
+ };
232
+ const response = new Response('', { status: 402, headers: headersRaw });
233
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-payment-derivation-prefix response header');
234
+ });
235
+ it('throws when derivation-prefix is an empty string', async () => {
236
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
237
+ const response = make402Response({ 'x-bsv-payment-derivation-prefix': '' });
238
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-payment-derivation-prefix response header');
239
+ });
240
+ });
241
+ // ---------------------------------------------------------------------------
242
+ // 5. handlePaymentAndRetry – incompatible context triggers new context creation
243
+ // ---------------------------------------------------------------------------
244
+ describe('AuthFetch.handlePaymentAndRetry – context compatibility', () => {
245
+ it('regenerates context when server changes payment requirements', async () => {
246
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
247
+ globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
248
+ globals_1.jest.spyOn(authFetch, 'wait').mockResolvedValue(undefined);
249
+ createNonceMock.mockResolvedValue('new-suffix');
250
+ const existingContext = {
251
+ satoshisRequired: 5, // server now asks for 10
252
+ transactionBase64: index_js_1.Utils.toBase64([1, 2, 3]),
253
+ derivationPrefix: 'pfx',
254
+ derivationSuffix: 'old-suffix',
255
+ serverIdentityKey: 'srv-key',
256
+ clientIdentityKey: 'client-key',
257
+ attempts: 0,
258
+ maxAttempts: 3,
259
+ errors: [],
260
+ requestSummary: {
261
+ url: 'https://example.com',
262
+ method: 'GET',
263
+ headers: {},
264
+ bodyType: 'none',
265
+ bodyByteLength: 0
266
+ }
267
+ };
268
+ const fetchSpy = globals_1.jest
269
+ .spyOn(authFetch, 'fetch')
270
+ .mockResolvedValue(new Response('ok', { status: 200 }));
271
+ const response = make402Response({ 'x-bsv-payment-satoshis-required': '10' }); // changed from 5
272
+ await authFetch.handlePaymentAndRetry('https://example.com', { paymentContext: existingContext }, response);
273
+ // createNonce should have been called because the context was regenerated
274
+ expect(createNonceMock).toHaveBeenCalled();
275
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
276
+ });
277
+ });
278
+ // ---------------------------------------------------------------------------
279
+ // 6. handlePaymentAndRetry – maxAttempts exceeded before first try
280
+ // ---------------------------------------------------------------------------
281
+ describe('AuthFetch.handlePaymentAndRetry – maxAttempts exceeded pre-check', () => {
282
+ it('throws immediately when attempts >= maxAttempts', async () => {
283
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
284
+ globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
285
+ const exhaustedContext = {
286
+ satoshisRequired: 10,
287
+ transactionBase64: index_js_1.Utils.toBase64([1]),
288
+ derivationPrefix: 'pfx',
289
+ derivationSuffix: 'sfx',
290
+ serverIdentityKey: 'srv-key',
291
+ clientIdentityKey: 'client-key',
292
+ attempts: 3,
293
+ maxAttempts: 3,
294
+ errors: [],
295
+ requestSummary: {
296
+ url: 'https://example.com',
297
+ method: 'GET',
298
+ headers: {},
299
+ bodyType: 'none',
300
+ bodyByteLength: 0
301
+ }
302
+ };
303
+ const response = make402Response();
304
+ await expect(authFetch.handlePaymentAndRetry('https://example.com', { paymentContext: exhaustedContext }, response)).rejects.toThrow('Paid request to https://example.com failed after 3/3 attempts');
305
+ });
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // 7. serializeRequest – header and body branches
309
+ // ---------------------------------------------------------------------------
310
+ describe('AuthFetch.serializeRequest (private)', () => {
311
+ it('serializes a GET request with no body or headers', async () => {
312
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
313
+ const nonce = new Array(32).fill(0);
314
+ const writer = await authFetch.serializeRequest('GET', {}, undefined, new URL('https://example.com/path'), nonce);
315
+ expect(writer).toBeDefined();
316
+ expect(writer.toArray().length).toBeGreaterThan(32);
317
+ });
318
+ it('serializes a POST request with a JSON body', async () => {
319
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
320
+ const nonce = new Array(32).fill(1);
321
+ const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json' }, { hello: 'world' }, new URL('https://example.com/api'), nonce);
322
+ expect(writer.toArray().length).toBeGreaterThan(32);
323
+ });
324
+ it('serializes a request with search params', async () => {
325
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
326
+ const nonce = new Array(32).fill(2);
327
+ const writer = await authFetch.serializeRequest('GET', {}, undefined, new URL('https://example.com/api?q=hello'), nonce);
328
+ expect(writer.toArray().length).toBeGreaterThan(32);
329
+ });
330
+ it('includes x-bsv-* custom headers', async () => {
331
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
332
+ const nonce = new Array(32).fill(3);
333
+ const writer = await authFetch.serializeRequest('GET', { 'x-bsv-custom': 'value123' }, undefined, new URL('https://example.com/'), nonce);
334
+ expect(writer.toArray().length).toBeGreaterThan(32);
335
+ });
336
+ it('includes authorization header', async () => {
337
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
338
+ const nonce = new Array(32).fill(4);
339
+ const writer = await authFetch.serializeRequest('GET', { authorization: 'Bearer token123' }, undefined, new URL('https://example.com/'), nonce);
340
+ expect(writer.toArray().length).toBeGreaterThan(32);
341
+ });
342
+ it('throws for x-bsv-auth-* headers', async () => {
343
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
344
+ const nonce = new Array(32).fill(5);
345
+ await expect(authFetch.serializeRequest('GET', { 'x-bsv-auth-identity-key': 'spoofed' }, undefined, new URL('https://example.com/'), nonce)).rejects.toThrow('No BSV auth headers allowed here!');
346
+ });
347
+ it('throws for unsupported headers', async () => {
348
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
349
+ const nonce = new Array(32).fill(6);
350
+ await expect(authFetch.serializeRequest('GET', { 'accept': 'application/json' }, undefined, new URL('https://example.com/'), nonce)).rejects.toThrow('Unsupported header');
351
+ });
352
+ it('normalizes content-type by stripping parameters', async () => {
353
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
354
+ const nonce = new Array(32).fill(7);
355
+ // Should not throw — content-type is allowed but parameters are stripped
356
+ const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json; charset=utf-8' }, '{"x":1}', new URL('https://example.com/api'), nonce);
357
+ expect(writer.toArray().length).toBeGreaterThan(32);
358
+ });
359
+ it('defaults POST body to {} when content-type is application/json and body is undefined', async () => {
360
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
361
+ const nonce = new Array(32).fill(8);
362
+ // Should not throw
363
+ const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json' }, undefined, new URL('https://example.com/api'), nonce);
364
+ expect(writer.toArray().length).toBeGreaterThan(32);
365
+ });
366
+ it('defaults DELETE body to empty string when no content-type and body is undefined', async () => {
367
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
368
+ const nonce = new Array(32).fill(9);
369
+ const writer = await authFetch.serializeRequest('DELETE', {}, undefined, new URL('https://example.com/resource'), nonce);
370
+ expect(writer.toArray().length).toBeGreaterThan(32);
371
+ });
372
+ });
373
+ // ---------------------------------------------------------------------------
374
+ // 8. describeRequestBodyForLogging – all body types
375
+ // ---------------------------------------------------------------------------
376
+ describe('AuthFetch.describeRequestBodyForLogging (private)', () => {
377
+ let authFetch;
378
+ beforeEach(() => {
379
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
380
+ });
381
+ it('returns type=none for null body', () => {
382
+ const result = authFetch.describeRequestBodyForLogging(null);
383
+ expect(result).toEqual({ type: 'none', byteLength: 0 });
384
+ });
385
+ it('returns type=none for undefined body', () => {
386
+ const result = authFetch.describeRequestBodyForLogging(undefined);
387
+ expect(result).toEqual({ type: 'none', byteLength: 0 });
388
+ });
389
+ it('returns type=string with correct byteLength', () => {
390
+ const result = authFetch.describeRequestBodyForLogging('hello');
391
+ expect(result.type).toBe('string');
392
+ expect(result.byteLength).toBe(5);
393
+ });
394
+ it('returns type=number[] for number array', () => {
395
+ const result = authFetch.describeRequestBodyForLogging([1, 2, 3]);
396
+ expect(result).toEqual({ type: 'number[]', byteLength: 3 });
397
+ });
398
+ it('returns type=array for non-number array', () => {
399
+ const result = authFetch.describeRequestBodyForLogging(['a', 'b']);
400
+ expect(result).toEqual({ type: 'array', byteLength: 2 });
401
+ });
402
+ it('returns type=ArrayBuffer for ArrayBuffer', () => {
403
+ const buf = new ArrayBuffer(8);
404
+ const result = authFetch.describeRequestBodyForLogging(buf);
405
+ expect(result).toEqual({ type: 'ArrayBuffer', byteLength: 8 });
406
+ });
407
+ it('returns typed array name for Uint8Array', () => {
408
+ const arr = new Uint8Array([1, 2, 3, 4]);
409
+ const result = authFetch.describeRequestBodyForLogging(arr);
410
+ expect(result.type).toBe('Uint8Array');
411
+ expect(result.byteLength).toBe(4);
412
+ });
413
+ it('returns type=Blob for Blob', () => {
414
+ const blob = new Blob(['hello world']);
415
+ const result = authFetch.describeRequestBodyForLogging(blob);
416
+ expect(result.type).toBe('Blob');
417
+ expect(result.byteLength).toBeGreaterThan(0);
418
+ });
419
+ it('returns type=URLSearchParams for URLSearchParams', () => {
420
+ const params = new URLSearchParams({ key: 'value' });
421
+ const result = authFetch.describeRequestBodyForLogging(params);
422
+ expect(result.type).toBe('URLSearchParams');
423
+ expect(result.byteLength).toBeGreaterThan(0);
424
+ });
425
+ it('returns type=FormData for FormData', () => {
426
+ const fd = new FormData();
427
+ fd.append('field', 'value');
428
+ const result = authFetch.describeRequestBodyForLogging(fd);
429
+ expect(result.type).toBe('FormData');
430
+ expect(result.byteLength).toBe(0);
431
+ });
432
+ it('returns type=object for a plain object', () => {
433
+ const result = authFetch.describeRequestBodyForLogging({ a: 1 });
434
+ expect(result.type).toBe('object');
435
+ expect(result.byteLength).toBeGreaterThan(0);
436
+ });
437
+ it('returns type=ReadableStream for ReadableStream', () => {
438
+ const stream = new ReadableStream();
439
+ const result = authFetch.describeRequestBodyForLogging(stream);
440
+ expect(result).toEqual({ type: 'ReadableStream', byteLength: 0 });
441
+ });
442
+ it('falls back to typeof for an unrecognised type', () => {
443
+ // A Symbol cannot be JSON-stringified, triggering the fallback
444
+ const sym = Symbol('test');
445
+ const result = authFetch.describeRequestBodyForLogging(sym);
446
+ expect(result.type).toBe('symbol');
447
+ expect(result.byteLength).toBe(0);
448
+ });
449
+ });
450
+ // ---------------------------------------------------------------------------
451
+ // 9. normalizeBodyToNumberArray – edge cases
452
+ // ---------------------------------------------------------------------------
453
+ describe('AuthFetch.normalizeBodyToNumberArray (private)', () => {
454
+ let authFetch;
455
+ beforeEach(() => {
456
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
457
+ });
458
+ it('returns [] for null', async () => {
459
+ const result = await authFetch.normalizeBodyToNumberArray(null);
460
+ expect(result).toEqual([]);
461
+ });
462
+ it('returns [] for undefined', async () => {
463
+ const result = await authFetch.normalizeBodyToNumberArray(undefined);
464
+ expect(result).toEqual([]);
465
+ });
466
+ it('converts a string to a number array', async () => {
467
+ const result = await authFetch.normalizeBodyToNumberArray('abc');
468
+ expect(result.length).toBe(3);
469
+ });
470
+ it('converts a number[] to a JSON-encoded number array', async () => {
471
+ // Arrays are objects, so they hit the typeof === 'object' branch first
472
+ // and are serialized via JSON.stringify before the number[] guard runs.
473
+ const input = [1, 2, 3];
474
+ const result = await authFetch.normalizeBodyToNumberArray(input);
475
+ // '[1,2,3]' encoded as UTF-8 bytes
476
+ const expected = index_js_1.Utils.toArray(JSON.stringify(input), 'utf8');
477
+ expect(result).toEqual(expected);
478
+ });
479
+ it('converts ArrayBuffer to a JSON-encoded number array', async () => {
480
+ // ArrayBuffer is an object, so it hits the typeof === 'object' branch first.
481
+ const buf = new Uint8Array([10, 20, 30]).buffer;
482
+ const result = await authFetch.normalizeBodyToNumberArray(buf);
483
+ // JSON.stringify of an ArrayBuffer produces '{}'
484
+ const expected = index_js_1.Utils.toArray(JSON.stringify(buf), 'utf8');
485
+ expect(result).toEqual(expected);
486
+ });
487
+ it('converts Uint8Array to a JSON-encoded number array', async () => {
488
+ // Uint8Array is an object, so it hits the typeof === 'object' branch first.
489
+ const arr = new Uint8Array([5, 6, 7]);
490
+ const result = await authFetch.normalizeBodyToNumberArray(arr);
491
+ // JSON.stringify of a Uint8Array produces e.g. '{"0":5,"1":6,"2":7}'
492
+ const expected = index_js_1.Utils.toArray(JSON.stringify(arr), 'utf8');
493
+ expect(result).toEqual(expected);
494
+ });
495
+ it('converts Blob via JSON.stringify (object branch)', async () => {
496
+ // Blob is an object — hits the typeof === 'object' branch before the Blob check.
497
+ const blob = new Blob(['hi']);
498
+ const result = await authFetch.normalizeBodyToNumberArray(blob);
499
+ // JSON.stringify(new Blob(...)) → '{}'
500
+ const expected = index_js_1.Utils.toArray(JSON.stringify(blob), 'utf8');
501
+ expect(result).toEqual(expected);
502
+ });
503
+ it('converts FormData via JSON.stringify (object branch)', async () => {
504
+ // FormData is an object — hits the typeof === 'object' branch before the FormData check.
505
+ const fd = new FormData();
506
+ fd.append('name', 'alice');
507
+ const result = await authFetch.normalizeBodyToNumberArray(fd);
508
+ // JSON.stringify(FormData) → '{}'
509
+ const expected = index_js_1.Utils.toArray(JSON.stringify(fd), 'utf8');
510
+ expect(result).toEqual(expected);
511
+ });
512
+ it('converts URLSearchParams via JSON.stringify (object branch)', async () => {
513
+ // URLSearchParams is an object — hits typeof === 'object' branch first.
514
+ const params = new URLSearchParams({ q: 'hello' });
515
+ const result = await authFetch.normalizeBodyToNumberArray(params);
516
+ // JSON.stringify(URLSearchParams) → '{}'
517
+ const expected = index_js_1.Utils.toArray(JSON.stringify(params), 'utf8');
518
+ expect(result).toEqual(expected);
519
+ });
520
+ it('converts ReadableStream via JSON.stringify (object branch)', async () => {
521
+ // ReadableStream is an object, so it hits the typeof === 'object' branch first
522
+ // and is serialized via JSON.stringify (produces '{}') rather than throwing.
523
+ const stream = new ReadableStream();
524
+ const result = await authFetch.normalizeBodyToNumberArray(stream);
525
+ const expected = index_js_1.Utils.toArray(JSON.stringify(stream), 'utf8');
526
+ expect(result).toEqual(expected);
527
+ });
528
+ it('converts a plain object via JSON.stringify', async () => {
529
+ const obj = { key: 'value' };
530
+ const result = await authFetch.normalizeBodyToNumberArray(obj);
531
+ expect(result.length).toBeGreaterThan(0);
532
+ });
533
+ });
534
+ // ---------------------------------------------------------------------------
535
+ // 10. getMaxPaymentAttempts
536
+ // ---------------------------------------------------------------------------
537
+ describe('AuthFetch.getMaxPaymentAttempts (private)', () => {
538
+ let authFetch;
539
+ beforeEach(() => {
540
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
541
+ });
542
+ it('returns 3 by default', () => {
543
+ expect(authFetch.getMaxPaymentAttempts({})).toBe(3);
544
+ });
545
+ it('returns the configured positive integer', () => {
546
+ expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 5 })).toBe(5);
547
+ });
548
+ it('floors the value for a float', () => {
549
+ expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 4.9 })).toBe(4);
550
+ });
551
+ it('returns 3 when paymentRetryAttempts is 0', () => {
552
+ expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 0 })).toBe(3);
553
+ });
554
+ it('returns 3 when paymentRetryAttempts is negative', () => {
555
+ expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: -1 })).toBe(3);
556
+ });
557
+ it('returns 3 when paymentRetryAttempts is a string', () => {
558
+ expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 'five' })).toBe(3);
559
+ });
560
+ });
561
+ // ---------------------------------------------------------------------------
562
+ // 11. getPaymentRetryDelay
563
+ // ---------------------------------------------------------------------------
564
+ describe('AuthFetch.getPaymentRetryDelay (private)', () => {
565
+ let authFetch;
566
+ beforeEach(() => {
567
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
568
+ });
569
+ it('returns 250 for attempt 1 (250 * 1)', () => {
570
+ expect(authFetch.getPaymentRetryDelay(1)).toBe(250);
571
+ });
572
+ it('returns 500 for attempt 2', () => {
573
+ expect(authFetch.getPaymentRetryDelay(2)).toBe(500);
574
+ });
575
+ it('caps multiplier at 5 for attempt >= 5', () => {
576
+ expect(authFetch.getPaymentRetryDelay(5)).toBe(1250);
577
+ expect(authFetch.getPaymentRetryDelay(10)).toBe(1250);
578
+ expect(authFetch.getPaymentRetryDelay(100)).toBe(1250);
579
+ });
580
+ });
581
+ // ---------------------------------------------------------------------------
582
+ // 12. wait()
583
+ // ---------------------------------------------------------------------------
584
+ describe('AuthFetch.wait (private)', () => {
585
+ it('resolves immediately for ms <= 0', async () => {
586
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
587
+ const start = Date.now();
588
+ await authFetch.wait(0);
589
+ expect(Date.now() - start).toBeLessThan(50);
590
+ });
591
+ it('resolves immediately for negative ms', async () => {
592
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
593
+ const start = Date.now();
594
+ await authFetch.wait(-100);
595
+ expect(Date.now() - start).toBeLessThan(50);
596
+ });
597
+ it('uses a timer for positive ms', async () => {
598
+ globals_1.jest.useFakeTimers();
599
+ try {
600
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
601
+ let resolved = false;
602
+ const promise = authFetch.wait(500).then(() => { resolved = true; });
603
+ expect(resolved).toBe(false);
604
+ await globals_1.jest.advanceTimersByTimeAsync(500);
605
+ await promise;
606
+ expect(resolved).toBe(true);
607
+ }
608
+ finally {
609
+ globals_1.jest.useRealTimers();
610
+ }
611
+ });
612
+ });
613
+ // ---------------------------------------------------------------------------
614
+ // 13. isPaymentContextCompatible
615
+ // ---------------------------------------------------------------------------
616
+ describe('AuthFetch.isPaymentContextCompatible (private)', () => {
617
+ let authFetch;
618
+ beforeEach(() => {
619
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
620
+ });
621
+ function makeCtx(overrides = {}) {
622
+ return {
623
+ satoshisRequired: 10,
624
+ serverIdentityKey: 'srv',
625
+ derivationPrefix: 'pfx',
626
+ ...overrides
627
+ };
628
+ }
629
+ it('returns true when all fields match', () => {
630
+ const ctx = makeCtx();
631
+ expect(authFetch.isPaymentContextCompatible(ctx, 10, 'srv', 'pfx')).toBe(true);
632
+ });
633
+ it('returns false when satoshis differ', () => {
634
+ const ctx = makeCtx();
635
+ expect(authFetch.isPaymentContextCompatible(ctx, 20, 'srv', 'pfx')).toBe(false);
636
+ });
637
+ it('returns false when serverIdentityKey differs', () => {
638
+ const ctx = makeCtx();
639
+ expect(authFetch.isPaymentContextCompatible(ctx, 10, 'other', 'pfx')).toBe(false);
640
+ });
641
+ it('returns false when derivationPrefix differs', () => {
642
+ const ctx = makeCtx();
643
+ expect(authFetch.isPaymentContextCompatible(ctx, 10, 'srv', 'other')).toBe(false);
644
+ });
645
+ });
646
+ // ---------------------------------------------------------------------------
647
+ // 14. consumeReceivedCertificates
648
+ // ---------------------------------------------------------------------------
649
+ describe('AuthFetch.consumeReceivedCertificates', () => {
650
+ it('returns all received certs and empties the buffer', () => {
651
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
652
+ authFetch.certificatesReceived.push({ serialNumber: 'cert1' }, { serialNumber: 'cert2' });
653
+ const certs = authFetch.consumeReceivedCertificates();
654
+ expect(certs).toHaveLength(2);
655
+ expect(certs[0]).toMatchObject({ serialNumber: 'cert1' });
656
+ // Buffer should now be empty
657
+ const second = authFetch.consumeReceivedCertificates();
658
+ expect(second).toHaveLength(0);
659
+ });
660
+ it('returns an empty array when no certs have been received', () => {
661
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
662
+ expect(authFetch.consumeReceivedCertificates()).toEqual([]);
663
+ });
664
+ });
665
+ // ---------------------------------------------------------------------------
666
+ // 15. sendCertificateRequest – creates a new peer when none exists
667
+ // ---------------------------------------------------------------------------
668
+ describe('AuthFetch.sendCertificateRequest – new peer creation', () => {
669
+ it('creates a new Peer transport when no peer exists for the base URL', async () => {
670
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
671
+ // Verify there is no existing peer
672
+ expect(authFetch.peers['https://new-server.com']).toBeUndefined();
673
+ const fakeCerts = [{ serialNumber: 'abc' }];
674
+ let capturedListener;
675
+ const peerProto = {
676
+ listenForCertificatesReceived: globals_1.jest.fn((cb) => {
677
+ capturedListener = cb;
678
+ return 99;
679
+ }),
680
+ stopListeningForCertificatesReceived: globals_1.jest.fn(),
681
+ requestCertificates: globals_1.jest.fn(async () => {
682
+ capturedListener?.('srv-key', fakeCerts);
683
+ })
684
+ };
685
+ // Intercept Peer constructor by injecting peer directly after fetch call starts
686
+ const origFetch = authFetch.sendCertificateRequest.bind(authFetch);
687
+ globals_1.jest.spyOn(authFetch, 'sendCertificateRequest').mockImplementationOnce(async (url, certs) => {
688
+ // Manually inject our stub peer so Peer constructor is bypassed
689
+ ;
690
+ authFetch.peers['https://new-server.com'] = {
691
+ peer: peerProto,
692
+ pendingCertificateRequests: []
693
+ };
694
+ return origFetch(url, certs);
695
+ });
696
+ const result = await authFetch.sendCertificateRequest('https://new-server.com/path', { certifiers: [], types: {} });
697
+ expect(result).toHaveLength(1);
698
+ });
699
+ });
700
+ // ---------------------------------------------------------------------------
701
+ // 16. logPaymentAttempt – all levels
702
+ // ---------------------------------------------------------------------------
703
+ describe('AuthFetch.logPaymentAttempt (private)', () => {
704
+ let authFetch;
705
+ beforeEach(() => {
706
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
707
+ });
708
+ it('calls console.error for level=error', () => {
709
+ const spy = globals_1.jest.spyOn(console, 'error').mockImplementation(() => { });
710
+ authFetch.logPaymentAttempt('error', 'test error', { a: 1 });
711
+ expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test error', { a: 1 });
712
+ });
713
+ it('calls console.warn for level=warn', () => {
714
+ const spy = globals_1.jest.spyOn(console, 'warn').mockImplementation(() => { });
715
+ authFetch.logPaymentAttempt('warn', 'test warn', { b: 2 });
716
+ expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test warn', { b: 2 });
717
+ });
718
+ it('calls console.info for level=info when available', () => {
719
+ const spy = globals_1.jest.spyOn(console, 'info').mockImplementation(() => { });
720
+ authFetch.logPaymentAttempt('info', 'test info', { c: 3 });
721
+ expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test info', { c: 3 });
722
+ });
723
+ it('falls back to console.log for level=info when console.info is not a function', () => {
724
+ const originalInfo = console.info;
725
+ console.info = undefined;
726
+ const spy = globals_1.jest.spyOn(console, 'log').mockImplementation(() => { });
727
+ try {
728
+ ;
729
+ authFetch.logPaymentAttempt('info', 'fallback log', {});
730
+ expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] fallback log', {});
731
+ }
732
+ finally {
733
+ console.info = originalInfo;
734
+ }
735
+ });
736
+ });
737
+ // ---------------------------------------------------------------------------
738
+ // 17. createPaymentErrorEntry
739
+ // ---------------------------------------------------------------------------
740
+ describe('AuthFetch.createPaymentErrorEntry (private)', () => {
741
+ let authFetch;
742
+ beforeEach(() => {
743
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
744
+ });
745
+ it('extracts message and stack from an Error instance', () => {
746
+ const err = new Error('something went wrong');
747
+ const entry = authFetch.createPaymentErrorEntry(2, err);
748
+ expect(entry.attempt).toBe(2);
749
+ expect(entry.message).toBe('something went wrong');
750
+ expect(typeof entry.stack).toBe('string');
751
+ expect(typeof entry.timestamp).toBe('string');
752
+ });
753
+ it('converts non-Error to string message', () => {
754
+ const entry = authFetch.createPaymentErrorEntry(1, 'just a string error');
755
+ expect(entry.message).toBe('just a string error');
756
+ expect(entry.stack).toBeUndefined();
757
+ });
758
+ it('converts numeric error to string message', () => {
759
+ const entry = authFetch.createPaymentErrorEntry(1, 42);
760
+ expect(entry.message).toBe('42');
761
+ });
762
+ });
763
+ // ---------------------------------------------------------------------------
764
+ // 18. buildPaymentFailureError
765
+ // ---------------------------------------------------------------------------
766
+ describe('AuthFetch.buildPaymentFailureError (private)', () => {
767
+ let authFetch;
768
+ beforeEach(() => {
769
+ authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
770
+ });
771
+ function makeContext() {
772
+ return {
773
+ satoshisRequired: 10,
774
+ transactionBase64: 'tx-base64',
775
+ derivationPrefix: 'pfx',
776
+ derivationSuffix: 'sfx',
777
+ serverIdentityKey: 'srv',
778
+ clientIdentityKey: 'cli',
779
+ attempts: 3,
780
+ maxAttempts: 3,
781
+ errors: [],
782
+ requestSummary: { url: 'https://ex.com', method: 'GET', headers: {}, bodyType: 'none', bodyByteLength: 0 }
783
+ };
784
+ }
785
+ it('creates an Error with a descriptive message', () => {
786
+ const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), new Error('last error'));
787
+ expect(err).toBeInstanceOf(Error);
788
+ expect(err.message).toContain('https://example.com/pay');
789
+ expect(err.message).toContain('3/3');
790
+ expect(err.message).toContain('10 satoshis');
791
+ });
792
+ it('attaches details to the error', () => {
793
+ const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), new Error('x'));
794
+ expect(err.details).toBeDefined();
795
+ expect(err.details.payment.satoshis).toBe(10);
796
+ expect(err.details.attempts.used).toBe(3);
797
+ });
798
+ it('sets cause when lastError is an Error', () => {
799
+ const cause = new Error('root cause');
800
+ const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), cause);
801
+ expect(err.cause).toBe(cause);
802
+ });
803
+ it('does not set cause when lastError is a string', () => {
804
+ const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), 'string error');
805
+ expect(err.cause).toBeUndefined();
806
+ });
807
+ });
808
+ // ---------------------------------------------------------------------------
809
+ // 19. buildPaymentRequestSummary
810
+ // ---------------------------------------------------------------------------
811
+ describe('AuthFetch.buildPaymentRequestSummary (private)', () => {
812
+ it('builds a summary with correct fields', () => {
813
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
814
+ const summary = authFetch.buildPaymentRequestSummary('https://example.com/resource', { method: 'post', headers: { 'X-Custom': 'value' }, body: 'hello' });
815
+ expect(summary.url).toBe('https://example.com/resource');
816
+ expect(summary.method).toBe('POST');
817
+ expect(summary.headers).toMatchObject({ 'X-Custom': 'value' });
818
+ expect(summary.bodyType).toBe('string');
819
+ expect(summary.bodyByteLength).toBe(5);
820
+ });
821
+ it('defaults method to GET when not provided', () => {
822
+ const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
823
+ const summary = authFetch.buildPaymentRequestSummary('https://example.com', {});
824
+ expect(summary.method).toBe('GET');
825
+ });
826
+ });
827
+ //# sourceMappingURL=AuthFetch.additional.test.js.map