mppx 0.12.0 → 0.13.2
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 +64 -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/sessions/request.d.ts +1 -1
- package/dist/cli/sessions/request.js +6 -8
- 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 +22 -7
- 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 +14 -2
- 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 +87 -53
- 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
|
@@ -13,34 +13,31 @@ export type ReserveChargeParameters = {
|
|
|
13
13
|
formatNeedVoucher(parameters: NeedVoucherEvent): string;
|
|
14
14
|
/** Store polling interval when `waitForUpdate` is unavailable. */
|
|
15
15
|
pollIntervalMs: number;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/** Identifier owned by this stream across reserve, commit, and release. */
|
|
17
|
+
reservationId: string;
|
|
18
|
+
/** Requires a previous uncommitted charge to retain its lease. */
|
|
19
|
+
requireExisting?: boolean | undefined;
|
|
18
20
|
/** Optional abort signal for long waits. */
|
|
19
21
|
signal?: AbortSignal | undefined;
|
|
20
22
|
/** Channel store used for state reads and waits. */
|
|
21
23
|
store: ChannelStore.ChannelStore;
|
|
22
24
|
};
|
|
23
|
-
/**
|
|
25
|
+
/** Inputs for updating the reservation owned by one stream. */
|
|
24
26
|
export type CommitReservedChargesParameters = {
|
|
25
|
-
/** Reserved amount to commit. */
|
|
26
|
-
amount: bigint;
|
|
27
|
-
/** Channel being metered. */
|
|
28
27
|
channelId: Hex;
|
|
29
|
-
|
|
28
|
+
reservationId: string;
|
|
30
29
|
store: ChannelStore.ChannelStore;
|
|
31
|
-
/** Number of charge units to add. */
|
|
32
|
-
units: number;
|
|
33
30
|
};
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* If the channel lacks headroom, emits one need-voucher frame, then waits for
|
|
38
|
-
* a store update or polling interval until the accepted voucher covers both
|
|
39
|
-
* already-reserved charges and the next requested amount.
|
|
40
|
-
*/
|
|
31
|
+
/** Lease duration for abandoned stream reservations. Active generators renew it. */
|
|
32
|
+
export declare const streamReservationLeaseMs = 30000;
|
|
33
|
+
/** Atomically reserves shared voucher coverage before billable stream work begins. */
|
|
41
34
|
export declare function reserveChargeOrWait(options: ReserveChargeParameters): Promise<void>;
|
|
42
|
-
/**
|
|
35
|
+
/** Commits only this stream's live reservation, removing it in the same atomic update. */
|
|
43
36
|
export declare function commitReservedCharges(options: CommitReservedChargesParameters): Promise<ChannelStore.State | undefined>;
|
|
37
|
+
/** Releases only the unused reservation owned by this stream. */
|
|
38
|
+
export declare function releaseReservedCharges({ store, channelId, reservationId, }: CommitReservedChargesParameters): Promise<void>;
|
|
39
|
+
/** Renews a live reservation without reviving an expired or released lease. */
|
|
40
|
+
export declare function renewReservedCharges({ store, channelId, reservationId, }: CommitReservedChargesParameters): Promise<void>;
|
|
44
41
|
/** Throws when a channel can no longer be used for streaming charges. */
|
|
45
42
|
export declare function throwIfChannelClosed(channel: ChannelStore.State): void;
|
|
46
43
|
/** Minimal socket event map supported by browser and Node-style WebSocket runtimes. */
|
|
@@ -64,6 +61,8 @@ export type SocketEventListener<type extends keyof SocketEventMap> = ((event: So
|
|
|
64
61
|
};
|
|
65
62
|
/** Minimal socket shape required by the session WebSocket adapter. */
|
|
66
63
|
export type Socket = {
|
|
64
|
+
/** Bytes queued by the runtime but not yet written to the network. */
|
|
65
|
+
readonly bufferedAmount?: number | undefined;
|
|
67
66
|
close(code?: number, reason?: string): unknown;
|
|
68
67
|
send(data: string): unknown;
|
|
69
68
|
addEventListener?: <type extends keyof SocketEventMap>(type: type, listener: SocketEventListener<type>) => unknown;
|
|
@@ -71,6 +70,10 @@ export type Socket = {
|
|
|
71
70
|
on?: <type extends keyof SocketEventMap>(type: type, listener: (event: SocketEventMap[type]) => void) => unknown;
|
|
72
71
|
off?: <type extends keyof SocketEventMap>(type: type, listener: (event: SocketEventMap[type]) => void) => unknown;
|
|
73
72
|
};
|
|
73
|
+
/** Raised after a socket is closed to keep its outbound queue bounded. */
|
|
74
|
+
export declare class WebSocketOutboundBufferLimitError extends Error {
|
|
75
|
+
readonly name = "WebSocketOutboundBufferLimitError";
|
|
76
|
+
}
|
|
74
77
|
/** Handlers for socket lifecycle and message events. */
|
|
75
78
|
export type SocketHandlers = {
|
|
76
79
|
/** Called when the socket closes. */
|
|
@@ -82,8 +85,10 @@ export type SocketHandlers = {
|
|
|
82
85
|
};
|
|
83
86
|
/** Subscribes to browser or Node-style socket events and returns an unsubscribe callback. */
|
|
84
87
|
export declare function subscribe(socket: Socket, handlers: SocketHandlers): () => void;
|
|
85
|
-
/** Sends a text frame
|
|
86
|
-
export declare function send(socket: Socket, data: string
|
|
88
|
+
/** Sends a text frame while bounding runtime-managed outbound buffering. */
|
|
89
|
+
export declare function send(socket: Socket, data: string, options?: {
|
|
90
|
+
maxBufferedAmount?: number;
|
|
91
|
+
}): Promise<void>;
|
|
87
92
|
/** Converts socket message payloads into text frames when possible. */
|
|
88
93
|
export declare function toText(value: unknown): string | null;
|
|
89
94
|
//# sourceMappingURL=Transports.d.ts.map
|
|
@@ -1,62 +1,133 @@
|
|
|
1
1
|
import { ChannelClosedError } from '../../../Errors.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* a store update or polling interval until the accepted voucher covers both
|
|
7
|
-
* already-reserved charges and the next requested amount.
|
|
8
|
-
*/
|
|
2
|
+
import * as ChannelStore from './ChannelStore.js';
|
|
3
|
+
/** Lease duration for abandoned stream reservations. Active generators renew it. */
|
|
4
|
+
export const streamReservationLeaseMs = 30_000;
|
|
5
|
+
/** Atomically reserves shared voucher coverage before billable stream work begins. */
|
|
9
6
|
export async function reserveChargeOrWait(options) {
|
|
10
|
-
const { amount, channelId, emit, formatNeedVoucher, pollIntervalMs,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
channelId,
|
|
20
|
-
requiredCumulative: (channel.spent + reservedAmount + amount).toString(),
|
|
21
|
-
acceptedCumulative: channel.highestVoucherAmount.toString(),
|
|
22
|
-
deposit: channel.deposit.toString(),
|
|
23
|
-
})));
|
|
24
|
-
while (!hasHeadroom(channel)) {
|
|
25
|
-
await waitForUpdate(store, channelId, pollIntervalMs, signal);
|
|
26
|
-
channel = await store.getChannel(channelId);
|
|
7
|
+
const { amount, channelId, emit, formatNeedVoucher, pollIntervalMs, reservationId, signal, store, } = options;
|
|
8
|
+
if (amount < 0n)
|
|
9
|
+
throw new Error('stream charge must be non-negative');
|
|
10
|
+
if (store.atomic === false)
|
|
11
|
+
throw new Error('stream reservations require an atomic store');
|
|
12
|
+
let lastRequired;
|
|
13
|
+
while (true) {
|
|
14
|
+
throwIfAborted(signal);
|
|
15
|
+
let channel = await store.getChannel(channelId);
|
|
27
16
|
if (!channel)
|
|
28
17
|
throw new Error('channel not found');
|
|
29
18
|
throwIfChannelClosed(channel);
|
|
19
|
+
if (options.requireExisting &&
|
|
20
|
+
(channel.streamReservations?.[reservationId]?.expiresAt ?? 0) <= Date.now())
|
|
21
|
+
throw new Error('stream reservation was lost');
|
|
22
|
+
if (channel.highestVoucherAmount - channel.spent - ChannelStore.reservedStreamAmount(channel) >=
|
|
23
|
+
amount) {
|
|
24
|
+
let reserved = false;
|
|
25
|
+
channel = await store.updateChannel(channelId, (current) => {
|
|
26
|
+
reserved = false;
|
|
27
|
+
if (!current)
|
|
28
|
+
return current;
|
|
29
|
+
throwIfChannelClosed(current);
|
|
30
|
+
throwIfAborted(signal);
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const previous = current.streamReservations?.[reservationId];
|
|
33
|
+
if (options.requireExisting && !previous)
|
|
34
|
+
throw new Error('stream reservation was lost');
|
|
35
|
+
if (previous && previous.expiresAt <= now)
|
|
36
|
+
throw new Error('stream reservation expired');
|
|
37
|
+
if (current.highestVoucherAmount -
|
|
38
|
+
current.spent -
|
|
39
|
+
ChannelStore.reservedStreamAmount(current) <
|
|
40
|
+
amount)
|
|
41
|
+
return current;
|
|
42
|
+
const reservations = Object.fromEntries(Object.entries(current.streamReservations ?? {}).filter(([, item]) => item.expiresAt > now));
|
|
43
|
+
reserved = true;
|
|
44
|
+
return {
|
|
45
|
+
...current,
|
|
46
|
+
streamReservations: {
|
|
47
|
+
...reservations,
|
|
48
|
+
[reservationId]: {
|
|
49
|
+
amount: (previous?.amount ?? 0n) + amount,
|
|
50
|
+
units: (previous?.units ?? 0) + 1,
|
|
51
|
+
expiresAt: now + streamReservationLeaseMs,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
if (reserved)
|
|
57
|
+
return;
|
|
58
|
+
if (!channel)
|
|
59
|
+
throw new Error('channel not found');
|
|
60
|
+
}
|
|
61
|
+
const required = channel.spent + ChannelStore.reservedStreamAmount(channel) + amount;
|
|
62
|
+
if (required !== lastRequired) {
|
|
63
|
+
lastRequired = required;
|
|
64
|
+
await emit(formatNeedVoucher({
|
|
65
|
+
channelId,
|
|
66
|
+
requiredCumulative: required.toString(),
|
|
67
|
+
acceptedCumulative: channel.highestVoucherAmount.toString(),
|
|
68
|
+
deposit: channel.deposit.toString(),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
await waitForUpdate(store, channelId, pollIntervalMs, signal);
|
|
30
72
|
}
|
|
31
73
|
}
|
|
32
|
-
/**
|
|
74
|
+
/** Commits only this stream's live reservation, removing it in the same atomic update. */
|
|
33
75
|
export async function commitReservedCharges(options) {
|
|
34
|
-
const {
|
|
35
|
-
if (amount === 0n || units === 0)
|
|
36
|
-
return;
|
|
76
|
+
const { channelId, reservationId, store } = options;
|
|
37
77
|
let committed = false;
|
|
38
78
|
const channel = await store.updateChannel(channelId, (current) => {
|
|
79
|
+
committed = false;
|
|
39
80
|
if (!current)
|
|
40
|
-
return null;
|
|
41
|
-
if (current.finalized)
|
|
42
|
-
return current;
|
|
43
|
-
if (current.closeRequestedAt !== 0n)
|
|
44
81
|
return current;
|
|
45
|
-
|
|
82
|
+
throwIfChannelClosed(current);
|
|
83
|
+
const reservation = current.streamReservations?.[reservationId];
|
|
84
|
+
if (!reservation)
|
|
46
85
|
return current;
|
|
86
|
+
if (reservation.expiresAt <= Date.now())
|
|
87
|
+
throw new Error('stream reservation expired');
|
|
88
|
+
if (current.highestVoucherAmount - current.spent < reservation.amount)
|
|
89
|
+
throw new Error('reserved voucher coverage is no longer available');
|
|
90
|
+
const { [reservationId]: _, ...remaining } = current.streamReservations;
|
|
47
91
|
committed = true;
|
|
48
92
|
return {
|
|
49
93
|
...current,
|
|
50
|
-
|
|
51
|
-
|
|
94
|
+
streamReservations: remaining,
|
|
95
|
+
spent: current.spent + reservation.amount,
|
|
96
|
+
units: current.units + reservation.units,
|
|
52
97
|
};
|
|
53
98
|
});
|
|
54
99
|
if (!channel)
|
|
55
100
|
throw new Error('channel not found');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
return committed ? channel : undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Releases only the unused reservation owned by this stream. */
|
|
104
|
+
export async function releaseReservedCharges({ store, channelId, reservationId, }) {
|
|
105
|
+
await store.updateChannel(channelId, (current) => {
|
|
106
|
+
if (!current?.streamReservations?.[reservationId])
|
|
107
|
+
return current;
|
|
108
|
+
const { [reservationId]: _, ...remaining } = current.streamReservations;
|
|
109
|
+
return { ...current, streamReservations: remaining };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Renews a live reservation without reviving an expired or released lease. */
|
|
113
|
+
export async function renewReservedCharges({ store, channelId, reservationId, }) {
|
|
114
|
+
await store.updateChannel(channelId, (current) => {
|
|
115
|
+
if (!current)
|
|
116
|
+
throw new Error('channel not found');
|
|
117
|
+
throwIfChannelClosed(current);
|
|
118
|
+
const reservation = current.streamReservations?.[reservationId];
|
|
119
|
+
if (!reservation)
|
|
120
|
+
return current;
|
|
121
|
+
if (reservation.expiresAt <= Date.now())
|
|
122
|
+
throw new Error('stream reservation expired');
|
|
123
|
+
return {
|
|
124
|
+
...current,
|
|
125
|
+
streamReservations: {
|
|
126
|
+
...current.streamReservations,
|
|
127
|
+
[reservationId]: { ...reservation, expiresAt: Date.now() + streamReservationLeaseMs },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
});
|
|
60
131
|
}
|
|
61
132
|
/** Throws when a channel can no longer be used for streaming charges. */
|
|
62
133
|
export function throwIfChannelClosed(channel) {
|
|
@@ -68,11 +139,19 @@ export function throwIfChannelClosed(channel) {
|
|
|
68
139
|
async function waitForUpdate(store, channelId, pollIntervalMs, signal) {
|
|
69
140
|
throwIfAborted(signal);
|
|
70
141
|
if (store.waitForUpdate) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
144
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
145
|
+
try {
|
|
146
|
+
await Promise.race([
|
|
147
|
+
store.waitForUpdate(channelId, controller.signal),
|
|
148
|
+
sleep(pollIntervalMs, controller.signal),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
signal?.removeEventListener('abort', onAbort);
|
|
153
|
+
controller.abort();
|
|
154
|
+
}
|
|
76
155
|
}
|
|
77
156
|
else {
|
|
78
157
|
await sleep(pollIntervalMs, signal);
|
|
@@ -92,21 +171,14 @@ function sleep(ms, signal) {
|
|
|
92
171
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
93
172
|
});
|
|
94
173
|
}
|
|
95
|
-
function onceAborted(signal) {
|
|
96
|
-
return new Promise((_, reject) => {
|
|
97
|
-
if (signal.aborted) {
|
|
98
|
-
reject(signal.reason ?? new Error('aborted'));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
signal.addEventListener('abort', () => reject(signal.reason ?? new Error('aborted')), {
|
|
102
|
-
once: true,
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
174
|
function throwIfAborted(signal) {
|
|
107
175
|
if (signal?.aborted)
|
|
108
176
|
throw signal.reason ?? new Error('aborted');
|
|
109
177
|
}
|
|
178
|
+
/** Raised after a socket is closed to keep its outbound queue bounded. */
|
|
179
|
+
export class WebSocketOutboundBufferLimitError extends Error {
|
|
180
|
+
name = 'WebSocketOutboundBufferLimitError';
|
|
181
|
+
}
|
|
110
182
|
/** Subscribes to browser or Node-style socket events and returns an unsubscribe callback. */
|
|
111
183
|
export function subscribe(socket, handlers) {
|
|
112
184
|
if (socket.addEventListener && socket.removeEventListener) {
|
|
@@ -133,9 +205,48 @@ export function subscribe(socket, handlers) {
|
|
|
133
205
|
}
|
|
134
206
|
throw new Error('unsupported websocket implementation');
|
|
135
207
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
208
|
+
const socketSendQueues = new WeakMap();
|
|
209
|
+
/** Sends a text frame while bounding runtime-managed outbound buffering. */
|
|
210
|
+
export async function send(socket, data, options = {}) {
|
|
211
|
+
const previous = socketSendQueues.get(socket) ?? Promise.resolve();
|
|
212
|
+
const current = previous
|
|
213
|
+
.catch(() => { })
|
|
214
|
+
.then(async () => {
|
|
215
|
+
const maxBufferedAmount = options.maxBufferedAmount ?? 1024 * 1024;
|
|
216
|
+
if ((socket.bufferedAmount ?? 0) + textByteLength(data) > maxBufferedAmount) {
|
|
217
|
+
try {
|
|
218
|
+
await Promise.resolve(socket.close(4008, 'outbound buffer limit exceeded'));
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
throw new WebSocketOutboundBufferLimitError('websocket outbound buffer limit exceeded');
|
|
222
|
+
}
|
|
223
|
+
await Promise.resolve(socket.send(data));
|
|
224
|
+
});
|
|
225
|
+
socketSendQueues.set(socket, current);
|
|
226
|
+
try {
|
|
227
|
+
await current;
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (socketSendQueues.get(socket) === current)
|
|
231
|
+
socketSendQueues.delete(socket);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function textByteLength(value) {
|
|
235
|
+
let bytes = 0;
|
|
236
|
+
for (let index = 0; index < value.length; index++) {
|
|
237
|
+
const codePoint = value.codePointAt(index);
|
|
238
|
+
if (codePoint <= 0x7f)
|
|
239
|
+
bytes += 1;
|
|
240
|
+
else if (codePoint <= 0x7ff)
|
|
241
|
+
bytes += 2;
|
|
242
|
+
else if (codePoint <= 0xffff)
|
|
243
|
+
bytes += 3;
|
|
244
|
+
else {
|
|
245
|
+
bytes += 4;
|
|
246
|
+
index += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return bytes;
|
|
139
250
|
}
|
|
140
251
|
/** Converts socket message payloads into text frames when possible. */
|
|
141
252
|
export function toText(value) {
|
|
@@ -12,7 +12,7 @@ export type SessionRouteResult = {
|
|
|
12
12
|
challenge: Response;
|
|
13
13
|
} | {
|
|
14
14
|
status: 200;
|
|
15
|
-
withReceipt(response?: Response): Response
|
|
15
|
+
withReceipt(response?: Response): Response | Promise<Response>;
|
|
16
16
|
};
|
|
17
17
|
/** HTTP route used to perform a WebSocket payment probe. */
|
|
18
18
|
export type SessionRoute = (request: Request) => Promise<SessionRouteResult>;
|
|
@@ -39,6 +39,10 @@ export declare namespace serve {
|
|
|
39
39
|
/** Application stream. A manual stream can call `charge(amount)` with a
|
|
40
40
|
* per-message raw-unit amount; omitting it uses the challenge tick cost. */
|
|
41
41
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
|
|
42
|
+
/** Maximum accepted inbound WebSocket frame size in bytes. @default 65536 */
|
|
43
|
+
maxIncomingMessageBytes?: number | undefined;
|
|
44
|
+
/** Maximum queued outbound bytes before the socket is closed. @default 1048576 */
|
|
45
|
+
maxBufferedAmount?: number | undefined;
|
|
42
46
|
pollIntervalMs?: number | undefined;
|
|
43
47
|
/** Payment route handler. Receives synthetic `POST` requests with only
|
|
44
48
|
* the `Authorization` header — no cookies, bodies, or upgrade headers. */
|
|
@@ -3,7 +3,7 @@ import * as Credential from '../../../Credential.js';
|
|
|
3
3
|
import { createSessionReceipt, deserializeSessionReceipt, formatApplicationMessage, formatCloseReadyMessage, formatErrorMessage, formatNeedVoucherMessage, formatReceiptMessage, parseMessage, readSessionChallengeAmount, requireSessionCredentialContext, } from '../precompile/Protocol.js';
|
|
4
4
|
import * as ChannelStore from './ChannelStore.js';
|
|
5
5
|
import { meterIterable } from './MeteredStream.js';
|
|
6
|
-
import { send, subscribe, toText } from './Transports.js';
|
|
6
|
+
import { send, subscribe, toText, WebSocketOutboundBufferLimitError, } from './Transports.js';
|
|
7
7
|
/** Public WebSocket payment frame helpers. */
|
|
8
8
|
export { formatApplicationMessage, formatAuthorizationMessage, formatCloseReadyMessage, formatCloseRequestMessage, formatErrorMessage, formatNeedVoucherMessage, formatReceiptMessage, parseMessage, } from '../precompile/Protocol.js';
|
|
9
9
|
/**
|
|
@@ -17,10 +17,11 @@ export { formatApplicationMessage, formatAuthorizationMessage, formatCloseReadyM
|
|
|
17
17
|
* HTTP-specific context beyond the `Authorization` header.
|
|
18
18
|
*/
|
|
19
19
|
export async function serve(options) {
|
|
20
|
-
const { amount: expectedAmount, generate, pollIntervalMs = 100, route, socket, store: rawStore, url, } = options;
|
|
20
|
+
const { amount: expectedAmount, generate, maxIncomingMessageBytes = 64 * 1024, maxBufferedAmount = 1024 * 1024, pollIntervalMs = 100, route, socket, store: rawStore, url, } = options;
|
|
21
21
|
const store = 'getChannel' in rawStore ? rawStore : ChannelStore.fromStore(rawStore);
|
|
22
22
|
const requestUrl = normalizeHttpUrl(url);
|
|
23
23
|
const maxQueuedPaymentMessages = 32;
|
|
24
|
+
const sendFrame = (data) => send(socket, data, { maxBufferedAmount });
|
|
24
25
|
const abortController = new AbortController();
|
|
25
26
|
const runtime = {
|
|
26
27
|
closed: false,
|
|
@@ -55,7 +56,7 @@ export async function serve(options) {
|
|
|
55
56
|
spent: channel.spent,
|
|
56
57
|
units: channel.units,
|
|
57
58
|
});
|
|
58
|
-
await
|
|
59
|
+
await sendFrame(formatCloseReadyMessage(receipt));
|
|
59
60
|
};
|
|
60
61
|
const runStream = async (context) => {
|
|
61
62
|
try {
|
|
@@ -67,23 +68,28 @@ export async function serve(options) {
|
|
|
67
68
|
onChargeCommitted: options.onChargeCommitted ?? options.settleScheduled,
|
|
68
69
|
pollIntervalMs,
|
|
69
70
|
signal: abortController.signal,
|
|
70
|
-
emitNeedVoucher: (message) =>
|
|
71
|
+
emitNeedVoucher: (message) => sendFrame(message),
|
|
71
72
|
formatNeedVoucher: formatNeedVoucherMessage,
|
|
72
73
|
})) {
|
|
73
74
|
if (abortController.signal.aborted)
|
|
74
75
|
break;
|
|
75
|
-
await
|
|
76
|
+
await sendFrame(formatApplicationMessage(value));
|
|
76
77
|
}
|
|
77
78
|
if (!abortController.signal.aborted)
|
|
78
79
|
await sendCloseReady();
|
|
79
80
|
}
|
|
80
81
|
catch (error) {
|
|
81
82
|
if (!abortController.signal.aborted) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
if (error instanceof WebSocketOutboundBufferLimitError) {
|
|
84
|
+
await close(4008, 'outbound buffer limit exceeded').catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await sendFrame(formatErrorMessage({
|
|
88
|
+
message: error instanceof Error ? error.message : 'websocket session failed',
|
|
89
|
+
status: 500,
|
|
90
|
+
})).catch(() => { });
|
|
91
|
+
await close(1011, 'websocket session failed').catch(() => { });
|
|
92
|
+
}
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
finally {
|
|
@@ -109,7 +115,7 @@ export async function serve(options) {
|
|
|
109
115
|
if (payload.action === 'close')
|
|
110
116
|
runtime.closeRequested = true;
|
|
111
117
|
if (expectedAmount && credential.challenge.request.amount !== expectedAmount) {
|
|
112
|
-
await
|
|
118
|
+
await sendFrame(formatErrorMessage({
|
|
113
119
|
message: 'credential amount does not match this endpoint',
|
|
114
120
|
status: 402,
|
|
115
121
|
}));
|
|
@@ -118,7 +124,7 @@ export async function serve(options) {
|
|
|
118
124
|
}
|
|
119
125
|
const authorizationResult = await authorizePaymentFrame({ authorization, requestUrl, route });
|
|
120
126
|
if (authorizationResult.status === 'rejected') {
|
|
121
|
-
await
|
|
127
|
+
await sendFrame(formatErrorMessage({
|
|
122
128
|
message: authorizationResult.message,
|
|
123
129
|
status: authorizationResult.responseStatus,
|
|
124
130
|
}));
|
|
@@ -129,7 +135,7 @@ export async function serve(options) {
|
|
|
129
135
|
if (payload.action === 'voucher' && runtime.streamContext) {
|
|
130
136
|
runtime.streamContext.challengeId = credential.challenge.id;
|
|
131
137
|
}
|
|
132
|
-
await
|
|
138
|
+
await sendFrame(formatReceiptMessage(receipt));
|
|
133
139
|
if (payload.action === 'close') {
|
|
134
140
|
await close(1000, 'payment session closed');
|
|
135
141
|
return;
|
|
@@ -155,6 +161,15 @@ export async function serve(options) {
|
|
|
155
161
|
const onMessage = (payload) => {
|
|
156
162
|
if (runtime.closed)
|
|
157
163
|
return;
|
|
164
|
+
const messageBytes = frameByteLength(payload);
|
|
165
|
+
if (messageBytes !== null && messageBytes > maxIncomingMessageBytes) {
|
|
166
|
+
runtime.closed = true;
|
|
167
|
+
abortController.abort();
|
|
168
|
+
unsubscribe();
|
|
169
|
+
void send(socket, formatErrorMessage({ message: 'payment message is too large', status: 413 })).catch(() => { });
|
|
170
|
+
void Promise.resolve(socket.close(1009, 'payment message is too large')).catch(() => { });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
158
173
|
const raw = toText(payload);
|
|
159
174
|
if (!raw)
|
|
160
175
|
return;
|
|
@@ -173,7 +188,7 @@ export async function serve(options) {
|
|
|
173
188
|
if (!work)
|
|
174
189
|
return;
|
|
175
190
|
if (runtime.queuedActions >= maxQueuedPaymentMessages) {
|
|
176
|
-
void
|
|
191
|
+
void sendFrame(formatErrorMessage({
|
|
177
192
|
message: 'too many queued payment messages',
|
|
178
193
|
status: 429,
|
|
179
194
|
})).catch(() => { });
|
|
@@ -194,11 +209,16 @@ export async function serve(options) {
|
|
|
194
209
|
})
|
|
195
210
|
.catch(async (error) => {
|
|
196
211
|
if (!runtime.closed) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
if (error instanceof WebSocketOutboundBufferLimitError) {
|
|
213
|
+
await close(4008, 'outbound buffer limit exceeded').catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
await sendFrame(formatErrorMessage({
|
|
217
|
+
message: error instanceof Error ? error.message : 'invalid payment message',
|
|
218
|
+
status: 400,
|
|
219
|
+
})).catch(() => { });
|
|
220
|
+
await close(1008, 'invalid payment message').catch(() => { });
|
|
221
|
+
}
|
|
202
222
|
}
|
|
203
223
|
});
|
|
204
224
|
};
|
|
@@ -215,6 +235,41 @@ export async function serve(options) {
|
|
|
215
235
|
message: onMessage,
|
|
216
236
|
});
|
|
217
237
|
}
|
|
238
|
+
function frameByteLength(value) {
|
|
239
|
+
if (value instanceof ArrayBuffer)
|
|
240
|
+
return value.byteLength;
|
|
241
|
+
if (ArrayBuffer.isView(value))
|
|
242
|
+
return value.byteLength;
|
|
243
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob)
|
|
244
|
+
return value.size;
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
let bytes = 0;
|
|
247
|
+
for (const fragment of value) {
|
|
248
|
+
const fragmentBytes = frameByteLength(fragment);
|
|
249
|
+
if (fragmentBytes === null)
|
|
250
|
+
return null;
|
|
251
|
+
bytes += fragmentBytes;
|
|
252
|
+
}
|
|
253
|
+
return bytes;
|
|
254
|
+
}
|
|
255
|
+
if (typeof value !== 'string')
|
|
256
|
+
return null;
|
|
257
|
+
let bytes = 0;
|
|
258
|
+
for (let index = 0; index < value.length; index++) {
|
|
259
|
+
const codePoint = value.codePointAt(index);
|
|
260
|
+
if (codePoint <= 0x7f)
|
|
261
|
+
bytes += 1;
|
|
262
|
+
else if (codePoint <= 0x7ff)
|
|
263
|
+
bytes += 2;
|
|
264
|
+
else if (codePoint <= 0xffff)
|
|
265
|
+
bytes += 3;
|
|
266
|
+
else {
|
|
267
|
+
bytes += 4;
|
|
268
|
+
index += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return bytes;
|
|
272
|
+
}
|
|
218
273
|
function normalizeHttpUrl(value) {
|
|
219
274
|
const url = new URL(value.toString());
|
|
220
275
|
if (url.protocol === 'ws:')
|
|
@@ -239,7 +294,7 @@ async function authorizePaymentFrame(parameters) {
|
|
|
239
294
|
responseStatus: response.status,
|
|
240
295
|
};
|
|
241
296
|
}
|
|
242
|
-
const response = result.withReceipt(new Response(null, { status: 204 }));
|
|
297
|
+
const response = await result.withReceipt(new Response(null, { status: 204 }));
|
|
243
298
|
const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
|
|
244
299
|
if (!receiptHeader)
|
|
245
300
|
throw new Error('management response missing Payment-Receipt header');
|
|
@@ -6,6 +6,8 @@ export type SubscriptionStore = {
|
|
|
6
6
|
activate<result extends {
|
|
7
7
|
subscription: SubscriptionRecord;
|
|
8
8
|
}>(parameters: ActivateParameters<result>): Promise<ActivateResult<result>>;
|
|
9
|
+
/** Releases a transient credential claim so the same challenge can be retried. */
|
|
10
|
+
releaseCredential(challengeId: string): Promise<void>;
|
|
9
11
|
/** Looks up a subscription by subscription ID. */
|
|
10
12
|
get(subscriptionId: string): Promise<SubscriptionRecord | null>;
|
|
11
13
|
/** Looks up a generated access key for a resolved request key. */
|
|
@@ -146,6 +146,9 @@ export function fromStore(store, options) {
|
|
|
146
146
|
await clearActivationState(subscription.lookupKey, challengeId);
|
|
147
147
|
return { status: 'activated', result };
|
|
148
148
|
},
|
|
149
|
+
async releaseCredential(challengeId) {
|
|
150
|
+
await store.delete(credentialKey(challengeId));
|
|
151
|
+
},
|
|
149
152
|
async get(subscriptionId) {
|
|
150
153
|
return (await store.get(recordKey(subscriptionId)));
|
|
151
154
|
},
|
package/dist/validation/core.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function* validateStream(options) {
|
|
|
35
35
|
});
|
|
36
36
|
const effectiveBody = resolvedBody ?? body;
|
|
37
37
|
const isMpp = challengeResults.some((r) => r.severity === 'pass' && r.label === 'Challenge parseable');
|
|
38
|
+
const challengeIsValid = !challengeResults.some((result) => result.severity === 'fail');
|
|
38
39
|
const chainIds = (parsedChallenges ?? [])
|
|
39
40
|
.filter((ch) => ch.method !== 'stripe')
|
|
40
41
|
.map((ch) => ch.request.methodDetails?.chainId)
|
|
@@ -63,7 +64,7 @@ export async function* validateStream(options) {
|
|
|
63
64
|
query: options.query,
|
|
64
65
|
});
|
|
65
66
|
yield { phase: 'errorHandling', endpoint, results: errorResults };
|
|
66
|
-
if (!options.skipPayment) {
|
|
67
|
+
if (!options.skipPayment && challengeIsValid) {
|
|
67
68
|
const onResults = options.onPaymentResults;
|
|
68
69
|
const paymentResults = await validatePaymentFlow(baseUrl, endpoint, verbose, {
|
|
69
70
|
body: effectiveBody,
|
|
@@ -25,7 +25,7 @@ export type Options = {
|
|
|
25
25
|
* and client bugs, not an active attacker. Body binding is unaffected either
|
|
26
26
|
* way — `challenge.digest` is verified against the actual body.
|
|
27
27
|
*
|
|
28
|
-
* @default '
|
|
28
|
+
* @default 'required'
|
|
29
29
|
*/
|
|
30
30
|
routeBinding?: RouteBindingMode | undefined;
|
|
31
31
|
};
|
|
@@ -38,8 +38,10 @@ export type ResolvedOptions = {
|
|
|
38
38
|
routeBinding: RouteBindingMode;
|
|
39
39
|
};
|
|
40
40
|
export type Path = {
|
|
41
|
+
credentialHeaders: readonly string[];
|
|
41
42
|
bindCredential: NonNullable<ServerTransport.Http['bindCredential']>;
|
|
42
43
|
getCredential: ServerTransport.Http['getCredential'];
|
|
44
|
+
matchCredential: NonNullable<ServerTransport.Http['matchCredential']>;
|
|
43
45
|
respondChallenge: (options: Parameters<ServerTransport.Http['respondChallenge']>[0], response?: Response | undefined) => Response | Promise<Response>;
|
|
44
46
|
respondReceipt: (options: Parameters<ServerTransport.Http['respondReceipt']>[0], response: Response) => Response;
|
|
45
47
|
};
|
|
@@ -37,12 +37,13 @@ export function resolveOptions(parameters) {
|
|
|
37
37
|
}
|
|
38
38
|
: {}),
|
|
39
39
|
maxTimeoutSeconds: parameters.options?.maxTimeoutSeconds ?? 300,
|
|
40
|
-
routeBinding: parameters.options?.routeBinding ?? '
|
|
40
|
+
routeBinding: parameters.options?.routeBinding ?? 'required',
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
/** Creates the x402 wire path for an EVM charge method. */
|
|
44
44
|
export function createPath(config) {
|
|
45
45
|
return {
|
|
46
|
+
credentialHeaders: [x402_Types.paymentSignatureHeader],
|
|
46
47
|
getCredential(request) {
|
|
47
48
|
const paymentSignature = request.headers.get(x402_Types.paymentSignatureHeader);
|
|
48
49
|
if (!paymentSignature)
|
|
@@ -53,6 +54,18 @@ export function createPath(config) {
|
|
|
53
54
|
payload: paymentPayload,
|
|
54
55
|
}));
|
|
55
56
|
},
|
|
57
|
+
matchCredential({ input, request }) {
|
|
58
|
+
const paymentSignature = input.headers.get(x402_Types.paymentSignatureHeader);
|
|
59
|
+
if (!paymentSignature)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
const paymentPayload = x402_Header.decodePaymentSignature(paymentSignature);
|
|
63
|
+
return isDeepStrictEqual(paymentPayload.accepted, toPaymentRequirements(request, config));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
56
69
|
async bindCredential({ challenge, credential, input }) {
|
|
57
70
|
const paymentPayload = parsePaymentPayload(credential.payload);
|
|
58
71
|
if (!paymentPayload)
|