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.
- package/CHANGELOG.md +55 -0
- 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/cli.d.ts +8 -0
- 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 +34 -12
- 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/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 +13 -1
- package/dist/server/Mppx.js +85 -44
- 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/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +17 -2
- 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/fee-payer.d.ts +6 -3
- package/dist/tempo/internal/fee-payer.js +23 -6
- 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.js +75 -46
- package/dist/tempo/server/Methods.d.ts +55 -10
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- 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 +20 -2
- package/dist/tempo/session/server/ChannelStore.js +25 -4
- package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
- package/dist/tempo/session/server/CredentialVerification.js +34 -6
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +2 -1
- package/dist/tempo/session/server/Settlement.d.ts +21 -0
- package/dist/tempo/session/server/Settlement.js +92 -17
- 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/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 +6 -6
- 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
|
@@ -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);
|
|
@@ -611,17 +619,34 @@ async function handleVoucherCredential(parameters) {
|
|
|
611
619
|
channelStateTtl,
|
|
612
620
|
client,
|
|
613
621
|
escrow,
|
|
622
|
+
forceRefresh: true,
|
|
614
623
|
lastOnChainVerified,
|
|
615
624
|
});
|
|
616
625
|
if (channelState.closeRequestedAt !== 0) {
|
|
617
|
-
await store.updateChannel(channelId, (current) => current
|
|
626
|
+
const closing = await store.updateChannel(channelId, (current) => current
|
|
618
627
|
? {
|
|
619
628
|
...current,
|
|
629
|
+
settledOnChain: channelState.settled > current.settledOnChain
|
|
630
|
+
? channelState.settled
|
|
631
|
+
: current.settledOnChain,
|
|
620
632
|
closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
|
|
621
633
|
? BigInt(channelState.closeRequestedAt)
|
|
622
634
|
: current.closeRequestedAt,
|
|
623
635
|
}
|
|
624
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' });
|
|
625
650
|
}
|
|
626
651
|
return ChannelStore.verifyAndAcceptVoucher({
|
|
627
652
|
store,
|
|
@@ -649,6 +674,9 @@ async function handleCloseCredential(parameters) {
|
|
|
649
674
|
escrow,
|
|
650
675
|
store,
|
|
651
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' });
|
|
652
680
|
if (channel.finalized)
|
|
653
681
|
throw new ChannelClosedError({ reason: 'channel is already finalized' });
|
|
654
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
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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. */
|
|
@@ -48,6 +48,7 @@ export function resolveCredentialFeePayer(parameters) {
|
|
|
48
48
|
return feePayer;
|
|
49
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,15 +107,63 @@ 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
|
+
});
|
|
124
|
+
}
|
|
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
|
+
});
|
|
115
167
|
}
|
|
116
168
|
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
117
169
|
export function shouldApplyVerifiedHttpAccounting(parameters) {
|
|
@@ -190,19 +242,42 @@ export function assertSettlementSender(parameters) {
|
|
|
190
242
|
/** Applies automatic settlement when the server-owned schedule is due. */
|
|
191
243
|
export async function maybeSettleScheduled(parameters) {
|
|
192
244
|
const { channel, schedule, store } = parameters;
|
|
193
|
-
if (!isSettlementDue(channel, schedule))
|
|
245
|
+
if (!schedule || !isSettlementDue(channel, schedule))
|
|
194
246
|
return undefined;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
200
|
-
onSessionSettlement: parameters.onSessionSettlement
|
|
201
|
-
? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
|
|
202
|
-
: undefined,
|
|
247
|
+
const leaseOwner = await claimScheduledSettlement({
|
|
248
|
+
channelId: channel.channelId,
|
|
249
|
+
schedule,
|
|
250
|
+
store,
|
|
203
251
|
});
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
}
|
|
206
281
|
}
|
|
207
282
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
208
283
|
export async function settle(store_, client, channelId_, options) {
|
|
@@ -27,7 +27,9 @@ 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 the reserved charge immediately before the chunk is emitted.
|
|
30
|
+
* 4. Commits the reserved charge immediately before the chunk is emitted. If a
|
|
31
|
+
* manual generator completes after charging without yielding another chunk,
|
|
32
|
+
* commits that terminal charge on successful completion.
|
|
31
33
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
32
34
|
*
|
|
33
35
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -26,7 +26,9 @@ 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 the reserved charge immediately before the chunk is emitted.
|
|
29
|
+
* 4. Commits the reserved charge immediately before the chunk is emitted. If a
|
|
30
|
+
* manual generator completes after charging without yielding another chunk,
|
|
31
|
+
* commits that terminal charge on successful completion.
|
|
30
32
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
31
33
|
*
|
|
32
34
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -34,28 +36,59 @@ export { extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptE
|
|
|
34
36
|
export function serve(options) {
|
|
35
37
|
const { store, channelId, challengeId, tickCost, generate, pollIntervalMs = 100, signal, } = options;
|
|
36
38
|
const encoder = new TextEncoder();
|
|
39
|
+
const abortController = new AbortController();
|
|
40
|
+
let streamController;
|
|
41
|
+
let terminal = false;
|
|
42
|
+
const iterator = meterIterable({
|
|
43
|
+
store,
|
|
44
|
+
channelId,
|
|
45
|
+
tickCost,
|
|
46
|
+
generate,
|
|
47
|
+
onChargeCommitted: options.onChargeCommitted,
|
|
48
|
+
pollIntervalMs,
|
|
49
|
+
prepaidUnits: options.prepaidUnits,
|
|
50
|
+
signal: abortController.signal,
|
|
51
|
+
emitNeedVoucher: (event) => streamController?.enqueue(encoder.encode(event)),
|
|
52
|
+
formatNeedVoucher: formatNeedVoucherEvent,
|
|
53
|
+
})[Symbol.asyncIterator]();
|
|
54
|
+
const cleanup = () => signal?.removeEventListener('abort', abort);
|
|
55
|
+
const terminate = (reason) => {
|
|
56
|
+
if (terminal)
|
|
57
|
+
return;
|
|
58
|
+
terminal = true;
|
|
59
|
+
abortController.abort(reason);
|
|
60
|
+
cleanup();
|
|
61
|
+
void Promise.resolve()
|
|
62
|
+
.then(() => iterator.return?.(undefined))
|
|
63
|
+
.catch(() => { });
|
|
64
|
+
try {
|
|
65
|
+
streamController?.close();
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
};
|
|
69
|
+
const abort = () => terminate(signal?.reason);
|
|
70
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
37
71
|
return new ReadableStream({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
start(controller) {
|
|
73
|
+
streamController = controller;
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
abort();
|
|
76
|
+
},
|
|
77
|
+
async pull(controller) {
|
|
78
|
+
if (terminal)
|
|
79
|
+
return;
|
|
41
80
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
prepaidUnits: options.prepaidUnits,
|
|
50
|
-
signal,
|
|
51
|
-
emitNeedVoucher: emit,
|
|
52
|
-
formatNeedVoucher: formatNeedVoucherEvent,
|
|
53
|
-
})) {
|
|
54
|
-
if (aborted())
|
|
55
|
-
break;
|
|
81
|
+
const next = await iterator.next();
|
|
82
|
+
if (terminal)
|
|
83
|
+
return;
|
|
84
|
+
if (!next.done) {
|
|
85
|
+
if (abortController.signal.aborted)
|
|
86
|
+
return;
|
|
87
|
+
const value = next.value;
|
|
56
88
|
controller.enqueue(encoder.encode(formatMessageEvent(value)));
|
|
89
|
+
return;
|
|
57
90
|
}
|
|
58
|
-
if (!aborted
|
|
91
|
+
if (!abortController.signal.aborted) {
|
|
59
92
|
const channel = await store.getChannel(channelId);
|
|
60
93
|
if (channel) {
|
|
61
94
|
const receipt = createSessionReceipt({
|
|
@@ -66,16 +99,29 @@ export function serve(options) {
|
|
|
66
99
|
units: channel.units,
|
|
67
100
|
});
|
|
68
101
|
controller.enqueue(encoder.encode(formatReceiptEvent(receipt)));
|
|
102
|
+
terminal = true;
|
|
103
|
+
cleanup();
|
|
104
|
+
controller.close();
|
|
105
|
+
return;
|
|
69
106
|
}
|
|
70
107
|
}
|
|
108
|
+
terminal = true;
|
|
109
|
+
cleanup();
|
|
110
|
+
controller.close();
|
|
71
111
|
}
|
|
72
112
|
catch (e) {
|
|
73
|
-
if (
|
|
113
|
+
if (terminal)
|
|
114
|
+
return;
|
|
115
|
+
terminal = true;
|
|
116
|
+
cleanup();
|
|
117
|
+
if (!abortController.signal.aborted)
|
|
74
118
|
controller.error(e);
|
|
119
|
+
else
|
|
120
|
+
controller.close();
|
|
75
121
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
122
|
+
},
|
|
123
|
+
cancel(reason) {
|
|
124
|
+
terminate(reason);
|
|
79
125
|
},
|
|
80
126
|
});
|
|
81
127
|
}
|
|
@@ -154,26 +200,30 @@ export async function* iterateData(response, options) {
|
|
|
154
200
|
while (true) {
|
|
155
201
|
const { value, done } = await reader.read();
|
|
156
202
|
if (done)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
buffer += decoder.decode();
|
|
204
|
+
else
|
|
205
|
+
buffer += decoder.decode(value, { stream: true });
|
|
206
|
+
while (true) {
|
|
207
|
+
const separator = findSseSeparator(buffer, done);
|
|
208
|
+
if (!separator)
|
|
209
|
+
break;
|
|
210
|
+
const event = buffer.slice(0, separator.index);
|
|
211
|
+
buffer = buffer.slice(separator.index + separator.length);
|
|
164
212
|
if (!event.trim())
|
|
165
213
|
continue;
|
|
166
|
-
const data = extractData(event);
|
|
214
|
+
const data = extractData(event.replace(/\r\n|\r/g, '\n'));
|
|
167
215
|
if (data === null)
|
|
168
216
|
continue;
|
|
169
217
|
if (skip?.(data))
|
|
170
218
|
continue;
|
|
171
219
|
yield data;
|
|
172
220
|
}
|
|
221
|
+
if (done)
|
|
222
|
+
break;
|
|
173
223
|
}
|
|
174
224
|
// Flush remaining buffer.
|
|
175
225
|
if (buffer.trim()) {
|
|
176
|
-
const data = extractData(buffer);
|
|
226
|
+
const data = extractData(buffer.replace(/\r\n|\r/g, '\n'));
|
|
177
227
|
if (data !== null && !skip?.(data))
|
|
178
228
|
yield data;
|
|
179
229
|
}
|
|
@@ -182,4 +232,27 @@ export async function* iterateData(response, options) {
|
|
|
182
232
|
reader.releaseLock();
|
|
183
233
|
}
|
|
184
234
|
}
|
|
235
|
+
function findSseSeparator(value, complete) {
|
|
236
|
+
for (let index = 0; index < value.length; index++) {
|
|
237
|
+
const first = lineEndingLength(value, index, complete);
|
|
238
|
+
if (!first)
|
|
239
|
+
continue;
|
|
240
|
+
const second = lineEndingLength(value, index + first, complete);
|
|
241
|
+
if (second)
|
|
242
|
+
return { index, length: first + second };
|
|
243
|
+
index += first - 1;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function lineEndingLength(value, index, complete) {
|
|
248
|
+
if (value[index] === '\n')
|
|
249
|
+
return 1;
|
|
250
|
+
if (value[index] !== '\r')
|
|
251
|
+
return 0;
|
|
252
|
+
if (value[index + 1] === '\n')
|
|
253
|
+
return 2;
|
|
254
|
+
if (index + 1 === value.length && !complete)
|
|
255
|
+
return 0;
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
185
258
|
//# sourceMappingURL=Sse.js.map
|