mppx 0.11.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.
- package/CHANGELOG.md +83 -0
- package/README.md +22 -1
- package/dist/Method.d.ts +7 -1
- package/dist/Method.js +7 -2
- package/dist/Receipt.d.ts +2 -0
- package/dist/Receipt.js +2 -0
- package/dist/cli/agent.d.ts +3 -0
- package/dist/cli/agent.js +20 -0
- package/dist/cli/cli.d.ts +9 -1
- package/dist/cli/cli.js +134 -136
- package/dist/cli/plugins/index.js +12 -14
- package/dist/cli/plugins/tempo.js +9 -2
- package/dist/cli/utils.d.ts +11 -1
- package/dist/cli/utils.js +15 -2
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.js +19 -4
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.js +10 -1
- package/dist/cli/validate/payment.js +63 -24
- package/dist/client/Methods.d.ts +0 -2
- package/dist/client/Methods.js +0 -2
- package/dist/client/Mppx.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.js +8 -2
- package/dist/client/internal/MethodChallenge.d.ts +1 -0
- package/dist/evm/Types.js +1 -1
- package/dist/evm/server/Charge.js +5 -0
- package/dist/evm/server/Methods.d.ts +164 -3
- package/dist/evm/server/Methods.js +50 -2
- package/dist/integrations/x402/Evm.js +4 -3
- package/dist/internal/HeaderCodec.d.ts +2 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.js +4 -7
- package/dist/middlewares/elysia.d.ts +11 -8
- package/dist/middlewares/elysia.js +67 -14
- package/dist/middlewares/express.js +13 -6
- package/dist/middlewares/hono.js +4 -4
- package/dist/middlewares/nextjs.js +9 -5
- package/dist/proxy/internal/Headers.js +65 -0
- package/dist/proxy/services/stripe.js +1 -0
- package/dist/server/Mppx.d.ts +18 -2
- package/dist/server/Mppx.js +204 -92
- package/dist/server/NodeListener.js +71 -6
- package/dist/server/Request.d.ts +11 -1
- package/dist/server/Request.js +132 -13
- package/dist/server/Transport.d.ts +14 -1
- package/dist/server/Transport.js +1 -0
- package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
- package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
- package/dist/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +19 -6
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/Methods.d.ts +1 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Charge.js +17 -13
- package/dist/tempo/client/Methods.d.ts +1 -5
- package/dist/tempo/client/Methods.js +10 -5
- package/dist/tempo/client/ResolveAccount.d.ts +3 -0
- package/dist/tempo/client/index.d.ts +0 -3
- package/dist/tempo/client/index.js +0 -2
- package/dist/tempo/internal/currencies.d.ts +19 -0
- package/dist/tempo/internal/currencies.js +41 -0
- package/dist/tempo/internal/defaults.d.ts +5 -3
- package/dist/tempo/internal/defaults.js +6 -2
- package/dist/tempo/internal/fee-payer.d.ts +11 -7
- package/dist/tempo/internal/fee-payer.js +30 -11
- package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
- package/dist/tempo/internal/machine-token-charge.js +2 -1
- package/dist/tempo/legacy/index.d.ts +1 -3
- package/dist/tempo/legacy/index.js +1 -3
- package/dist/tempo/server/Charge.d.ts +12 -1
- package/dist/tempo/server/Charge.js +109 -78
- package/dist/tempo/server/Methods.d.ts +5048 -40
- package/dist/tempo/server/Methods.js +104 -13
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.js +28 -11
- package/dist/tempo/session/client/ChannelOps.js +5 -0
- package/dist/tempo/session/client/Session.d.ts +1 -0
- package/dist/tempo/session/client/Session.js +7 -4
- package/dist/tempo/session/client/SessionManager.js +3 -0
- package/dist/tempo/session/client/Transports.d.ts +8 -1
- package/dist/tempo/session/client/Transports.js +12 -3
- package/dist/tempo/session/precompile/Chain.js +16 -1
- package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
- package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
- package/dist/tempo/session/precompile/Voucher.js +48 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
- package/dist/tempo/session/server/ChannelStore.js +37 -7
- package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -19
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/RequestState.d.ts +6 -0
- package/dist/tempo/session/server/RequestState.js +11 -4
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +10 -2
- package/dist/tempo/session/server/Settlement.d.ts +23 -0
- package/dist/tempo/session/server/Settlement.js +105 -25
- package/dist/tempo/session/server/Sse.d.ts +3 -1
- package/dist/tempo/session/server/Sse.js +105 -32
- package/dist/tempo/session/server/Transports.d.ts +24 -19
- package/dist/tempo/session/server/Transports.js +169 -58
- package/dist/tempo/session/server/Ws.d.ts +5 -1
- package/dist/tempo/session/server/Ws.js +75 -20
- package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.js +3 -0
- package/dist/validation/core.js +2 -1
- package/dist/x402/server/EvmCharge.d.ts +3 -1
- package/dist/x402/server/EvmCharge.js +14 -1
- package/package.json +8 -8
- package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
- package/dist/tempo/legacy/client/ChannelOps.js +0 -168
- package/dist/tempo/legacy/client/Session.d.ts +0 -185
- package/dist/tempo/legacy/client/Session.js +0 -279
- package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
- package/dist/tempo/legacy/client/SessionManager.js +0 -706
- package/dist/tempo/legacy/client/index.d.ts +0 -7
- package/dist/tempo/legacy/client/index.js +0 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAddress, isAddressEqual, zeroAddress, } from 'viem';
|
|
2
|
-
import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
|
|
2
|
+
import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import * as Chain from '../precompile/Chain.js';
|
|
4
4
|
import { readChannelClosedReceiptFields } from '../precompile/Chain.js';
|
|
5
5
|
import * as Channel from '../precompile/Channel.js';
|
|
@@ -7,7 +7,7 @@ import { createSessionReceipt, uint96, } from '../precompile/Protocol.js';
|
|
|
7
7
|
import * as Voucher from '../precompile/Voucher.js';
|
|
8
8
|
import * as ChannelStore from './ChannelStore.js';
|
|
9
9
|
import { getChallengePaymentFields } from './RequestState.js';
|
|
10
|
-
import { assertSettlementSender, getClientAccount } from './Settlement.js';
|
|
10
|
+
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, } from './Settlement.js';
|
|
11
11
|
/** Returns the effective voucher signer for a TIP-1034 descriptor. */
|
|
12
12
|
export function authorizedSigner(descriptor) {
|
|
13
13
|
return isAddressEqual(descriptor.authorizedSigner, zeroAddress)
|
|
@@ -233,6 +233,7 @@ export function requireSessionCredentialPayload(payload) {
|
|
|
233
233
|
descriptor: readDescriptor(candidate.descriptor),
|
|
234
234
|
cumulativeAmount: readRawAmount(candidate.cumulativeAmount, 'cumulativeAmount'),
|
|
235
235
|
signature: readHex(candidate.signature, 'signature'),
|
|
236
|
+
closeSignature: readHex(candidate.closeSignature, 'closeSignature'),
|
|
236
237
|
};
|
|
237
238
|
}
|
|
238
239
|
}
|
|
@@ -375,6 +376,8 @@ async function validateVoucherCredential(parameters, payload) {
|
|
|
375
376
|
escrow,
|
|
376
377
|
lastOnChainVerified,
|
|
377
378
|
});
|
|
379
|
+
if (parameters.allowPendingClose && channelState.closeRequestedAt !== 0)
|
|
380
|
+
return;
|
|
378
381
|
await ChannelStore.validateVoucher({
|
|
379
382
|
channel,
|
|
380
383
|
channelState,
|
|
@@ -384,10 +387,12 @@ async function validateVoucherCredential(parameters, payload) {
|
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
389
|
async function resolveVoucherChannelState(parameters) {
|
|
387
|
-
const { channel, channelId, channelStateTtl, client, escrow, lastOnChainVerified } = parameters;
|
|
388
|
-
const
|
|
389
|
-
const state =
|
|
390
|
-
if (state)
|
|
390
|
+
const { channel, channelId, channelStateTtl, client, escrow, forceRefresh, lastOnChainVerified } = parameters;
|
|
391
|
+
const shouldRefresh = forceRefresh || Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl;
|
|
392
|
+
const state = shouldRefresh ? await Chain.getChannelState(client, channelId, escrow) : undefined;
|
|
393
|
+
if (state?.closeRequestedAt)
|
|
394
|
+
lastOnChainVerified.delete(channelId);
|
|
395
|
+
else if (state)
|
|
391
396
|
lastOnChainVerified.set(channelId, Date.now());
|
|
392
397
|
return {
|
|
393
398
|
deposit: state?.deposit ?? uint96(channel.deposit),
|
|
@@ -410,6 +415,9 @@ async function validateCloseCredential(parameters, payload) {
|
|
|
410
415
|
escrow,
|
|
411
416
|
store,
|
|
412
417
|
});
|
|
418
|
+
const closeAuthorized = Voucher.verifyCloseAuthorization(escrow, chainId, { channelId, cumulativeAmount, signature: payload.closeSignature }, [channel.payer, channel.authorizedSigner]);
|
|
419
|
+
if (!closeAuthorized)
|
|
420
|
+
throw new InvalidSignatureError({ reason: 'invalid close authorization signature' });
|
|
413
421
|
if (channel.finalized)
|
|
414
422
|
throw new ChannelClosedError({ reason: 'channel is already finalized' });
|
|
415
423
|
const state = await Chain.getChannelState(client, channelId, escrow);
|
|
@@ -513,18 +521,26 @@ async function handleOpenCredential(parameters) {
|
|
|
513
521
|
const { descriptor, state } = result;
|
|
514
522
|
assertSameDescriptor(descriptor, payload.descriptor);
|
|
515
523
|
validateChannelState(state, request.amount);
|
|
516
|
-
const updated = await store.updateChannel(channelId, (current) =>
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
524
|
+
const updated = await store.updateChannel(channelId, (current) => {
|
|
525
|
+
if (parameters.requireVoucherAdvance &&
|
|
526
|
+
current !== null &&
|
|
527
|
+
cumulativeAmount <= current.highestVoucherAmount)
|
|
528
|
+
throw new DeltaTooSmallError({
|
|
529
|
+
reason: 'voucher does not add new funds for this request',
|
|
530
|
+
});
|
|
531
|
+
return ChannelStore.openChannelState({
|
|
532
|
+
authorizedSigner: authorizedSigner(descriptor),
|
|
533
|
+
chainId,
|
|
534
|
+
channelId,
|
|
535
|
+
current,
|
|
536
|
+
descriptor,
|
|
537
|
+
escrow,
|
|
538
|
+
expiringNonceHash: result.expiringNonceHash,
|
|
539
|
+
cumulativeAmount,
|
|
540
|
+
signature: payload.signature,
|
|
541
|
+
state,
|
|
542
|
+
});
|
|
543
|
+
});
|
|
528
544
|
if (!updated)
|
|
529
545
|
throw new VerificationFailedError({ reason: 'failed to create channel' });
|
|
530
546
|
return createSessionReceipt({
|
|
@@ -603,21 +619,39 @@ async function handleVoucherCredential(parameters) {
|
|
|
603
619
|
channelStateTtl,
|
|
604
620
|
client,
|
|
605
621
|
escrow,
|
|
622
|
+
forceRefresh: true,
|
|
606
623
|
lastOnChainVerified,
|
|
607
624
|
});
|
|
608
625
|
if (channelState.closeRequestedAt !== 0) {
|
|
609
|
-
await store.updateChannel(channelId, (current) => current
|
|
626
|
+
const closing = await store.updateChannel(channelId, (current) => current
|
|
610
627
|
? {
|
|
611
628
|
...current,
|
|
629
|
+
settledOnChain: channelState.settled > current.settledOnChain
|
|
630
|
+
? channelState.settled
|
|
631
|
+
: current.settledOnChain,
|
|
612
632
|
closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
|
|
613
633
|
? BigInt(channelState.closeRequestedAt)
|
|
614
634
|
: current.closeRequestedAt,
|
|
615
635
|
}
|
|
616
636
|
: current);
|
|
637
|
+
if (closing && !closing.finalized && channelState.deposit > 0n)
|
|
638
|
+
await maybeSettleScheduled({
|
|
639
|
+
account: parameters.account,
|
|
640
|
+
channel: closing,
|
|
641
|
+
client,
|
|
642
|
+
feePayer: parameters.feePayer,
|
|
643
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
644
|
+
feeToken: parameters.feeToken,
|
|
645
|
+
onSessionSettlement: parameters.onSessionSettlement,
|
|
646
|
+
schedule: {},
|
|
647
|
+
store,
|
|
648
|
+
});
|
|
649
|
+
throw new ChannelClosedError({ reason: 'channel has a pending close request' });
|
|
617
650
|
}
|
|
618
651
|
return ChannelStore.verifyAndAcceptVoucher({
|
|
619
652
|
store,
|
|
620
653
|
minVoucherDelta,
|
|
654
|
+
requireAdvance: parameters.requireVoucherAdvance,
|
|
621
655
|
challenge,
|
|
622
656
|
channel,
|
|
623
657
|
voucher,
|
|
@@ -640,6 +674,9 @@ async function handleCloseCredential(parameters) {
|
|
|
640
674
|
escrow,
|
|
641
675
|
store,
|
|
642
676
|
});
|
|
677
|
+
const closeAuthorized = Voucher.verifyCloseAuthorization(escrow, chainId, { channelId, cumulativeAmount, signature: payload.closeSignature }, [channel.payer, channel.authorizedSigner]);
|
|
678
|
+
if (!closeAuthorized)
|
|
679
|
+
throw new InvalidSignatureError({ reason: 'invalid close authorization signature' });
|
|
643
680
|
if (channel.finalized)
|
|
644
681
|
throw new ChannelClosedError({ reason: 'channel is already finalized' });
|
|
645
682
|
const state = await Chain.getChannelState(client, channelId, escrow);
|
|
@@ -8,8 +8,9 @@ export type SessionController = {
|
|
|
8
8
|
* Reserve voucher coverage for the next emitted chunk.
|
|
9
9
|
*
|
|
10
10
|
* The reservation blocks until sufficient voucher headroom exists, but the
|
|
11
|
-
* charge is
|
|
12
|
-
*
|
|
11
|
+
* charge is committed when a chunk is emitted or when the generator finishes
|
|
12
|
+
* successfully. A reservation is only dropped when the stream aborts or the
|
|
13
|
+
* generator fails before the charge can be committed.
|
|
13
14
|
*
|
|
14
15
|
* Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
|
|
15
16
|
* pricing. When omitted, the session challenge's configured tick cost is
|
|
@@ -1,46 +1,102 @@
|
|
|
1
|
-
import { commitReservedCharges, reserveChargeOrWait } from './Transports.js';
|
|
1
|
+
import { commitReservedCharges, releaseReservedCharges, renewReservedCharges, reserveChargeOrWait, streamReservationLeaseMs, } from './Transports.js';
|
|
2
2
|
/** Applies voucher reservation and spend commits to an async session stream. */
|
|
3
3
|
export async function* meterIterable(options) {
|
|
4
4
|
let prepaidUnits = options.prepaidUnits ?? 0;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const signal = controller.signal;
|
|
7
|
+
const reservation = {
|
|
8
|
+
store: options.store,
|
|
9
|
+
channelId: options.channelId,
|
|
10
|
+
reservationId: globalThis.crypto.randomUUID(),
|
|
11
|
+
};
|
|
12
|
+
let pending = false;
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
const onAbort = () => {
|
|
15
|
+
if (signal.aborted)
|
|
16
|
+
return;
|
|
17
|
+
cancelled = true;
|
|
18
|
+
controller.abort(options.signal?.reason);
|
|
19
|
+
};
|
|
20
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
21
|
+
if (options.signal?.aborted)
|
|
22
|
+
onAbort();
|
|
23
|
+
const renewal = setInterval(() => {
|
|
24
|
+
if (pending)
|
|
25
|
+
void renewReservedCharges(reservation).catch((error) => controller.abort(error));
|
|
26
|
+
}, streamReservationLeaseMs / 3);
|
|
27
|
+
renewal.unref?.();
|
|
28
|
+
const releaseOnAbort = () => {
|
|
29
|
+
clearInterval(renewal);
|
|
30
|
+
void releaseReservedCharges(reservation).catch(() => { });
|
|
31
|
+
};
|
|
32
|
+
signal.addEventListener('abort', releaseOnAbort, { once: true });
|
|
33
|
+
if (signal.aborted)
|
|
34
|
+
releaseOnAbort();
|
|
7
35
|
const charge = async (amount) => {
|
|
36
|
+
signal.throwIfAborted();
|
|
8
37
|
if (amount === undefined && prepaidUnits > 0) {
|
|
9
38
|
prepaidUnits -= 1;
|
|
10
39
|
return;
|
|
11
40
|
}
|
|
12
|
-
|
|
41
|
+
if ((amount ?? options.tickCost) === 0n)
|
|
42
|
+
return;
|
|
13
43
|
await reserveChargeOrWait({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
amount:
|
|
17
|
-
reservedAmount,
|
|
44
|
+
...reservation,
|
|
45
|
+
requireExisting: pending,
|
|
46
|
+
amount: amount ?? options.tickCost,
|
|
18
47
|
emit: options.emitNeedVoucher,
|
|
19
48
|
formatNeedVoucher: options.formatNeedVoucher,
|
|
20
49
|
pollIntervalMs: options.pollIntervalMs,
|
|
21
|
-
signal
|
|
50
|
+
signal,
|
|
22
51
|
});
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
pending = true;
|
|
53
|
+
signal.throwIfAborted();
|
|
25
54
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
const commit = async () => {
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
if (!pending)
|
|
58
|
+
return;
|
|
59
|
+
const channel = await commitReservedCharges(reservation);
|
|
60
|
+
if (!channel)
|
|
61
|
+
throw new Error('stream reservation was lost');
|
|
62
|
+
pending = false;
|
|
63
|
+
await options.onChargeCommitted?.(channel);
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const manual = typeof options.generate === 'function';
|
|
67
|
+
const iterable = typeof options.generate === 'function'
|
|
68
|
+
? options.generate({ charge, signal })
|
|
69
|
+
: options.generate;
|
|
70
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
71
|
+
try {
|
|
72
|
+
while (!signal.aborted) {
|
|
73
|
+
const { done, value } = await iterator.next();
|
|
74
|
+
if (signal.aborted)
|
|
75
|
+
break;
|
|
76
|
+
if (done) {
|
|
77
|
+
if (manual)
|
|
78
|
+
await commit();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!manual)
|
|
82
|
+
await charge();
|
|
83
|
+
await commit();
|
|
84
|
+
yield value;
|
|
85
|
+
}
|
|
86
|
+
if (!cancelled)
|
|
87
|
+
signal.throwIfAborted();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearInterval(renewal);
|
|
91
|
+
await releaseReservedCharges(reservation);
|
|
92
|
+
await iterator.return?.();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
clearInterval(renewal);
|
|
97
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
98
|
+
signal.removeEventListener('abort', releaseOnAbort);
|
|
99
|
+
await releaseReservedCharges(reservation);
|
|
44
100
|
}
|
|
45
101
|
}
|
|
46
102
|
//# sourceMappingURL=MeteredStream.js.map
|
|
@@ -18,6 +18,10 @@ export type ResolveSessionSnapshotParameters = {
|
|
|
18
18
|
channelId: Hex | undefined;
|
|
19
19
|
/** Payment fields the reusable channel must match before it is advertised. */
|
|
20
20
|
expected?: SessionSnapshotPaymentFields | undefined;
|
|
21
|
+
/** Minimum raw-unit increase required when the next voucher must advance. */
|
|
22
|
+
minimumVoucherAdvance?: bigint | undefined;
|
|
23
|
+
/** Whether the next voucher must advance beyond the accepted cumulative amount. */
|
|
24
|
+
requireVoucherAdvance?: boolean | undefined;
|
|
21
25
|
/** Server channel store. */
|
|
22
26
|
store: ChannelStore.ChannelStore;
|
|
23
27
|
};
|
|
@@ -149,12 +153,14 @@ export type ResolveSessionPaymentRequestParameters = {
|
|
|
149
153
|
decimals: number;
|
|
150
154
|
defaultFeePayer?: viem_Account | undefined;
|
|
151
155
|
getClient: ResolveRequestChainIdParameters['getClient'];
|
|
156
|
+
minVoucherDelta?: string | undefined;
|
|
152
157
|
parameterChainId?: number | undefined;
|
|
153
158
|
parameterEscrowContract?: Address | undefined;
|
|
154
159
|
parameterFeePayer?: ParameterFeePayer;
|
|
155
160
|
request: SessionPaymentRequestInput;
|
|
156
161
|
resolveChannelId?: ResolveSessionChannelId | undefined;
|
|
157
162
|
source?: string | undefined;
|
|
163
|
+
sseEnabled?: boolean | undefined;
|
|
158
164
|
store: ChannelStore.ChannelStore;
|
|
159
165
|
};
|
|
160
166
|
/** Inputs used to resolve shared context for credential verification. */
|
|
@@ -47,7 +47,7 @@ export async function resolveSessionChannelId(parameters) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Builds server bootstrap hints for a reusable precompile session channel. */
|
|
49
49
|
export async function resolveSessionSnapshot(parameters) {
|
|
50
|
-
const { amount, channelId, expected, store } = parameters;
|
|
50
|
+
const { amount, channelId, expected, minimumVoucherAdvance = 0n, requireVoucherAdvance = false, store, } = parameters;
|
|
51
51
|
if (!channelId)
|
|
52
52
|
return undefined;
|
|
53
53
|
const channel = await store.getChannel(ChannelStore.normalizeChannelId(channelId));
|
|
@@ -63,7 +63,11 @@ export async function resolveSessionSnapshot(parameters) {
|
|
|
63
63
|
return undefined;
|
|
64
64
|
if (expected && !matchesSnapshotPaymentFields(channel, expected))
|
|
65
65
|
return undefined;
|
|
66
|
-
const
|
|
66
|
+
const spendBoundary = channel.spent + amount;
|
|
67
|
+
const advanceBoundary = requireVoucherAdvance
|
|
68
|
+
? channel.highestVoucherAmount + (minimumVoucherAdvance > 0n ? minimumVoucherAdvance : 1n)
|
|
69
|
+
: 0n;
|
|
70
|
+
const requiredCumulative = spendBoundary > advanceBoundary ? spendBoundary : advanceBoundary;
|
|
67
71
|
return {
|
|
68
72
|
acceptedCumulative: channel.highestVoucherAmount.toString(),
|
|
69
73
|
chainId: channel.chainId,
|
|
@@ -158,7 +162,7 @@ export async function resolveRequestChainId(parameters) {
|
|
|
158
162
|
}
|
|
159
163
|
/** Resolves request-time TIP-1034 details and server bootstrap hints for a challenge. */
|
|
160
164
|
export async function resolveSessionPaymentRequest(parameters) {
|
|
161
|
-
const { capturedRequest, credential, decimals, defaultFeePayer, getClient, parameterChainId, parameterEscrowContract, parameterFeePayer, request, resolveChannelId, source, store, } = parameters;
|
|
165
|
+
const { capturedRequest, credential, decimals, defaultFeePayer, getClient, minVoucherDelta, parameterChainId, parameterEscrowContract, parameterFeePayer, request, resolveChannelId, source, sseEnabled, store, } = parameters;
|
|
162
166
|
const chainId = await resolveRequestChainId({
|
|
163
167
|
getClient,
|
|
164
168
|
parameterChainId,
|
|
@@ -172,6 +176,7 @@ export async function resolveSessionPaymentRequest(parameters) {
|
|
|
172
176
|
const escrowContract = resolveRequestEscrowContract(request.escrowContract, parameterEscrowContract);
|
|
173
177
|
const operator = resolveRequestOperator(request.operator);
|
|
174
178
|
const requestAmount = parseUnits(request.amount, decimals);
|
|
179
|
+
const isContentRequest = capturedRequest ? isSessionContentRequest(capturedRequest) : false;
|
|
175
180
|
const channelId = await resolveSessionChannelId({
|
|
176
181
|
capturedRequest,
|
|
177
182
|
credential,
|
|
@@ -181,7 +186,7 @@ export async function resolveSessionPaymentRequest(parameters) {
|
|
|
181
186
|
store,
|
|
182
187
|
});
|
|
183
188
|
const sessionSnapshot = await resolveSessionSnapshot({
|
|
184
|
-
amount: capturedRequest && !
|
|
189
|
+
amount: capturedRequest && !isContentRequest ? 0n : requestAmount,
|
|
185
190
|
channelId,
|
|
186
191
|
expected: {
|
|
187
192
|
chainId,
|
|
@@ -189,6 +194,8 @@ export async function resolveSessionPaymentRequest(parameters) {
|
|
|
189
194
|
escrowContract,
|
|
190
195
|
recipient: readChallengeAddress(request.recipient, 'recipient'),
|
|
191
196
|
},
|
|
197
|
+
minimumVoucherAdvance: parseUnits(minVoucherDelta ?? '0', decimals),
|
|
198
|
+
requireVoucherAdvance: Boolean(capturedRequest) && !sseEnabled && isContentRequest,
|
|
192
199
|
store,
|
|
193
200
|
});
|
|
194
201
|
const { operator: _operator, ...requestWithoutOperator } = request;
|
|
@@ -59,6 +59,7 @@ export declare function session<const parameters extends session.Parameters>(p?:
|
|
|
59
59
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
60
60
|
action: import("zod/mini").ZodMiniLiteral<"close">;
|
|
61
61
|
channelId: import("zod/mini").ZodMiniString<string>;
|
|
62
|
+
closeSignature: import("zod/mini").ZodMiniString<string>;
|
|
62
63
|
cumulativeAmount: import("zod/mini").ZodMiniString<string>;
|
|
63
64
|
descriptor: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<import("../precompile/Protocol.js").ChannelDescriptor, import("../precompile/Protocol.js").ChannelDescriptor>>;
|
|
64
65
|
signature: import("zod/mini").ZodMiniString<string>;
|
|
@@ -137,7 +138,7 @@ export declare namespace session {
|
|
|
137
138
|
type FeePayerPolicy = Partial<FeePayer.Policy>;
|
|
138
139
|
/** Parameters accepted by the TIP-1034 server session payment method. */
|
|
139
140
|
type Parameters = {
|
|
140
|
-
/** TTL in milliseconds for cached on-chain channel state
|
|
141
|
+
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation. Voucher acceptance always refreshes on-chain state. @default 5_000 */
|
|
141
142
|
channelStateTtl?: number | undefined;
|
|
142
143
|
/** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
|
|
143
144
|
feePayerPolicy?: FeePayerPolicy | undefined;
|
|
@@ -26,7 +26,7 @@ import { broadcastCredentialPayload, validateCredentialPayload } from './Credent
|
|
|
26
26
|
import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
27
27
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
28
28
|
import { respondToSessionCredential } from './RequestState.js';
|
|
29
|
-
import { applyVerifiedHttpAccounting, chargeSessionChannel, } from './Settlement.js';
|
|
29
|
+
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
30
30
|
import { isSettlementDue, maybeSettleScheduled } from './Settlement.js';
|
|
31
31
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
32
32
|
import * as Ws from './Ws.js';
|
|
@@ -243,7 +243,7 @@ export function session(p) {
|
|
|
243
243
|
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
244
244
|
})
|
|
245
245
|
: undefined;
|
|
246
|
-
const validateCredential = async ({ credential, request, }) => {
|
|
246
|
+
const validateCredential = async ({ credential, operation, request, }) => {
|
|
247
247
|
const payload = requireSessionCredentialPayload(credential.payload);
|
|
248
248
|
const context = await resolveCredentialVerificationContext({
|
|
249
249
|
decimals,
|
|
@@ -254,6 +254,7 @@ export function session(p) {
|
|
|
254
254
|
});
|
|
255
255
|
const details = await validateCredentialPayload({
|
|
256
256
|
account,
|
|
257
|
+
allowPendingClose: operation === 'broadcast',
|
|
257
258
|
challenge: credential.challenge,
|
|
258
259
|
channelStateTtl,
|
|
259
260
|
chainId: context.chainId,
|
|
@@ -305,6 +306,11 @@ export function session(p) {
|
|
|
305
306
|
minVoucherDelta: context.minVoucherDelta,
|
|
306
307
|
onSessionSettlement,
|
|
307
308
|
payload,
|
|
309
|
+
requireVoucherAdvance: shouldApplyVerifiedHttpAccounting({
|
|
310
|
+
capturedRequest: envelope?.capturedRequest,
|
|
311
|
+
payloadAction: payload.action,
|
|
312
|
+
sseEnabled: Boolean(parameters.sse),
|
|
313
|
+
}),
|
|
308
314
|
store,
|
|
309
315
|
});
|
|
310
316
|
return applyVerifiedHttpAccounting({
|
|
@@ -372,11 +378,13 @@ export function session(p) {
|
|
|
372
378
|
decimals,
|
|
373
379
|
defaultFeePayer: feePayer,
|
|
374
380
|
getClient,
|
|
381
|
+
minVoucherDelta: parameters.minVoucherDelta,
|
|
375
382
|
parameterChainId: parameters.chainId,
|
|
376
383
|
parameterEscrowContract: parameters.escrowContract,
|
|
377
384
|
parameterFeePayer: configuredFeePayer,
|
|
378
385
|
request,
|
|
379
386
|
resolveChannelId: parameters.resolveChannelId,
|
|
387
|
+
sseEnabled: Boolean(parameters.sse),
|
|
380
388
|
store,
|
|
381
389
|
});
|
|
382
390
|
return {
|
|
@@ -89,6 +89,7 @@ export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePr
|
|
|
89
89
|
/** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
|
|
90
90
|
export type MarkSettlementCompleteParameters = {
|
|
91
91
|
channelId: ChannelStore.State['channelId'];
|
|
92
|
+
leaseOwner: string;
|
|
92
93
|
settledAt?: string | undefined;
|
|
93
94
|
store: ChannelStore.ChannelStore;
|
|
94
95
|
};
|
|
@@ -106,6 +107,26 @@ export declare function resolveSettlementProgress(channel: ChannelStore.State):
|
|
|
106
107
|
export declare function isSettlementDue(channel: ChannelStore.State, schedule: ResolvedSettlementSchedule | undefined): boolean;
|
|
107
108
|
/** Records the channel spend/unit counters that a scheduled settlement captured. */
|
|
108
109
|
export declare function markSettlementComplete(parameters: MarkSettlementCompleteParameters): Promise<void>;
|
|
110
|
+
/** Atomically claims one due scheduled settlement across server workers. */
|
|
111
|
+
export declare function claimScheduledSettlement(parameters: {
|
|
112
|
+
channelId: Hex;
|
|
113
|
+
leaseMs?: number | undefined;
|
|
114
|
+
schedule: ResolvedSettlementSchedule;
|
|
115
|
+
store: ChannelStore.ChannelStore;
|
|
116
|
+
}): Promise<string | undefined>;
|
|
117
|
+
/** Extends an owned scheduled settlement lease while its transaction is in flight. */
|
|
118
|
+
export declare function renewScheduledSettlement(parameters: {
|
|
119
|
+
channelId: Hex;
|
|
120
|
+
leaseMs?: number | undefined;
|
|
121
|
+
leaseOwner: string;
|
|
122
|
+
store: ChannelStore.ChannelStore;
|
|
123
|
+
}): Promise<void>;
|
|
124
|
+
/** Releases a scheduled settlement claim after a failed attempt. */
|
|
125
|
+
export declare function releaseScheduledSettlement(parameters: {
|
|
126
|
+
channelId: Hex;
|
|
127
|
+
leaseOwner: string;
|
|
128
|
+
store: ChannelStore.ChannelStore;
|
|
129
|
+
}): Promise<void>;
|
|
109
130
|
/** Callback used by post-verification accounting to deduct spend from a channel. */
|
|
110
131
|
export type ChargeSessionChannel = (channelId: Hex, amount: bigint) => Promise<ChannelStore.State>;
|
|
111
132
|
/** Callback used by post-verification accounting to run server-owned settlement policy. */
|
|
@@ -138,6 +159,8 @@ export type ApplyVerifiedHttpAccountingParameters = {
|
|
|
138
159
|
/** Runs optional server settlement policy after a successful content charge. */
|
|
139
160
|
settleCharged: SettleChargedSessionChannel;
|
|
140
161
|
};
|
|
162
|
+
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
163
|
+
export declare function shouldApplyVerifiedHttpAccounting(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction' | 'sseEnabled'>): boolean;
|
|
141
164
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
142
165
|
export declare function applyVerifiedHttpAccounting(parameters: ApplyVerifiedHttpAccountingParameters): Promise<SessionReceipt>;
|
|
143
166
|
/** Atomically deducts spend from a channel and maps store failures to typed session errors. */
|
|
@@ -46,8 +46,9 @@ export function resolveCredentialFeePayer(parameters) {
|
|
|
46
46
|
return requestFeePayer;
|
|
47
47
|
if (typeof feePayer === 'object')
|
|
48
48
|
return feePayer;
|
|
49
|
-
return typeof feePayer === 'string' ? true : undefined;
|
|
49
|
+
return feePayer === true || typeof feePayer === 'string' ? true : undefined;
|
|
50
50
|
}
|
|
51
|
+
const scheduledSettlementLeaseMs = 5 * 60_000;
|
|
51
52
|
/** Converts a public settlement schedule into raw-unit thresholds. */
|
|
52
53
|
export function resolveSettlementSchedule(schedule, decimals) {
|
|
53
54
|
if (!schedule)
|
|
@@ -93,6 +94,9 @@ export function isSettlementDue(channel, schedule) {
|
|
|
93
94
|
const progress = resolveSettlementProgress(channel);
|
|
94
95
|
if (!progress)
|
|
95
96
|
return false;
|
|
97
|
+
// A detected force-close makes outstanding vouchers immediately due.
|
|
98
|
+
if (!channel.finalized && channel.closeRequestedAt > 0n)
|
|
99
|
+
return true;
|
|
96
100
|
if (schedule.units !== undefined && progress.units >= schedule.units)
|
|
97
101
|
return true;
|
|
98
102
|
if (schedule.amount !== undefined && progress.amount >= schedule.amount)
|
|
@@ -103,26 +107,79 @@ export function isSettlementDue(channel, schedule) {
|
|
|
103
107
|
}
|
|
104
108
|
/** Records the channel spend/unit counters that a scheduled settlement captured. */
|
|
105
109
|
export async function markSettlementComplete(parameters) {
|
|
106
|
-
const { channelId, store, settledAt = new Date().toISOString() } = parameters;
|
|
107
|
-
await store.updateChannel(channelId, (current) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
const { channelId, leaseOwner, store, settledAt = new Date().toISOString() } = parameters;
|
|
111
|
+
await store.updateChannel(channelId, (current) => {
|
|
112
|
+
if (!current)
|
|
113
|
+
return current;
|
|
114
|
+
if (current.scheduledSettlementLease?.owner !== leaseOwner)
|
|
115
|
+
return current;
|
|
116
|
+
const { scheduledSettlementLease: _, ...channel } = current;
|
|
117
|
+
return {
|
|
118
|
+
...channel,
|
|
110
119
|
lastSettlementAt: settledAt,
|
|
111
120
|
lastSettlementSpent: current.spent,
|
|
112
121
|
lastSettlementUnits: current.units,
|
|
113
|
-
}
|
|
114
|
-
|
|
122
|
+
};
|
|
123
|
+
});
|
|
115
124
|
}
|
|
116
|
-
/**
|
|
117
|
-
export async function
|
|
118
|
-
const {
|
|
125
|
+
/** Atomically claims one due scheduled settlement across server workers. */
|
|
126
|
+
export async function claimScheduledSettlement(parameters) {
|
|
127
|
+
const { channelId, leaseMs = scheduledSettlementLeaseMs, schedule, store } = parameters;
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const owner = globalThis.crypto.randomUUID();
|
|
130
|
+
let claimed = false;
|
|
131
|
+
await store.updateChannel(channelId, (current) => {
|
|
132
|
+
claimed = false;
|
|
133
|
+
if (!current || !isSettlementDue(current, schedule))
|
|
134
|
+
return current;
|
|
135
|
+
const lease = current.scheduledSettlementLease;
|
|
136
|
+
if (lease && lease.expiresAt > now)
|
|
137
|
+
return current;
|
|
138
|
+
claimed = true;
|
|
139
|
+
return {
|
|
140
|
+
...current,
|
|
141
|
+
scheduledSettlementLease: { expiresAt: now + leaseMs, owner },
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
return claimed ? owner : undefined;
|
|
145
|
+
}
|
|
146
|
+
/** Extends an owned scheduled settlement lease while its transaction is in flight. */
|
|
147
|
+
export async function renewScheduledSettlement(parameters) {
|
|
148
|
+
const { channelId, leaseMs = scheduledSettlementLeaseMs, leaseOwner, store } = parameters;
|
|
149
|
+
await store.updateChannel(channelId, (current) => {
|
|
150
|
+
if (!current || current.scheduledSettlementLease?.owner !== leaseOwner)
|
|
151
|
+
return current;
|
|
152
|
+
return {
|
|
153
|
+
...current,
|
|
154
|
+
scheduledSettlementLease: { expiresAt: Date.now() + leaseMs, owner: leaseOwner },
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Releases a scheduled settlement claim after a failed attempt. */
|
|
159
|
+
export async function releaseScheduledSettlement(parameters) {
|
|
160
|
+
const { channelId, leaseOwner, store } = parameters;
|
|
161
|
+
await store.updateChannel(channelId, (current) => {
|
|
162
|
+
if (!current || current.scheduledSettlementLease?.owner !== leaseOwner)
|
|
163
|
+
return current;
|
|
164
|
+
const { scheduledSettlementLease: _, ...channel } = current;
|
|
165
|
+
return channel;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
169
|
+
export function shouldApplyVerifiedHttpAccounting(parameters) {
|
|
170
|
+
const { capturedRequest, payloadAction, sseEnabled } = parameters;
|
|
119
171
|
if (!capturedRequest)
|
|
120
|
-
return
|
|
172
|
+
return false;
|
|
121
173
|
if (payloadAction !== 'open' && payloadAction !== 'voucher')
|
|
122
|
-
return
|
|
174
|
+
return false;
|
|
123
175
|
if (sseEnabled && capturedRequest.method === 'POST')
|
|
124
|
-
return
|
|
125
|
-
|
|
176
|
+
return false;
|
|
177
|
+
return isSessionContentRequest(capturedRequest);
|
|
178
|
+
}
|
|
179
|
+
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
180
|
+
export async function applyVerifiedHttpAccounting(parameters) {
|
|
181
|
+
const { receipt, sseEnabled } = parameters;
|
|
182
|
+
if (!shouldApplyVerifiedHttpAccounting(parameters))
|
|
126
183
|
return receipt;
|
|
127
184
|
const requestAmount = parameters.getRequestAmount();
|
|
128
185
|
const charged = await parameters.charge(receipt.channelId, requestAmount);
|
|
@@ -185,19 +242,42 @@ export function assertSettlementSender(parameters) {
|
|
|
185
242
|
/** Applies automatic settlement when the server-owned schedule is due. */
|
|
186
243
|
export async function maybeSettleScheduled(parameters) {
|
|
187
244
|
const { channel, schedule, store } = parameters;
|
|
188
|
-
if (!isSettlementDue(channel, schedule))
|
|
245
|
+
if (!schedule || !isSettlementDue(channel, schedule))
|
|
189
246
|
return undefined;
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
195
|
-
onSessionSettlement: parameters.onSessionSettlement
|
|
196
|
-
? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
|
|
197
|
-
: undefined,
|
|
247
|
+
const leaseOwner = await claimScheduledSettlement({
|
|
248
|
+
channelId: channel.channelId,
|
|
249
|
+
schedule,
|
|
250
|
+
store,
|
|
198
251
|
});
|
|
199
|
-
|
|
200
|
-
|
|
252
|
+
if (!leaseOwner)
|
|
253
|
+
return undefined;
|
|
254
|
+
const renewal = setInterval(() => {
|
|
255
|
+
void renewScheduledSettlement({
|
|
256
|
+
channelId: channel.channelId,
|
|
257
|
+
leaseOwner,
|
|
258
|
+
store,
|
|
259
|
+
}).catch(() => undefined);
|
|
260
|
+
}, scheduledSettlementLeaseMs / 2);
|
|
261
|
+
try {
|
|
262
|
+
const txHash = await settle(store, parameters.client, channel.channelId, {
|
|
263
|
+
account: parameters.account,
|
|
264
|
+
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
265
|
+
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
266
|
+
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
267
|
+
onSessionSettlement: parameters.onSessionSettlement
|
|
268
|
+
? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
|
|
269
|
+
: undefined,
|
|
270
|
+
});
|
|
271
|
+
await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store });
|
|
272
|
+
return txHash;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
await releaseScheduledSettlement({ channelId: channel.channelId, leaseOwner, store }).catch(() => undefined);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
clearInterval(renewal);
|
|
280
|
+
}
|
|
201
281
|
}
|
|
202
282
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
203
283
|
export async function settle(store_, client, channelId_, options) {
|