mppx 0.13.2 → 0.13.4
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 +21 -0
- package/dist/cli/cli.js +3 -3
- package/dist/cli/plugins/index.js +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.js +2 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.js +6 -3
- package/dist/server/internal/challengeMeta.d.ts +5 -0
- package/dist/server/internal/challengeMeta.js +14 -0
- package/dist/tempo/server/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.js +5 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/Transports.js +39 -12
- package/dist/tempo/session/precompile/Chain.d.ts +4 -0
- package/dist/tempo/session/precompile/Chain.js +8 -2
- package/dist/tempo/session/server/ChannelStore.d.ts +8 -0
- package/dist/tempo/session/server/ChannelStore.js +17 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -11
- package/dist/tempo/session/server/Session.d.ts +4 -2
- package/dist/tempo/session/server/Session.js +23 -5
- package/dist/tempo/session/server/Settlement.d.ts +40 -1
- package/dist/tempo/session/server/Settlement.js +105 -34
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/x402/server/EvmCharge.js +10 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAddress, isAddressEqual, parseUnits, zeroAddress, } from 'viem';
|
|
1
|
+
import { BaseError as viem_BaseError, 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';
|
|
@@ -105,7 +105,7 @@ export function isSettlementDue(channel, schedule) {
|
|
|
105
105
|
return true;
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
|
-
/**
|
|
108
|
+
/** Releases a completed scheduled settlement lease; {@link settle} records the settled counters. */
|
|
109
109
|
export async function markSettlementComplete(parameters) {
|
|
110
110
|
const { channelId, leaseOwner, store, settledAt = new Date().toISOString() } = parameters;
|
|
111
111
|
await store.updateChannel(channelId, (current) => {
|
|
@@ -114,12 +114,7 @@ export async function markSettlementComplete(parameters) {
|
|
|
114
114
|
if (current.scheduledSettlementLease?.owner !== leaseOwner)
|
|
115
115
|
return current;
|
|
116
116
|
const { scheduledSettlementLease: _, ...channel } = current;
|
|
117
|
-
return {
|
|
118
|
-
...channel,
|
|
119
|
-
lastSettlementAt: settledAt,
|
|
120
|
-
lastSettlementSpent: current.spent,
|
|
121
|
-
lastSettlementUnits: current.units,
|
|
122
|
-
};
|
|
117
|
+
return { ...channel, lastSettlementAt: settledAt };
|
|
123
118
|
});
|
|
124
119
|
}
|
|
125
120
|
/** Atomically claims one due scheduled settlement across server workers. */
|
|
@@ -244,10 +239,20 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
244
239
|
const { channel, schedule, store } = parameters;
|
|
245
240
|
if (!schedule || !isSettlementDue(channel, schedule))
|
|
246
241
|
return undefined;
|
|
242
|
+
const report = (error) => reportSessionSettlementFailure(parameters.onSessionSettlementFailure, {
|
|
243
|
+
chainId: channel.chainId,
|
|
244
|
+
channelId: channel.channelId,
|
|
245
|
+
error,
|
|
246
|
+
trigger: 'scheduled',
|
|
247
|
+
});
|
|
247
248
|
const leaseOwner = await claimScheduledSettlement({
|
|
248
249
|
channelId: channel.channelId,
|
|
249
250
|
schedule,
|
|
250
251
|
store,
|
|
252
|
+
}).catch(async (error) => {
|
|
253
|
+
await report(error);
|
|
254
|
+
// Nothing was submitted, so the next settlement can still collect this charge.
|
|
255
|
+
throw new SettlementLeaseError({ cause: error });
|
|
251
256
|
});
|
|
252
257
|
if (!leaseOwner)
|
|
253
258
|
return undefined;
|
|
@@ -258,26 +263,28 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
258
263
|
store,
|
|
259
264
|
}).catch(() => undefined);
|
|
260
265
|
}, scheduledSettlementLeaseMs / 2);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
const release = () => releaseScheduledSettlement({ channelId: channel.channelId, leaseOwner, store }).catch(() => undefined);
|
|
267
|
+
const txHash = await settle(store, parameters.client, channel.channelId, {
|
|
268
|
+
account: parameters.account,
|
|
269
|
+
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
270
|
+
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
271
|
+
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
272
|
+
onSessionSettlement: parameters.onSessionSettlement
|
|
273
|
+
? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
|
|
274
|
+
: undefined,
|
|
275
|
+
})
|
|
276
|
+
.catch(async (error) => {
|
|
277
|
+
await release();
|
|
278
|
+
await report(error);
|
|
279
|
+
// The transaction confirmed and collected the charge; only the local record failed.
|
|
280
|
+
if (error instanceof SettlementCheckpointError)
|
|
281
|
+
return error.txHash;
|
|
276
282
|
throw error;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
283
|
+
})
|
|
284
|
+
.finally(() => clearInterval(renewal));
|
|
285
|
+
// Bookkeeping after a confirmed settlement must not fail the charged request.
|
|
286
|
+
await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store }).catch(release);
|
|
287
|
+
return txHash;
|
|
281
288
|
}
|
|
282
289
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
283
290
|
export async function settle(store_, client, channelId_, options) {
|
|
@@ -311,34 +318,61 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
311
318
|
: undefined);
|
|
312
319
|
const receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
313
320
|
const settled = readSettledReceiptFields(Chain.getChannelEvent(receipt, 'Settled', channelId));
|
|
314
|
-
const { newSettled } = settled;
|
|
321
|
+
const { deltaPaid, newSettled } = settled;
|
|
315
322
|
if (newSettled < amount)
|
|
316
323
|
throw new VerificationFailedError({ reason: 'Settled event is below voucher amount' });
|
|
317
|
-
|
|
324
|
+
// A replica behind the receipt's block would report the pre-settlement state.
|
|
325
|
+
const state = await Chain.readbackWithRetry(() => Chain.getChannelState(client, channelId, escrow, receipt.blockNumber));
|
|
318
326
|
if (state.settled !== newSettled)
|
|
319
327
|
throw new VerificationFailedError({
|
|
320
328
|
reason: 'on-chain channel state does not match settle receipt',
|
|
321
329
|
});
|
|
322
|
-
|
|
330
|
+
let checkpointError;
|
|
331
|
+
await store
|
|
332
|
+
.updateChannel(channelId, (current) => current
|
|
323
333
|
? {
|
|
324
334
|
...current,
|
|
325
335
|
settledOnChain: newSettled > current.settledOnChain ? newSettled : current.settledOnChain,
|
|
326
336
|
lastSettlementAt: new Date().toISOString(),
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
// Charges accepted after the voucher was read are not covered by this transaction.
|
|
338
|
+
lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n, channel.spent),
|
|
339
|
+
lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0, channel.units),
|
|
329
340
|
}
|
|
330
|
-
: current)
|
|
341
|
+
: current)
|
|
342
|
+
.catch((cause) => {
|
|
343
|
+
checkpointError = { cause };
|
|
344
|
+
});
|
|
331
345
|
if (options?.onSessionSettlement) {
|
|
332
346
|
await emitSessionSettlement(options.onSessionSettlement, {
|
|
333
347
|
txHash,
|
|
334
348
|
channelId,
|
|
335
349
|
trigger: 'settle',
|
|
336
350
|
amount: newSettled,
|
|
337
|
-
|
|
351
|
+
// The stored checkpoint can be stale after a failed write; the receipt is authoritative.
|
|
352
|
+
delta: deltaPaid,
|
|
338
353
|
});
|
|
339
354
|
}
|
|
355
|
+
if (checkpointError)
|
|
356
|
+
throw new SettlementCheckpointError({ cause: checkpointError.cause, txHash });
|
|
340
357
|
return txHash;
|
|
341
358
|
}
|
|
359
|
+
/** Raised when a settlement transaction confirmed but the channel store could not record it. */
|
|
360
|
+
export class SettlementCheckpointError extends Error {
|
|
361
|
+
name = 'SettlementCheckpointError';
|
|
362
|
+
/** Hash of the confirmed settlement transaction. */
|
|
363
|
+
txHash;
|
|
364
|
+
constructor(options) {
|
|
365
|
+
super(`Settlement ${options.txHash} confirmed but was not recorded.`, { cause: options.cause });
|
|
366
|
+
this.txHash = options.txHash;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** Raised when a scheduled settlement could not claim its lease; no transaction was submitted. */
|
|
370
|
+
export class SettlementLeaseError extends Error {
|
|
371
|
+
name = 'SettlementLeaseError';
|
|
372
|
+
constructor(options) {
|
|
373
|
+
super('Scheduled settlement lease could not be claimed.', { cause: options.cause });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
342
376
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
343
377
|
export async function settleBatch(store, client, channelIds, options) {
|
|
344
378
|
const hashes = [];
|
|
@@ -346,6 +380,43 @@ export async function settleBatch(store, client, channelIds, options) {
|
|
|
346
380
|
hashes.push(await settle(store, client, channelId, options));
|
|
347
381
|
return hashes;
|
|
348
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* @internal Keeps a charged request served when its scheduled settlement hit a transport or RPC
|
|
385
|
+
* failure; the next settlement retries it. Reverts and configuration errors fail the request.
|
|
386
|
+
*/
|
|
387
|
+
export function ignoreRetryableSettlementFailure(error) {
|
|
388
|
+
if (isRetryableSettlementFailure(error))
|
|
389
|
+
return undefined;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
function isRetryableSettlementFailure(error) {
|
|
393
|
+
if (error instanceof SettlementLeaseError)
|
|
394
|
+
return true;
|
|
395
|
+
if (!(error instanceof viem_BaseError))
|
|
396
|
+
return false;
|
|
397
|
+
// Only unavailable or overloaded upstreams clear on retry; node-rejected transactions repeat.
|
|
398
|
+
return Boolean(error.walk((cause) => (cause instanceof HttpRequestError &&
|
|
399
|
+
(cause.status === undefined || cause.status === 429 || cause.status >= 500)) ||
|
|
400
|
+
// Rate limits some providers return in a JSON-RPC body rather than as HTTP 429.
|
|
401
|
+
(cause instanceof RpcRequestError && cause.code === 429) ||
|
|
402
|
+
cause instanceof InternalRpcError ||
|
|
403
|
+
cause instanceof LimitExceededRpcError ||
|
|
404
|
+
cause instanceof ResourceUnavailableRpcError ||
|
|
405
|
+
cause instanceof SocketClosedError ||
|
|
406
|
+
cause instanceof TimeoutError ||
|
|
407
|
+
// The settlement was broadcast; a later settlement collects the charge if it never lands.
|
|
408
|
+
cause instanceof WaitForTransactionReceiptTimeoutError ||
|
|
409
|
+
cause instanceof WebSocketRequestError));
|
|
410
|
+
}
|
|
411
|
+
/** @internal Reports a settlement failure without letting observer errors replace it. */
|
|
412
|
+
export async function reportSessionSettlementFailure(onSessionSettlementFailure, context) {
|
|
413
|
+
try {
|
|
414
|
+
await onSessionSettlementFailure?.(Object.freeze(context));
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// Errors are isolated: observers cannot replace the settlement failure.
|
|
418
|
+
}
|
|
419
|
+
}
|
|
349
420
|
async function emitSessionSettlement(onSessionSettlement, context) {
|
|
350
421
|
try {
|
|
351
422
|
await onSessionSettlement(Object.freeze(context));
|
|
@@ -2,5 +2,5 @@ export { charge, session, settle, settleBatch } from './Session.js';
|
|
|
2
2
|
/** SSE helpers and types for Tempo session streams. */
|
|
3
3
|
export * as Sse from './Sse.js';
|
|
4
4
|
/** Server-side automatic settlement schedule. */
|
|
5
|
-
export type { OnSessionSettlement, ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, SessionSettlementContext, SettlementSchedule, } from './Session.js';
|
|
5
|
+
export type { OnSessionSettlement, OnSessionSettlementFailure, ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, SessionSettlementContext, SessionSettlementFailureContext, SettlementSchedule, } from './Session.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,6 +7,7 @@ import * as Credential_ from '../../Credential.js';
|
|
|
7
7
|
import { VerificationFailedError } from '../../Errors.js';
|
|
8
8
|
import * as Types from '../../evm/Types.js';
|
|
9
9
|
import * as PaymentRequest from '../../PaymentRequest.js';
|
|
10
|
+
import * as ChallengeMeta from '../../server/internal/challengeMeta.js';
|
|
10
11
|
import * as Scope from '../../server/internal/scope.js';
|
|
11
12
|
import * as x402_Header from '../Header.js';
|
|
12
13
|
import * as x402_RouteBinding from '../internal/RouteBinding.js';
|
|
@@ -83,8 +84,9 @@ export function createPath(config) {
|
|
|
83
84
|
const clientNonce = paymentPayload.extensions?.[mppxExtensionKey]?.info.nonce;
|
|
84
85
|
const isRouteBound = clientNonce !== undefined;
|
|
85
86
|
const routeRequiresBinding = challenge.digest !== undefined ||
|
|
86
|
-
challenge.
|
|
87
|
-
|
|
87
|
+
(challenge.meta === undefined
|
|
88
|
+
? challenge.opaque !== undefined
|
|
89
|
+
: ChallengeMeta.routeMeta(challenge.meta) !== undefined);
|
|
88
90
|
// `extensions.mppx` binding is not part of the x402 spec, so a client mppx
|
|
89
91
|
// did not write cannot produce it. Requiring it makes every scoped route —
|
|
90
92
|
// and everything behind `Proxy`, which scopes what it serves — unpayable by
|
|
@@ -296,7 +298,12 @@ function routeExtensions(challenge, input) {
|
|
|
296
298
|
binding[Scope.reservedMetaKey] = scope;
|
|
297
299
|
if (challenge.digest !== undefined)
|
|
298
300
|
binding.digest = challenge.digest;
|
|
299
|
-
const
|
|
301
|
+
const meta = ChallengeMeta.routeMeta(challenge.meta);
|
|
302
|
+
const opaque = challenge.meta === undefined
|
|
303
|
+
? challenge.opaque
|
|
304
|
+
: meta
|
|
305
|
+
? PaymentRequest.serialize(meta)
|
|
306
|
+
: undefined;
|
|
300
307
|
if (opaque !== undefined)
|
|
301
308
|
binding.opaque = opaque;
|
|
302
309
|
return {
|