mppx 0.12.0 → 0.13.1

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 (108) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/Method.d.ts +7 -1
  3. package/dist/Method.js +7 -2
  4. package/dist/Receipt.d.ts +2 -0
  5. package/dist/Receipt.js +2 -0
  6. package/dist/cli/cli.d.ts +8 -0
  7. package/dist/cli/cli.js +134 -136
  8. package/dist/cli/plugins/index.js +12 -14
  9. package/dist/cli/plugins/tempo.js +9 -2
  10. package/dist/cli/utils.d.ts +11 -1
  11. package/dist/cli/utils.js +15 -2
  12. package/dist/cli/validate/challenge.d.ts +2 -0
  13. package/dist/cli/validate/challenge.js +19 -4
  14. package/dist/cli/validate/discovery.d.ts +1 -0
  15. package/dist/cli/validate/discovery.js +10 -1
  16. package/dist/cli/validate/payment.js +34 -12
  17. package/dist/client/Methods.d.ts +0 -2
  18. package/dist/client/Methods.js +0 -2
  19. package/dist/client/Mppx.js +1 -0
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.js +1 -1
  22. package/dist/client/internal/Fetch.d.ts +2 -0
  23. package/dist/client/internal/Fetch.js +8 -2
  24. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  25. package/dist/evm/Types.js +1 -1
  26. package/dist/evm/server/Charge.js +5 -0
  27. package/dist/integrations/x402/Evm.js +4 -3
  28. package/dist/internal/HeaderCodec.d.ts +2 -2
  29. package/dist/internal/version.d.ts +2 -2
  30. package/dist/internal/version.js +1 -1
  31. package/dist/mcp/client/McpClient.js +4 -7
  32. package/dist/middlewares/elysia.d.ts +11 -8
  33. package/dist/middlewares/elysia.js +67 -14
  34. package/dist/middlewares/express.js +13 -6
  35. package/dist/middlewares/hono.js +4 -4
  36. package/dist/middlewares/nextjs.js +9 -5
  37. package/dist/proxy/internal/Headers.js +65 -0
  38. package/dist/proxy/services/stripe.js +1 -0
  39. package/dist/server/Mppx.d.ts +13 -1
  40. package/dist/server/Mppx.js +85 -44
  41. package/dist/server/NodeListener.js +71 -6
  42. package/dist/server/Request.d.ts +11 -1
  43. package/dist/server/Request.js +132 -13
  44. package/dist/server/Transport.d.ts +14 -1
  45. package/dist/server/Transport.js +1 -0
  46. package/dist/stripe/server/Methods.d.ts +10 -3
  47. package/dist/stripe/server/Methods.js +17 -2
  48. package/dist/stripe/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/Methods.d.ts +1 -0
  50. package/dist/tempo/Methods.js +1 -0
  51. package/dist/tempo/client/Charge.js +17 -13
  52. package/dist/tempo/client/Methods.d.ts +1 -5
  53. package/dist/tempo/client/Methods.js +10 -5
  54. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  55. package/dist/tempo/client/index.d.ts +0 -3
  56. package/dist/tempo/client/index.js +0 -2
  57. package/dist/tempo/internal/fee-payer.d.ts +6 -3
  58. package/dist/tempo/internal/fee-payer.js +23 -6
  59. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  60. package/dist/tempo/internal/machine-token-charge.js +2 -1
  61. package/dist/tempo/legacy/index.d.ts +1 -3
  62. package/dist/tempo/legacy/index.js +1 -3
  63. package/dist/tempo/server/Charge.js +75 -46
  64. package/dist/tempo/server/Methods.d.ts +55 -10
  65. package/dist/tempo/server/Subscription.d.ts +1 -1
  66. package/dist/tempo/server/Subscription.js +31 -4
  67. package/dist/tempo/server/internal/html.gen.js +1 -1
  68. package/dist/tempo/server/internal/transport.js +28 -11
  69. package/dist/tempo/session/client/ChannelOps.js +5 -0
  70. package/dist/tempo/session/client/Session.d.ts +1 -0
  71. package/dist/tempo/session/client/Session.js +7 -4
  72. package/dist/tempo/session/client/SessionManager.js +3 -0
  73. package/dist/tempo/session/client/Transports.d.ts +8 -1
  74. package/dist/tempo/session/client/Transports.js +12 -3
  75. package/dist/tempo/session/precompile/Chain.js +16 -1
  76. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  77. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  78. package/dist/tempo/session/precompile/Voucher.js +48 -0
  79. package/dist/tempo/session/server/ChannelStore.d.ts +20 -2
  80. package/dist/tempo/session/server/ChannelStore.js +25 -4
  81. package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
  82. package/dist/tempo/session/server/CredentialVerification.js +34 -6
  83. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  84. package/dist/tempo/session/server/MeteredStream.js +85 -29
  85. package/dist/tempo/session/server/Session.d.ts +2 -1
  86. package/dist/tempo/session/server/Session.js +2 -1
  87. package/dist/tempo/session/server/Settlement.d.ts +21 -0
  88. package/dist/tempo/session/server/Settlement.js +92 -17
  89. package/dist/tempo/session/server/Sse.d.ts +3 -1
  90. package/dist/tempo/session/server/Sse.js +105 -32
  91. package/dist/tempo/session/server/Transports.d.ts +24 -19
  92. package/dist/tempo/session/server/Transports.js +169 -58
  93. package/dist/tempo/session/server/Ws.d.ts +5 -1
  94. package/dist/tempo/session/server/Ws.js +75 -20
  95. package/dist/tempo/subscription/Store.d.ts +2 -0
  96. package/dist/tempo/subscription/Store.js +3 -0
  97. package/dist/validation/core.js +2 -1
  98. package/dist/x402/server/EvmCharge.d.ts +3 -1
  99. package/dist/x402/server/EvmCharge.js +14 -1
  100. package/package.json +6 -6
  101. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  102. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  103. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  104. package/dist/tempo/legacy/client/Session.js +0 -279
  105. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  106. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  107. package/dist/tempo/legacy/client/index.d.ts +0 -7
  108. package/dist/tempo/legacy/client/index.js +0 -5
@@ -1,706 +0,0 @@
1
- import { parseUnits } from 'viem';
2
- import * as Challenge from '../../../Challenge.js';
3
- import * as Fetch from '../../../client/internal/Fetch.js';
4
- import * as Constants from '../../../Constants.js';
5
- import * as PaymentCredential from '../../../Credential.js';
6
- import * as AcceptPayment from '../../../internal/AcceptPayment.js';
7
- import { deserializeSessionReceipt } from '../../session/precompile/Protocol.js';
8
- import { parseEvent } from '../../session/precompile/Protocol.js';
9
- import * as Ws from '../../session/precompile/Protocol.js';
10
- import { session as sessionPlugin } from './Session.js';
11
- const WebSocketReadyState = {
12
- CONNECTING: 0,
13
- OPEN: 1,
14
- CLOSING: 2,
15
- CLOSED: 3,
16
- };
17
- // Browser-style WebSocket clients may only initiate close with 1000 or 3000-4999.
18
- // Keep protocol/policy close codes on the server side and use an app-defined code here.
19
- const ClientWebSocketProtocolErrorCloseCode = 3008;
20
- /**
21
- * Creates a session manager that handles the full client payment lifecycle:
22
- * channel open, incremental vouchers, SSE streaming, and channel close.
23
- *
24
- * Internally delegates to the `session()` method for all
25
- * channel state management and credential creation, and to `Fetch.from`
26
- * for the 402 challenge/retry flow.
27
- *
28
- * ## Session resumption
29
- *
30
- * All channel state is held **in memory**. If the client process restarts,
31
- * the session is lost and a new on-chain channel will be opened on the next
32
- * request — the previous channel's deposit is orphaned until manually closed.
33
- *
34
- * When the server includes a `channelId` in the 402 challenge `methodDetails`,
35
- * the client will attempt to recover the channel by reading its on-chain state
36
- * via `getOnChainChannel()`. If the channel has a positive deposit and is not
37
- * finalized, it resumes from the on-chain settled amount.
38
- */
39
- export function sessionManager(parameters) {
40
- const allowCustomEscrow = parameters.allowCustomEscrow ?? false;
41
- const fetchFn = parameters.fetch ?? globalThis.fetch;
42
- const WebSocketImpl = parameters.webSocket ??
43
- globalThis.WebSocket;
44
- const maxVoucherCumulative = parameters.maxDeposit !== undefined
45
- ? parseUnits(parameters.maxDeposit, parameters.decimals ?? 6)
46
- : null;
47
- let channel = null;
48
- let lastChallenge = null;
49
- let lastUrl = null;
50
- let acceptedCumulative = 0n;
51
- let spent = 0n;
52
- let activeSocketChallenge = null;
53
- let activeSocketChannelId = null;
54
- let activeSocket = null;
55
- let closeReadyReceipt = null;
56
- let closeReadyWaiter = null;
57
- let expectedSocketCloseAmount = null;
58
- let receiptWaiter = null;
59
- let wsDeliveredChunks = 0n;
60
- let wsTickCost = 0n;
61
- const method = sessionPlugin({
62
- account: parameters.account,
63
- allowCustomEscrow,
64
- voucherSigner: parameters.voucherSigner,
65
- getClient: parameters.client ? () => parameters.client : parameters.getClient,
66
- escrowContract: parameters.escrowContract,
67
- decimals: parameters.decimals,
68
- maxDeposit: parameters.maxDeposit,
69
- onChannelUpdate(entry) {
70
- if (entry.channelId !== channel?.channelId) {
71
- acceptedCumulative = 0n;
72
- spent = 0n;
73
- }
74
- channel = entry;
75
- },
76
- });
77
- const wrappedFetch = Fetch.from({
78
- fetch: fetchFn,
79
- methods: [method],
80
- onChallenge: async (challenge, _helpers) => {
81
- lastChallenge = challenge;
82
- return undefined;
83
- },
84
- orderChallenges: parameters.orderChallenges,
85
- });
86
- function updateSpentFromReceipt(receipt) {
87
- if (!receipt || receipt.channelId !== channel?.channelId)
88
- return;
89
- assertReceiptWithinLocalState(receipt);
90
- const nextAcceptedCumulative = BigInt(receipt.acceptedCumulative);
91
- const next = BigInt(receipt.spent);
92
- acceptedCumulative =
93
- acceptedCumulative > nextAcceptedCumulative ? acceptedCumulative : nextAcceptedCumulative;
94
- spent = spent > next ? spent : next;
95
- }
96
- function assertReceiptWithinLocalState(receipt) {
97
- if (!channel || receipt.channelId !== channel.channelId)
98
- return;
99
- const acceptedCumulative = BigInt(receipt.acceptedCumulative);
100
- const receiptSpent = BigInt(receipt.spent);
101
- if (receiptSpent > acceptedCumulative) {
102
- throw new Error('receipt spent exceeds accepted cumulative voucher amount');
103
- }
104
- if (acceptedCumulative > channel.cumulativeAmount) {
105
- throw new Error('receipt accepted cumulative exceeds local voucher state');
106
- }
107
- if (receiptSpent > channel.cumulativeAmount) {
108
- throw new Error('receipt spent exceeds local voucher state');
109
- }
110
- assertVoucherWithinLocalLimit(acceptedCumulative);
111
- assertVoucherWithinLocalLimit(receiptSpent);
112
- }
113
- function waitForReceipt(predicate = () => true) {
114
- if (receiptWaiter)
115
- throw new Error('receipt wait already in progress');
116
- return new Promise((resolve, reject) => {
117
- receiptWaiter = { predicate, resolve, reject };
118
- });
119
- }
120
- function waitForCloseReady() {
121
- if (closeReadyReceipt)
122
- return Promise.resolve(closeReadyReceipt);
123
- if (closeReadyWaiter)
124
- throw new Error('close-ready wait already in progress');
125
- return new Promise((resolve, reject) => {
126
- closeReadyWaiter = { resolve, reject };
127
- });
128
- }
129
- function settleReceipt(receipt) {
130
- if (!receiptWaiter)
131
- return;
132
- if (!receiptWaiter.predicate(receipt))
133
- return;
134
- const waiter = receiptWaiter;
135
- receiptWaiter = null;
136
- waiter.resolve(receipt);
137
- }
138
- function settleCloseReady(receipt) {
139
- closeReadyReceipt = receipt;
140
- if (!closeReadyWaiter)
141
- return;
142
- const waiter = closeReadyWaiter;
143
- closeReadyWaiter = null;
144
- waiter.resolve(receipt);
145
- }
146
- function rejectReceipt(error) {
147
- if (!receiptWaiter)
148
- return;
149
- const waiter = receiptWaiter;
150
- receiptWaiter = null;
151
- waiter.reject(error);
152
- }
153
- function rejectCloseReady(error) {
154
- if (!closeReadyWaiter)
155
- return;
156
- const waiter = closeReadyWaiter;
157
- closeReadyWaiter = null;
158
- waiter.reject(error);
159
- }
160
- function getFallbackCloseAmount(challenge, channelId) {
161
- if (closeReadyReceipt &&
162
- closeReadyReceipt.challengeId === challenge.id &&
163
- closeReadyReceipt.channelId === channelId) {
164
- return closeReadyReceipt.spent;
165
- }
166
- const cumulative = channel?.channelId === channelId ? channel.cumulativeAmount : 0n;
167
- // For WS sessions, use delivered chunk count × tick cost as a tight spend
168
- // estimate. Without this, a socket death before close-ready would cause
169
- // the client to sign for the full cumulative voucher authorization —
170
- // potentially orders of magnitude more than what was actually consumed.
171
- // The estimate may undercount by at most 1 chunk (if the server committed
172
- // a charge but the socket died before delivering the message).
173
- if (wsTickCost > 0n) {
174
- const deliveryEstimate = wsDeliveredChunks * wsTickCost;
175
- const bestSpent = spent > deliveryEstimate ? spent : deliveryEstimate;
176
- return (bestSpent > cumulative ? cumulative : bestSpent).toString();
177
- }
178
- // Some legacy servers report receipt.spent as a per-request delta while
179
- // still requiring close at the highest accepted cumulative voucher.
180
- return (spent > acceptedCumulative ? spent : acceptedCumulative).toString();
181
- }
182
- function assertVoucherWithinLocalLimit(cumulativeAmount) {
183
- if (maxVoucherCumulative === null)
184
- return;
185
- if (cumulativeAmount <= maxVoucherCumulative)
186
- return;
187
- throw new Error(`requested voucher amount ${cumulativeAmount} exceeds local maxDeposit ${maxVoucherCumulative}`);
188
- }
189
- function toPaymentResponse(response) {
190
- const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
191
- const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null;
192
- updateSpentFromReceipt(receipt);
193
- return Object.assign(response, {
194
- receipt,
195
- challenge: lastChallenge,
196
- channelId: channel?.channelId ?? null,
197
- cumulative: channel?.cumulativeAmount ?? 0n,
198
- });
199
- }
200
- async function doFetch(input, init) {
201
- lastUrl = input;
202
- const response = await wrappedFetch(input, init);
203
- return toPaymentResponse(response);
204
- }
205
- function createManagedSocket(socket) {
206
- const listeners = new Map();
207
- let emittedClose = false;
208
- let messageBuffer = [];
209
- let readyState = socket.readyState;
210
- const add = (type, listener, options) => {
211
- let set = listeners.get(type);
212
- if (!set) {
213
- set = new Set();
214
- listeners.set(type, set);
215
- }
216
- set.add({
217
- once: typeof options === 'object' ? options.once === true : false,
218
- value: listener,
219
- });
220
- if (type === 'message' && messageBuffer) {
221
- const buffered = messageBuffer;
222
- messageBuffer = null;
223
- for (const event of buffered)
224
- emit('message', event);
225
- }
226
- };
227
- const remove = (type, listener) => {
228
- const set = listeners.get(type);
229
- if (!set)
230
- return;
231
- for (const entry of set) {
232
- if (entry.value === listener)
233
- set.delete(entry);
234
- }
235
- };
236
- const emit = (type, event) => {
237
- if (type === 'close') {
238
- if (emittedClose)
239
- return;
240
- emittedClose = true;
241
- readyState = WebSocketReadyState.CLOSED;
242
- messageBuffer = null;
243
- }
244
- if (type === 'open')
245
- readyState = WebSocketReadyState.OPEN;
246
- if (type === 'message' && messageBuffer) {
247
- messageBuffer.push(event);
248
- return;
249
- }
250
- const property = `on${type}`;
251
- const handler = managed[property];
252
- if (typeof handler === 'function')
253
- handler(event);
254
- const set = listeners.get(type);
255
- if (!set)
256
- return;
257
- for (const entry of Array.from(set)) {
258
- if (typeof entry.value === 'function')
259
- entry.value(event);
260
- else
261
- entry.value.handleEvent(event);
262
- if (entry.once)
263
- set.delete(entry);
264
- }
265
- };
266
- const managed = {
267
- addEventListener: add,
268
- close(code, reason) {
269
- socket.close(code, reason);
270
- },
271
- get bufferedAmount() {
272
- return socket.bufferedAmount;
273
- },
274
- get extensions() {
275
- return socket.extensions;
276
- },
277
- on(type, listener) {
278
- add(type, listener);
279
- },
280
- onclose: null,
281
- onerror: null,
282
- _onmessage: null,
283
- get onmessage() {
284
- return managed._onmessage;
285
- },
286
- set onmessage(fn) {
287
- managed._onmessage = fn;
288
- if (fn && messageBuffer) {
289
- const buffered = messageBuffer;
290
- messageBuffer = null;
291
- for (const event of buffered)
292
- emit('message', event);
293
- }
294
- },
295
- onopen: null,
296
- off(type, listener) {
297
- remove(type, listener);
298
- },
299
- get protocol() {
300
- return socket.protocol;
301
- },
302
- get readyState() {
303
- return readyState;
304
- },
305
- removeEventListener: remove,
306
- send(data) {
307
- socket.send(data);
308
- },
309
- get url() {
310
- return socket.url;
311
- },
312
- };
313
- return {
314
- emit,
315
- socket: managed,
316
- };
317
- }
318
- const self = {
319
- get channelId() {
320
- return channel?.channelId;
321
- },
322
- get cumulative() {
323
- return channel?.cumulativeAmount ?? 0n;
324
- },
325
- get opened() {
326
- return channel?.opened ?? false;
327
- },
328
- async open(options) {
329
- if (channel?.opened)
330
- return;
331
- if (!lastChallenge) {
332
- throw new Error('No challenge available. Make a request first to receive a 402 challenge, or pass a challenge via .fetch()/.sse().');
333
- }
334
- const deposit = options?.deposit;
335
- const credential = await method.createCredential({
336
- challenge: lastChallenge,
337
- context: {
338
- ...(deposit !== undefined && { depositRaw: deposit.toString() }),
339
- },
340
- });
341
- if (!lastUrl)
342
- throw new Error('No URL available — call fetch() or sse() before open().');
343
- const response = await fetchFn(lastUrl, {
344
- method: 'POST',
345
- headers: { [Constants.Headers.authorization]: credential },
346
- });
347
- if (!response.ok) {
348
- const body = await response.text().catch(() => '');
349
- const wwwAuth = response.headers.get(Constants.Headers.wwwAuthenticate) ?? '';
350
- throw new Error(`Open request failed with status ${response.status}${body ? `: ${body}` : ''}${wwwAuth ? ` [${Constants.Headers.wwwAuthenticate}: ${wwwAuth}]` : ''}`);
351
- }
352
- },
353
- fetch: doFetch,
354
- async sse(input, init) {
355
- const { onReceipt, signal, ...fetchInit } = init ?? {};
356
- const sseInit = {
357
- ...fetchInit,
358
- headers: {
359
- ...Fetch.normalizeHeaders(fetchInit.headers),
360
- Accept: 'text/event-stream',
361
- },
362
- ...(signal ? { signal } : {}),
363
- };
364
- const response = await doFetch(input, sseInit);
365
- // Snapshot the challenge at SSE open time so concurrent
366
- // calls don't overwrite it.
367
- const sseChallenge = lastChallenge;
368
- if (!response.body)
369
- throw new Error('Response has no body.');
370
- const reader = response.body.getReader();
371
- const decoder = new TextDecoder();
372
- async function* iterate() {
373
- let buffer = '';
374
- try {
375
- while (true) {
376
- if (signal?.aborted)
377
- break;
378
- const { done, value } = await reader.read();
379
- if (done)
380
- break;
381
- buffer += decoder.decode(value, { stream: true });
382
- const parts = buffer.split('\n\n');
383
- buffer = parts.pop();
384
- for (const part of parts) {
385
- if (!part.trim())
386
- continue;
387
- const event = parseEvent(part);
388
- if (!event)
389
- continue;
390
- switch (event.type) {
391
- case 'message':
392
- yield event.data;
393
- break;
394
- case 'payment-need-voucher': {
395
- if (!channel || !sseChallenge)
396
- break;
397
- const required = BigInt(event.data.requiredCumulative);
398
- assertVoucherWithinLocalLimit(required);
399
- channel.cumulativeAmount =
400
- channel.cumulativeAmount > required ? channel.cumulativeAmount : required;
401
- const credential = await method.createCredential({
402
- challenge: sseChallenge,
403
- context: {
404
- action: 'voucher',
405
- channelId: channel.channelId,
406
- cumulativeAmountRaw: channel.cumulativeAmount.toString(),
407
- },
408
- });
409
- const voucherResponse = await fetchFn(input, {
410
- method: 'POST',
411
- headers: {
412
- Accept: 'text/event-stream',
413
- [Constants.Headers.authorization]: credential,
414
- },
415
- });
416
- if (!voucherResponse.ok) {
417
- throw new Error(`Voucher POST failed with status ${voucherResponse.status}`);
418
- }
419
- break;
420
- }
421
- case 'payment-receipt':
422
- updateSpentFromReceipt(event.data);
423
- onReceipt?.(event.data);
424
- break;
425
- }
426
- }
427
- }
428
- }
429
- finally {
430
- reader.releaseLock();
431
- }
432
- }
433
- return iterate();
434
- },
435
- async ws(input, init) {
436
- if (!WebSocketImpl) {
437
- throw new Error('No WebSocket implementation available. Pass `webSocket` to sessionManager() in this runtime.');
438
- }
439
- const { onReceipt, protocols, signal } = init ?? {};
440
- const wsUrl = new URL(input.toString());
441
- const httpUrl = new URL(wsUrl.toString());
442
- if (httpUrl.protocol === 'ws:')
443
- httpUrl.protocol = 'http:';
444
- if (httpUrl.protocol === 'wss:')
445
- httpUrl.protocol = 'https:';
446
- lastUrl = httpUrl.toString();
447
- const probe = await fetchFn(httpUrl, signal ? { signal } : undefined);
448
- if (probe.status !== 402) {
449
- throw new Error(`Expected a 402 payment challenge from ${httpUrl}, received ${probe.status} instead.`);
450
- }
451
- const candidates = AcceptPayment.selectChallengeCandidates(Challenge.fromResponseList(probe), [method], AcceptPayment.resolve([method]).entries);
452
- const challenge = (await resolveSessionChallengeOrder(candidates, init?.orderChallenges ?? parameters.orderChallenges))[0]?.challenge;
453
- if (!challenge) {
454
- throw new Error('No payment challenge received from HTTP endpoint for this WebSocket URL. The server may not require payment or did not advertise a challenge.');
455
- }
456
- lastChallenge = challenge;
457
- const credential = await method.createCredential({
458
- challenge: challenge,
459
- context: {},
460
- });
461
- closeReadyReceipt = null;
462
- activeSocketChallenge = challenge;
463
- wsDeliveredChunks = 0n;
464
- wsTickCost = BigInt(challenge.request.amount);
465
- const openCredential = PaymentCredential.deserialize(credential);
466
- activeSocketChannelId = openCredential.payload.channelId;
467
- const rawSocket = new WebSocketImpl(wsUrl, protocols);
468
- activeSocket = rawSocket;
469
- const managedSocket = createManagedSocket(rawSocket);
470
- const failSocketFlow = (message) => {
471
- rejectReceipt(new Error(message));
472
- rejectCloseReady(new Error(message));
473
- if (rawSocket.readyState === WebSocketReadyState.CONNECTING ||
474
- rawSocket.readyState === WebSocketReadyState.OPEN) {
475
- rawSocket.close(ClientWebSocketProtocolErrorCloseCode, message);
476
- }
477
- };
478
- const isExpectedReceipt = (receipt) => receipt.challengeId === challenge.id && receipt.channelId === activeSocketChannelId;
479
- const socketOpened = new Promise((resolve, reject) => {
480
- const onOpen = () => {
481
- rawSocket.removeEventListener('error', onError);
482
- managedSocket.emit('open', { type: 'open' });
483
- resolve();
484
- };
485
- const onError = () => {
486
- rawSocket.removeEventListener('open', onOpen);
487
- reject(new Error(`WebSocket connection to ${wsUrl} failed to open.`));
488
- };
489
- rawSocket.addEventListener('open', onOpen, { once: true });
490
- rawSocket.addEventListener('error', onError, { once: true });
491
- });
492
- rawSocket.addEventListener('close', (event) => {
493
- if (activeSocket === rawSocket)
494
- activeSocket = null;
495
- if (activeSocketChallenge === challenge)
496
- activeSocketChallenge = null;
497
- if (activeSocketChannelId === openCredential.payload.channelId)
498
- activeSocketChannelId = null;
499
- expectedSocketCloseAmount = null;
500
- rejectReceipt(new Error('WebSocket closed before the payment flow completed.'));
501
- rejectCloseReady(new Error('WebSocket closed before the payment flow completed.'));
502
- managedSocket.emit('close', {
503
- code: event.code ?? 1000,
504
- reason: event.reason ?? '',
505
- type: 'close',
506
- wasClean: true,
507
- });
508
- });
509
- rawSocket.addEventListener('error', () => {
510
- managedSocket.emit('error', { type: 'error' });
511
- });
512
- rawSocket.addEventListener('message', async (event) => {
513
- const raw = typeof event.data === 'string' ? event.data : undefined;
514
- if (!raw)
515
- return;
516
- const message = Ws.parseMessage(raw);
517
- if (!message) {
518
- managedSocket.emit('message', { data: raw, type: 'message' });
519
- return;
520
- }
521
- switch (message.mpp) {
522
- case 'authorization':
523
- break;
524
- case 'message':
525
- wsDeliveredChunks += 1n;
526
- managedSocket.emit('message', { data: message.data, type: 'message' });
527
- break;
528
- case 'payment-close-ready':
529
- if (!isExpectedReceipt(message.data)) {
530
- failSocketFlow('received mismatched payment-close-ready frame');
531
- break;
532
- }
533
- if (BigInt(message.data.spent) > (channel?.cumulativeAmount ?? 0n)) {
534
- failSocketFlow('received payment-close-ready beyond local voucher state');
535
- break;
536
- }
537
- updateSpentFromReceipt(message.data);
538
- onReceipt?.(message.data);
539
- settleCloseReady(message.data);
540
- managedSocket.emit('close', { code: 1000, reason: 'stream complete', type: 'close' });
541
- break;
542
- case 'payment-error':
543
- rejectReceipt(new Error(message.message));
544
- rejectCloseReady(new Error(message.message));
545
- break;
546
- case 'payment-need-voucher': {
547
- if (message.data.channelId !== activeSocketChannelId) {
548
- failSocketFlow('received mismatched payment-need-voucher frame');
549
- break;
550
- }
551
- const required = BigInt(message.data.requiredCumulative);
552
- try {
553
- assertVoucherWithinLocalLimit(required);
554
- }
555
- catch (error) {
556
- failSocketFlow(error instanceof Error
557
- ? error.message
558
- : 'requested voucher amount exceeds local maxDeposit');
559
- break;
560
- }
561
- const nextCumulative = (channel?.cumulativeAmount ?? 0n) > required
562
- ? (channel?.cumulativeAmount ?? 0n)
563
- : required;
564
- if (channel?.channelId === activeSocketChannelId)
565
- channel.cumulativeAmount = nextCumulative;
566
- const voucher = await method.createCredential({
567
- challenge: challenge,
568
- context: {
569
- action: 'voucher',
570
- channelId: activeSocketChannelId,
571
- cumulativeAmountRaw: nextCumulative.toString(),
572
- },
573
- });
574
- rawSocket.send(Ws.formatAuthorizationMessage(voucher));
575
- break;
576
- }
577
- case 'payment-receipt':
578
- if (!isExpectedReceipt(message.data)) {
579
- failSocketFlow('received mismatched payment-receipt frame');
580
- break;
581
- }
582
- if (expectedSocketCloseAmount !== null &&
583
- Boolean(message.data.txHash) &&
584
- (message.data.acceptedCumulative !== expectedSocketCloseAmount ||
585
- message.data.spent !== expectedSocketCloseAmount)) {
586
- failSocketFlow('received mismatched payment-close receipt frame');
587
- break;
588
- }
589
- updateSpentFromReceipt(message.data);
590
- onReceipt?.(message.data);
591
- settleReceipt(message.data);
592
- break;
593
- }
594
- });
595
- if (signal) {
596
- signal.addEventListener('abort', () => {
597
- rejectReceipt(new Error('WebSocket payment flow aborted.'));
598
- rejectCloseReady(new Error('WebSocket payment flow aborted.'));
599
- rawSocket.close();
600
- }, { once: true });
601
- }
602
- await socketOpened;
603
- rawSocket.send(Ws.formatAuthorizationMessage(credential));
604
- await waitForReceipt();
605
- return managedSocket.socket;
606
- },
607
- async close() {
608
- const closeChallenge = activeSocketChallenge ?? lastChallenge;
609
- const closeChannelId = activeSocketChannelId ?? channel?.channelId;
610
- if (!channel?.opened)
611
- return undefined;
612
- if (!closeChallenge) {
613
- throw new Error('Cannot close session: no challenge available. This usually means close() was called on a SessionManager instance that was recreated after the session was opened. Use the same SessionManager instance that opened the session, or make a request first to receive a fresh 402 challenge.');
614
- }
615
- if (!closeChannelId) {
616
- throw new Error('Cannot close session: no channel ID available. The session may not have been fully opened.');
617
- }
618
- if (activeSocket?.readyState === WebSocketReadyState.OPEN) {
619
- const ready = closeReadyReceipt ??
620
- (await (async () => {
621
- activeSocket.send(Ws.formatCloseRequestMessage());
622
- return waitForCloseReady();
623
- })());
624
- const readySpent = BigInt(ready.spent);
625
- if (readySpent > (channel.cumulativeAmount > spent ? channel.cumulativeAmount : spent)) {
626
- throw new Error('close-ready spent exceeds local voucher state');
627
- }
628
- const credential = await method.createCredential({
629
- challenge: closeChallenge,
630
- context: {
631
- action: 'close',
632
- channelId: closeChannelId,
633
- cumulativeAmountRaw: readySpent.toString(),
634
- },
635
- });
636
- const expectedCloseAmount = readySpent.toString();
637
- expectedSocketCloseAmount = expectedCloseAmount;
638
- try {
639
- const pendingReceipt = waitForReceipt((receipt) => Boolean(receipt.txHash) &&
640
- receipt.challengeId === closeChallenge.id &&
641
- receipt.channelId === closeChannelId &&
642
- receipt.acceptedCumulative === expectedCloseAmount &&
643
- receipt.spent === expectedCloseAmount);
644
- activeSocket.send(Ws.formatAuthorizationMessage(credential));
645
- const receipt = await pendingReceipt;
646
- activeSocket.close();
647
- closeReadyReceipt = null;
648
- return receipt;
649
- }
650
- finally {
651
- expectedSocketCloseAmount = null;
652
- }
653
- }
654
- const credential = await method.createCredential({
655
- challenge: closeChallenge,
656
- context: {
657
- action: 'close',
658
- channelId: closeChannelId,
659
- cumulativeAmountRaw: (() => {
660
- const closeAmount = BigInt(getFallbackCloseAmount(closeChallenge, closeChannelId));
661
- if (closeAmount > channel.cumulativeAmount) {
662
- throw new Error('fallback close amount exceeds local voucher state');
663
- }
664
- assertVoucherWithinLocalLimit(closeAmount);
665
- return closeAmount.toString();
666
- })(),
667
- },
668
- });
669
- if (!lastUrl) {
670
- throw new Error('Cannot close session: no URL available. This usually means close() was called on a SessionManager instance that was recreated after the session was opened. Use the same SessionManager instance that opened the session, or call fetch()/sse() before close().');
671
- }
672
- const response = await fetchFn(lastUrl, {
673
- method: 'POST',
674
- headers: { [Constants.Headers.authorization]: credential },
675
- });
676
- if (!response.ok) {
677
- const body = await response.text().catch(() => '');
678
- const detail = (() => {
679
- if (!body)
680
- return '';
681
- if (!response.headers.get('Content-Type')?.includes('application/problem+json')) {
682
- return body;
683
- }
684
- try {
685
- const problem = JSON.parse(body);
686
- return problem.detail ?? body;
687
- }
688
- catch {
689
- return body;
690
- }
691
- })();
692
- const wwwAuth = response.headers.get(Constants.Headers.wwwAuthenticate) ?? '';
693
- throw new Error(`Close request failed with status ${response.status}${detail ? `: ${detail}` : ''}${wwwAuth ? ` [${Constants.Headers.wwwAuthenticate}: ${wwwAuth}]` : ''}`);
694
- }
695
- const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
696
- const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : undefined;
697
- return receipt;
698
- },
699
- };
700
- return self;
701
- }
702
- async function resolveSessionChallengeOrder(candidates, override) {
703
- const orderChallenges = override;
704
- return orderChallenges ? orderChallenges(candidates) : candidates;
705
- }
706
- //# sourceMappingURL=SessionManager.js.map