mppx 0.13.4 → 0.13.6
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.
- package/CHANGELOG.md +48 -0
- package/dist/Credential.d.ts +3 -0
- package/dist/Credential.js +9 -0
- package/dist/Errors.d.ts +63 -2
- package/dist/Errors.js +52 -2
- package/dist/Method.d.ts +10 -0
- package/dist/cli/cli.js +92 -92
- package/dist/cli/plugins/evm.js +2 -2
- package/dist/cli/plugins/index.js +12 -12
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.js +8 -10
- package/dist/client/internal/Fetch.js +107 -8
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/client/internal/MethodResponse.d.ts +6 -0
- package/dist/client/internal/MethodResponse.js +16 -0
- package/dist/client/internal/protocols/Mcp.js +36 -5
- package/dist/evm/server/Charge.js +3 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/internal/warnOnce.d.ts +3 -0
- package/dist/internal/warnOnce.js +9 -0
- package/dist/server/Mppx.d.ts +5 -0
- package/dist/server/Mppx.js +45 -18
- package/dist/server/Transport.js +10 -0
- package/dist/stripe/server/Methods.js +4 -1
- package/dist/tempo/Attribution.d.ts +24 -9
- package/dist/tempo/Attribution.js +25 -5
- package/dist/tempo/client/Charge.d.ts +116 -10
- package/dist/tempo/client/Charge.js +117 -22
- package/dist/tempo/client/Methods.d.ts +88 -9
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/internal/auto-swap.d.ts +10 -0
- package/dist/tempo/internal/auto-swap.js +35 -8
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.js +20 -8
- package/dist/tempo/legacy/session/Chain.js +8 -2
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.js +160 -29
- package/dist/tempo/server/Methods.js +28 -5
- package/dist/tempo/server/Relay.js +6 -22
- package/dist/tempo/server/Subscription.js +5 -1
- package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
- package/dist/tempo/server/internal/fallback-store.js +13 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +3 -0
- package/dist/tempo/server/internal/transport.js +5 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/client/ChannelStore.js +47 -0
- package/dist/tempo/session/client/CredentialState.js +8 -3
- package/dist/tempo/session/client/Session.js +50 -33
- package/dist/tempo/session/client/SessionManager.js +186 -65
- package/dist/tempo/session/client/Transports.d.ts +6 -0
- package/dist/tempo/session/client/Transports.js +52 -7
- package/dist/tempo/session/precompile/Chain.d.ts +2 -0
- package/dist/tempo/session/precompile/Chain.js +8 -3
- package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
- package/dist/tempo/session/server/ChannelStore.js +69 -9
- package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
- package/dist/tempo/session/server/CredentialVerification.js +220 -48
- package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
- package/dist/tempo/session/server/MeteredStream.js +10 -26
- package/dist/tempo/session/server/Session.d.ts +29 -2
- package/dist/tempo/session/server/Session.js +124 -44
- package/dist/tempo/session/server/Settlement.d.ts +9 -0
- package/dist/tempo/session/server/Settlement.js +23 -3
- package/dist/tempo/session/server/Sse.d.ts +4 -3
- package/dist/tempo/session/server/Sse.js +3 -3
- package/dist/tempo/session/server/Ws.d.ts +9 -1
- package/dist/tempo/session/server/Ws.js +1 -0
- package/package.json +1 -1
|
@@ -13,24 +13,33 @@ import * as Errors from '../../../Errors.js';
|
|
|
13
13
|
import * as Expires from '../../../Expires.js';
|
|
14
14
|
import * as Method from '../../../Method.js';
|
|
15
15
|
import * as ChallengeMeta from '../../../server/internal/challengeMeta.js';
|
|
16
|
-
import * as Store from '../../../Store.js';
|
|
17
16
|
import * as Client from '../../../viem/Client.js';
|
|
18
17
|
import * as Account from '../../internal/account.js';
|
|
19
18
|
import * as defaults from '../../internal/defaults.js';
|
|
20
19
|
import * as Methods from '../../Methods.js';
|
|
21
20
|
import * as ChargeServer from '../../server/Charge.js';
|
|
21
|
+
import { fallbackStore } from '../../server/internal/fallback-store.js';
|
|
22
22
|
import * as Transport from '../../server/internal/transport.js';
|
|
23
|
+
import * as Chain from '../precompile/Chain.js';
|
|
23
24
|
import { tip20ChannelEscrow } from '../precompile/Protocol.js';
|
|
24
25
|
import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot, } from '../Snapshot.js';
|
|
25
26
|
import * as ChannelStore from './ChannelStore.js';
|
|
26
27
|
import { broadcastCredentialPayload, validateCredentialPayload } from './CredentialVerification.js';
|
|
27
|
-
import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
28
|
+
import { requireSessionCredentialPayload, validateChannelState } from './CredentialVerification.js';
|
|
28
29
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
29
30
|
import { respondToSessionCredential } from './RequestState.js';
|
|
30
|
-
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
31
|
+
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, shouldChargeVoucherOnAcceptance, } from './Settlement.js';
|
|
31
32
|
import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
32
33
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
33
34
|
import * as Ws from './Ws.js';
|
|
35
|
+
const streamVerifiers = new WeakMap();
|
|
36
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
37
|
+
export function verifyStream(method, channelId) {
|
|
38
|
+
const verify = streamVerifiers.get(method);
|
|
39
|
+
if (!verify)
|
|
40
|
+
throw new Error('Session stream verifier is not configured');
|
|
41
|
+
return verify(channelId);
|
|
42
|
+
}
|
|
34
43
|
export { settle, settleBatch } from './Settlement.js';
|
|
35
44
|
function deriveServerDefaults(values) {
|
|
36
45
|
// `Method.toServer()` models defaults from request input fields. Tempo session
|
|
@@ -66,17 +75,14 @@ function createBootstrapChallenge(parameters) {
|
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
function respondBootstrapChallenge(challenge, error) {
|
|
69
|
-
const headers = new Headers({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const headers = new Headers({ 'Cache-Control': 'no-store' });
|
|
79
|
+
// A 403 is final: omit the challenge, as `Transport.http()` does.
|
|
80
|
+
if (error?.status !== 403)
|
|
81
|
+
headers.set(Constants.Headers.wwwAuthenticate, Challenge.serialize(challenge));
|
|
73
82
|
if (!error)
|
|
74
83
|
return new Response(null, { status: 402, headers });
|
|
75
84
|
headers.set('Content-Type', 'application/problem+json');
|
|
76
|
-
return new Response(JSON.stringify(error.toProblemDetails(challenge.id)), {
|
|
77
|
-
status: error.status,
|
|
78
|
-
headers,
|
|
79
|
-
});
|
|
85
|
+
return new Response(JSON.stringify(error.toProblemDetails(error.status === 403 ? undefined : challenge.id)), { status: error.status, headers });
|
|
80
86
|
}
|
|
81
87
|
function assertBootstrapChallengeMatches(expected, actual) {
|
|
82
88
|
if (actual.method !== Constants.Methods.tempo || actual.intent !== Constants.Intents.charge)
|
|
@@ -164,7 +170,9 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
catch (error) {
|
|
167
|
-
return respondBootstrapChallenge(challenge, error instanceof Errors.PaymentError
|
|
173
|
+
return respondBootstrapChallenge(challenge, error instanceof Errors.PaymentError
|
|
174
|
+
? error
|
|
175
|
+
: new Errors.InternalPaymentError({ cause: error }));
|
|
168
176
|
}
|
|
169
177
|
const channelId = await resolveSessionChannelId({
|
|
170
178
|
capturedRequest: parameters.capturedRequest,
|
|
@@ -192,10 +200,23 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
192
200
|
}
|
|
193
201
|
return new Response(null, { status: 204, headers });
|
|
194
202
|
}
|
|
203
|
+
/** Binds a reusable session challenge to its route and channel terms but not to its quoted amount. */
|
|
204
|
+
const reusableSessionBinding = (request) => {
|
|
205
|
+
const { methodDetails } = request;
|
|
206
|
+
return {
|
|
207
|
+
chainId: methodDetails.chainId,
|
|
208
|
+
currency: request.currency,
|
|
209
|
+
escrowContract: methodDetails.escrowContract,
|
|
210
|
+
operator: methodDetails.operator,
|
|
211
|
+
recipient: request.recipient,
|
|
212
|
+
sessionProtocol: methodDetails[Constants.MethodDetailKeys.sessionProtocol],
|
|
213
|
+
unitType: request.unitType,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
195
216
|
/** Creates a server-side TIP20EscrowChannel precompile session payment method. */
|
|
196
217
|
export function session(p) {
|
|
197
218
|
const parameters = p;
|
|
198
|
-
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore =
|
|
219
|
+
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = fallbackStore('tempo.session'), suggestedDeposit, unitType, } = parameters;
|
|
199
220
|
const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
|
|
200
221
|
const onSessionSettlement = parameters.onSessionSettlement;
|
|
201
222
|
const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
|
|
@@ -237,8 +258,33 @@ export function session(p) {
|
|
|
237
258
|
// A failed scheduled settlement is reported and retried by the next one; the charged request
|
|
238
259
|
// stays served because the payer's voucher already covers the charge.
|
|
239
260
|
const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
|
|
261
|
+
// Stream checks have a bounded TTL even when callers disable validation caching.
|
|
262
|
+
// Cache only successful reads; RPC failures must stop delivery.
|
|
263
|
+
const beforeStreamCharge = async (channelId) => {
|
|
264
|
+
channelId = ChannelStore.normalizeChannelId(channelId);
|
|
265
|
+
const channel = await store.getChannel(channelId);
|
|
266
|
+
if (!channel || channel.finalized || channel.closeRequestedAt !== 0n)
|
|
267
|
+
throw new Errors.ChannelClosedError({ reason: 'stream channel is closed' });
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const ttl = Math.min(Math.max(channelStateTtl, 0), 5_000);
|
|
270
|
+
const lastVerified = lastOnChainVerified.get(channelId);
|
|
271
|
+
if (lastVerified !== undefined && now >= lastVerified && now - lastVerified < ttl)
|
|
272
|
+
return;
|
|
273
|
+
let state;
|
|
274
|
+
try {
|
|
275
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
276
|
+
state = await Chain.getChannelState(client, channelId, channel.escrowContract);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Transport errors can include private RPC URLs or authentication details.
|
|
280
|
+
throw new Error('Unable to verify stream escrow');
|
|
281
|
+
}
|
|
282
|
+
validateChannelState(state);
|
|
283
|
+
lastOnChainVerified.set(channelId, now);
|
|
284
|
+
};
|
|
240
285
|
const serveWebSocket = (options) => Ws.serve({
|
|
241
286
|
...options,
|
|
287
|
+
beforeCharge: beforeStreamCharge,
|
|
242
288
|
onChargeCommitted: settleCharged,
|
|
243
289
|
store,
|
|
244
290
|
});
|
|
@@ -254,9 +300,11 @@ export function session(p) {
|
|
|
254
300
|
});
|
|
255
301
|
const transport = parameters.sse
|
|
256
302
|
? Transport.sse({
|
|
303
|
+
...(typeof parameters.sse === 'object' ? parameters.sse : {}),
|
|
304
|
+
beforeCharge: beforeStreamCharge,
|
|
305
|
+
reusableChallenges: parameters.reusableChallenges,
|
|
257
306
|
settleCharged,
|
|
258
307
|
store,
|
|
259
|
-
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
260
308
|
})
|
|
261
309
|
: undefined;
|
|
262
310
|
const validateCredential = async ({ credential, operation, request, }) => {
|
|
@@ -268,24 +316,31 @@ export function session(p) {
|
|
|
268
316
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
269
317
|
request,
|
|
270
318
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
319
|
+
// A broadcast voucher is checked against the stored channel by the update that accepts it.
|
|
320
|
+
const details = operation === 'broadcast' && payload.action === 'voucher'
|
|
321
|
+
? {
|
|
322
|
+
action: payload.action,
|
|
323
|
+
channelId: ChannelStore.normalizeChannelId(payload.channelId),
|
|
324
|
+
}
|
|
325
|
+
: await validateCredentialPayload({
|
|
326
|
+
account,
|
|
327
|
+
allowPendingClose: operation === 'broadcast',
|
|
328
|
+
challenge: credential.challenge,
|
|
329
|
+
channelStateTtl,
|
|
330
|
+
chainId: context.chainId,
|
|
331
|
+
client: context.client,
|
|
332
|
+
credentialSource: credential.source,
|
|
333
|
+
escrow: context.escrow,
|
|
334
|
+
expectedOperator: context.methodDetails.operator,
|
|
335
|
+
feePayer: context.feePayer,
|
|
336
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
337
|
+
feeToken: context.methodDetails.feeToken,
|
|
338
|
+
lastOnChainVerified,
|
|
339
|
+
minVoucherDelta: context.minVoucherDelta,
|
|
340
|
+
payload,
|
|
341
|
+
store,
|
|
342
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
343
|
+
});
|
|
289
344
|
return {
|
|
290
345
|
challenge: credential.challenge,
|
|
291
346
|
credential: { ...credential, payload },
|
|
@@ -306,9 +361,34 @@ export function session(p) {
|
|
|
306
361
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
307
362
|
request,
|
|
308
363
|
});
|
|
364
|
+
const chargeAmount = parameters.chargeOnAcceptance &&
|
|
365
|
+
shouldChargeVoucherOnAcceptance({
|
|
366
|
+
capturedRequest: envelope?.capturedRequest,
|
|
367
|
+
payloadAction: payload.action,
|
|
368
|
+
unitType: context.request.unitType,
|
|
369
|
+
})
|
|
370
|
+
? BigInt(context.request.amount ?? challenge.request.amount)
|
|
371
|
+
: undefined;
|
|
372
|
+
const acceptance = {};
|
|
373
|
+
const settleAcceptedCharge = (channel) => maybeSettleScheduled({
|
|
374
|
+
account,
|
|
375
|
+
client: context.client,
|
|
376
|
+
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
377
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
378
|
+
feeToken: parameters.feeToken,
|
|
379
|
+
onSessionSettlement,
|
|
380
|
+
onSessionSettlementFailure,
|
|
381
|
+
schedule: settlementSchedule,
|
|
382
|
+
store,
|
|
383
|
+
channel,
|
|
384
|
+
}).catch(ignoreRetryableSettlementFailure);
|
|
309
385
|
const sessionReceipt = await broadcastCredentialPayload({
|
|
310
386
|
account,
|
|
311
387
|
challenge,
|
|
388
|
+
chargeAmount,
|
|
389
|
+
onCharged: (channel) => {
|
|
390
|
+
acceptance.charged = channel;
|
|
391
|
+
},
|
|
312
392
|
channelStateTtl,
|
|
313
393
|
chainId: context.chainId,
|
|
314
394
|
client: context.client,
|
|
@@ -329,7 +409,16 @@ export function session(p) {
|
|
|
329
409
|
sseEnabled: Boolean(parameters.sse),
|
|
330
410
|
}),
|
|
331
411
|
store,
|
|
412
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
332
413
|
});
|
|
414
|
+
if (acceptance.charged) {
|
|
415
|
+
const settlementTxHash = await settleAcceptedCharge(acceptance.charged);
|
|
416
|
+
const receipt = {
|
|
417
|
+
...sessionReceipt,
|
|
418
|
+
...(settlementTxHash ? { txHash: settlementTxHash } : {}),
|
|
419
|
+
};
|
|
420
|
+
return parameters.sse ? Transport.markPrepaidSessionTick(receipt) : receipt;
|
|
421
|
+
}
|
|
333
422
|
return applyVerifiedHttpAccounting({
|
|
334
423
|
capturedRequest: envelope?.capturedRequest,
|
|
335
424
|
payloadAction: payload.action,
|
|
@@ -338,18 +427,7 @@ export function session(p) {
|
|
|
338
427
|
sseEnabled: Boolean(parameters.sse),
|
|
339
428
|
markPrepaidReceipt: Transport.markPrepaidSessionTick,
|
|
340
429
|
charge: (channelId, requestAmount) => chargeSessionChannel({ store, channelId, amount: requestAmount }),
|
|
341
|
-
settleCharged:
|
|
342
|
-
account,
|
|
343
|
-
client: context.client,
|
|
344
|
-
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
345
|
-
feePayerPolicy: parameters.feePayerPolicy,
|
|
346
|
-
feeToken: parameters.feeToken,
|
|
347
|
-
onSessionSettlement,
|
|
348
|
-
onSessionSettlementFailure,
|
|
349
|
-
schedule: settlementSchedule,
|
|
350
|
-
store,
|
|
351
|
-
channel,
|
|
352
|
-
}).catch(ignoreRetryableSettlementFailure),
|
|
430
|
+
settleCharged: settleAcceptedCharge,
|
|
353
431
|
});
|
|
354
432
|
};
|
|
355
433
|
const method = Method.toServer(Methods.session, {
|
|
@@ -365,6 +443,7 @@ export function session(p) {
|
|
|
365
443
|
unitType,
|
|
366
444
|
}),
|
|
367
445
|
extensions: { serveWebSocket, settleScheduled },
|
|
446
|
+
...(parameters.reusableChallenges ? { stableBinding: reusableSessionBinding } : {}),
|
|
368
447
|
transport: deriveTransport(transport),
|
|
369
448
|
preflight: parameters.bootstrap
|
|
370
449
|
? async ({ capturedRequest, credential, expires, input, options, realm, secretKey }) => {
|
|
@@ -431,6 +510,7 @@ export function session(p) {
|
|
|
431
510
|
});
|
|
432
511
|
},
|
|
433
512
|
});
|
|
513
|
+
streamVerifiers.set(method, beforeStreamCharge);
|
|
434
514
|
return method;
|
|
435
515
|
}
|
|
436
516
|
(function (session) {
|
|
@@ -174,6 +174,15 @@ export type ApplyVerifiedHttpAccountingParameters = {
|
|
|
174
174
|
};
|
|
175
175
|
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
176
176
|
export declare function shouldApplyVerifiedHttpAccounting(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction' | 'sseEnabled'>): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
179
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
180
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
181
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
182
|
+
*/
|
|
183
|
+
export declare function shouldChargeVoucherOnAcceptance(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction'> & {
|
|
184
|
+
unitType: unknown;
|
|
185
|
+
}): boolean;
|
|
177
186
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
178
187
|
export declare function applyVerifiedHttpAccounting(parameters: ApplyVerifiedHttpAccountingParameters): Promise<SessionReceipt>;
|
|
179
188
|
/** Atomically deducts spend from a channel and maps store failures to typed session errors. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseError as viem_BaseError, HttpRequestError, InternalRpcError, isAddress, isAddressEqual, LimitExceededRpcError, parseUnits, ResourceUnavailableRpcError, RpcRequestError, SocketClosedError, TimeoutError, WaitForTransactionReceiptTimeoutError, WebSocketRequestError, zeroAddress, } from 'viem';
|
|
1
|
+
import { BaseError as viem_BaseError, ExecutionRevertedError, HttpRequestError, InternalRpcError, isAddress, isAddressEqual, LimitExceededRpcError, parseUnits, ResourceUnavailableRpcError, RpcRequestError, SocketClosedError, TimeoutError, WaitForTransactionReceiptTimeoutError, WebSocketRequestError, zeroAddress, } from 'viem';
|
|
2
2
|
import { BadRequestError, ChannelClosedError, ChannelNotFoundError, InsufficientBalanceError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import { isSessionContentRequest } from '../../server/internal/request-body.js';
|
|
4
4
|
import * as Chain from '../precompile/Chain.js';
|
|
@@ -171,6 +171,18 @@ export function shouldApplyVerifiedHttpAccounting(parameters) {
|
|
|
171
171
|
return false;
|
|
172
172
|
return isSessionContentRequest(capturedRequest);
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
176
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
177
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
178
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
179
|
+
*/
|
|
180
|
+
export function shouldChargeVoucherOnAcceptance(parameters) {
|
|
181
|
+
const { capturedRequest, payloadAction, unitType } = parameters;
|
|
182
|
+
if (!capturedRequest || payloadAction !== 'voucher' || unitType !== 'request')
|
|
183
|
+
return false;
|
|
184
|
+
return isSessionContentRequest(capturedRequest);
|
|
185
|
+
}
|
|
174
186
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
175
187
|
export async function applyVerifiedHttpAccounting(parameters) {
|
|
176
188
|
const { receipt, sseEnabled } = parameters;
|
|
@@ -307,7 +319,10 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
307
319
|
sender: account?.address,
|
|
308
320
|
});
|
|
309
321
|
const amount = uint96(channel.highestVoucher.cumulativeAmount);
|
|
310
|
-
const
|
|
322
|
+
const { signature } = channel.highestVoucher;
|
|
323
|
+
// A replica behind the open or top-up that admitted this voucher rejects the settle while
|
|
324
|
+
// preparing it (e.g. `ChannelNotFound`); nothing was broadcast, so retry until it catches up.
|
|
325
|
+
const txHash = await Chain.readbackWithRetry(() => Chain.settleOnChain(client, channel.descriptor, amount, signature, escrow, account
|
|
311
326
|
? {
|
|
312
327
|
account,
|
|
313
328
|
...(options?.feePayer ? { feePayer: options.feePayer } : {}),
|
|
@@ -315,7 +330,7 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
315
330
|
...(options?.feeToken ? { feeToken: options.feeToken } : {}),
|
|
316
331
|
candidateFeeTokens: options?.candidateFeeTokens ?? [channel.token],
|
|
317
332
|
}
|
|
318
|
-
: undefined);
|
|
333
|
+
: undefined), { shouldRetry: isExecutionRevert });
|
|
319
334
|
const receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
320
335
|
const settled = readSettledReceiptFields(Chain.getChannelEvent(receipt, 'Settled', channelId));
|
|
321
336
|
const { deltaPaid, newSettled } = settled;
|
|
@@ -389,6 +404,11 @@ export function ignoreRetryableSettlementFailure(error) {
|
|
|
389
404
|
return undefined;
|
|
390
405
|
throw error;
|
|
391
406
|
}
|
|
407
|
+
/** Whether a settlement failed because the node's execution reverted it. */
|
|
408
|
+
function isExecutionRevert(error) {
|
|
409
|
+
return (error instanceof viem_BaseError &&
|
|
410
|
+
Boolean(error.walk((cause) => cause instanceof ExecutionRevertedError)));
|
|
411
|
+
}
|
|
392
412
|
function isRetryableSettlementFailure(error) {
|
|
393
413
|
if (error instanceof SettlementLeaseError)
|
|
394
414
|
return true;
|
|
@@ -27,9 +27,8 @@ export type { SessionController } from './MeteredStream.js';
|
|
|
27
27
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
28
28
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
29
29
|
* and polls store until the client tops up the channel.
|
|
30
|
-
* 4. Commits
|
|
31
|
-
*
|
|
32
|
-
* commits that terminal charge on successful completion.
|
|
30
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
31
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
33
32
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
34
33
|
*
|
|
35
34
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -43,6 +42,8 @@ export declare namespace serve {
|
|
|
43
42
|
challengeId: string;
|
|
44
43
|
tickCost: bigint;
|
|
45
44
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
|
|
45
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
46
|
+
beforeCharge?: MeteredStreamOptions['beforeCharge'];
|
|
46
47
|
/** Callback invoked after each nonzero stream charge is committed. */
|
|
47
48
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
48
49
|
pollIntervalMs?: number | undefined;
|
|
@@ -26,9 +26,8 @@ export { extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptE
|
|
|
26
26
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
27
27
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
28
28
|
* and polls store until the client tops up the channel.
|
|
29
|
-
* 4. Commits
|
|
30
|
-
*
|
|
31
|
-
* commits that terminal charge on successful completion.
|
|
29
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
30
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
32
31
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
33
32
|
*
|
|
34
33
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -44,6 +43,7 @@ export function serve(options) {
|
|
|
44
43
|
channelId,
|
|
45
44
|
tickCost,
|
|
46
45
|
generate,
|
|
46
|
+
beforeCharge: options.beforeCharge,
|
|
47
47
|
onChargeCommitted: options.onChargeCommitted,
|
|
48
48
|
pollIntervalMs,
|
|
49
49
|
prepaidUnits: options.prepaidUnits,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionCredentialContext } from '../precompile/Protocol.js';
|
|
1
2
|
import * as ChannelStore from './ChannelStore.js';
|
|
2
3
|
import type { MeteredStreamOptions, SessionController } from './MeteredStream.js';
|
|
3
4
|
import type { SettleChargedSessionChannel } from './Settlement.js';
|
|
@@ -16,6 +17,11 @@ export type SessionRouteResult = {
|
|
|
16
17
|
};
|
|
17
18
|
/** HTTP route used to perform a WebSocket payment probe. */
|
|
18
19
|
export type SessionRoute = (request: Request) => Promise<SessionRouteResult>;
|
|
20
|
+
type StreamContext = {
|
|
21
|
+
challengeId: string;
|
|
22
|
+
channelId: SessionCredentialContext['channelId'];
|
|
23
|
+
tickCost: bigint;
|
|
24
|
+
};
|
|
19
25
|
/**
|
|
20
26
|
* Bridge a WebSocket connection to a Tempo session payment flow.
|
|
21
27
|
*
|
|
@@ -47,7 +53,9 @@ export declare namespace serve {
|
|
|
47
53
|
/** Payment route handler. Receives synthetic `POST` requests with only
|
|
48
54
|
* the `Authorization` header — no cookies, bodies, or upgrade headers. */
|
|
49
55
|
route: SessionRoute;
|
|
50
|
-
/**
|
|
56
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
57
|
+
beforeCharge?: ((channelId: StreamContext['channelId']) => void | Promise<void>) | undefined;
|
|
58
|
+
/** Callback invoked after each nonzero stream charge is committed. */
|
|
51
59
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
52
60
|
/** @deprecated Use `onChargeCommitted`. */
|
|
53
61
|
settleScheduled?: SettleChargedSessionChannel | undefined;
|
|
@@ -65,6 +65,7 @@ export async function serve(options) {
|
|
|
65
65
|
channelId: context.channelId,
|
|
66
66
|
tickCost: context.tickCost,
|
|
67
67
|
generate,
|
|
68
|
+
beforeCharge: () => options.beforeCharge?.(context.channelId),
|
|
68
69
|
onChargeCommitted: options.onChargeCommitted ?? options.settleScheduled,
|
|
69
70
|
pollIntervalMs,
|
|
70
71
|
signal: abortController.signal,
|