mppx 0.13.5 → 0.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/Credential.d.ts +3 -0
- package/dist/Credential.js +9 -0
- package/dist/cli/cli.js +131 -131
- package/dist/cli/plugins/evm.js +2 -2
- package/dist/cli/plugins/index.js +11 -11
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/server/Mppx.js +31 -5
- package/dist/stripe/server/Methods.js +4 -1
- package/dist/tempo/client/Charge.js +1 -0
- package/dist/tempo/internal/auto-swap.d.ts +10 -0
- package/dist/tempo/internal/auto-swap.js +35 -8
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.js +20 -12
- package/dist/tempo/legacy/session/Chain.js +8 -2
- package/dist/tempo/server/Methods.js +12 -1
- package/dist/tempo/server/Relay.js +6 -22
- package/dist/tempo/server/Subscription.js +3 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +3 -0
- package/dist/tempo/server/internal/transport.js +5 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/client/ChannelStore.js +47 -0
- package/dist/tempo/session/client/CredentialState.js +8 -3
- package/dist/tempo/session/client/Session.js +50 -33
- package/dist/tempo/session/client/SessionManager.js +186 -65
- package/dist/tempo/session/client/Transports.d.ts +6 -0
- package/dist/tempo/session/client/Transports.js +52 -7
- package/dist/tempo/session/precompile/Chain.js +5 -2
- package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
- package/dist/tempo/session/server/ChannelStore.js +69 -9
- package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
- package/dist/tempo/session/server/CredentialVerification.js +220 -48
- package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
- package/dist/tempo/session/server/MeteredStream.js +10 -26
- package/dist/tempo/session/server/Session.d.ts +29 -2
- package/dist/tempo/session/server/Session.js +114 -33
- package/dist/tempo/session/server/Settlement.d.ts +9 -0
- package/dist/tempo/session/server/Settlement.js +12 -0
- package/dist/tempo/session/server/Sse.d.ts +4 -3
- package/dist/tempo/session/server/Sse.js +3 -3
- package/dist/tempo/session/server/Ws.d.ts +9 -1
- package/dist/tempo/session/server/Ws.js +1 -0
- package/package.json +1 -1
|
@@ -5,12 +5,11 @@ import * as ChannelStore from './ChannelStore.js';
|
|
|
5
5
|
/** Controller passed to manual-charge streaming generators. */
|
|
6
6
|
export type SessionController = {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Commit a charge before performing billable work.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* charge is
|
|
12
|
-
*
|
|
13
|
-
* generator fails before the charge can be committed.
|
|
10
|
+
* Waits for sufficient voucher coverage and commits the charge before resolving.
|
|
11
|
+
* Once resolved, the charge is retained even if the stream is canceled or the
|
|
12
|
+
* generator fails. Await this method before starting the corresponding work.
|
|
14
13
|
*
|
|
15
14
|
* Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
|
|
16
15
|
* pricing. When omitted, the session challenge's configured tick cost is
|
|
@@ -32,6 +31,8 @@ export type MeteredStreamOptions = {
|
|
|
32
31
|
formatNeedVoucher(parameters: NeedVoucherEvent): string;
|
|
33
32
|
/** Async source or manual-charge source. */
|
|
34
33
|
generate: SessionStreamGenerator;
|
|
34
|
+
/** Verifies that the channel can still fund work and delivery; failures stop the stream. */
|
|
35
|
+
beforeCharge?: (() => MaybePromise<void>) | undefined;
|
|
35
36
|
/** Runs after a nonzero reserved charge has been committed. */
|
|
36
37
|
onChargeCommitted?: ((channel: ChannelStore.State) => MaybePromise<unknown>) | undefined;
|
|
37
38
|
/** Store polling interval when `waitForUpdate` is unavailable. */
|
|
@@ -1,38 +1,24 @@
|
|
|
1
|
-
import { commitReservedCharges, releaseReservedCharges,
|
|
1
|
+
import { commitReservedCharges, releaseReservedCharges, reserveChargeOrWait } from './Transports.js';
|
|
2
2
|
/** Applies voucher reservation and spend commits to an async session stream. */
|
|
3
3
|
export async function* meterIterable(options) {
|
|
4
|
+
const manual = typeof options.generate === 'function';
|
|
4
5
|
let prepaidUnits = options.prepaidUnits ?? 0;
|
|
5
|
-
const
|
|
6
|
-
const signal = controller.signal;
|
|
6
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
7
7
|
const reservation = {
|
|
8
8
|
store: options.store,
|
|
9
9
|
channelId: options.channelId,
|
|
10
10
|
reservationId: globalThis.crypto.randomUUID(),
|
|
11
11
|
};
|
|
12
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
13
|
const releaseOnAbort = () => {
|
|
29
|
-
clearInterval(renewal);
|
|
30
14
|
void releaseReservedCharges(reservation).catch(() => { });
|
|
31
15
|
};
|
|
32
16
|
signal.addEventListener('abort', releaseOnAbort, { once: true });
|
|
33
17
|
if (signal.aborted)
|
|
34
18
|
releaseOnAbort();
|
|
35
19
|
const charge = async (amount) => {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
await options.beforeCharge?.();
|
|
36
22
|
signal.throwIfAborted();
|
|
37
23
|
if (amount === undefined && prepaidUnits > 0) {
|
|
38
24
|
prepaidUnits -= 1;
|
|
@@ -50,9 +36,13 @@ export async function* meterIterable(options) {
|
|
|
50
36
|
signal,
|
|
51
37
|
});
|
|
52
38
|
pending = true;
|
|
39
|
+
if (manual)
|
|
40
|
+
await commit();
|
|
53
41
|
signal.throwIfAborted();
|
|
54
42
|
};
|
|
55
43
|
const commit = async () => {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
await options.beforeCharge?.();
|
|
56
46
|
signal.throwIfAborted();
|
|
57
47
|
if (!pending)
|
|
58
48
|
return;
|
|
@@ -63,7 +53,6 @@ export async function* meterIterable(options) {
|
|
|
63
53
|
await options.onChargeCommitted?.(channel);
|
|
64
54
|
};
|
|
65
55
|
try {
|
|
66
|
-
const manual = typeof options.generate === 'function';
|
|
67
56
|
const iterable = typeof options.generate === 'function'
|
|
68
57
|
? options.generate({ charge, signal })
|
|
69
58
|
: options.generate;
|
|
@@ -74,7 +63,7 @@ export async function* meterIterable(options) {
|
|
|
74
63
|
if (signal.aborted)
|
|
75
64
|
break;
|
|
76
65
|
if (done) {
|
|
77
|
-
if (manual)
|
|
66
|
+
if (manual && pending)
|
|
78
67
|
await commit();
|
|
79
68
|
break;
|
|
80
69
|
}
|
|
@@ -83,18 +72,13 @@ export async function* meterIterable(options) {
|
|
|
83
72
|
await commit();
|
|
84
73
|
yield value;
|
|
85
74
|
}
|
|
86
|
-
if (!cancelled)
|
|
87
|
-
signal.throwIfAborted();
|
|
88
75
|
}
|
|
89
76
|
finally {
|
|
90
|
-
clearInterval(renewal);
|
|
91
77
|
await releaseReservedCharges(reservation);
|
|
92
78
|
await iterator.return?.();
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
finally {
|
|
96
|
-
clearInterval(renewal);
|
|
97
|
-
options.signal?.removeEventListener('abort', onAbort);
|
|
98
82
|
signal.removeEventListener('abort', releaseOnAbort);
|
|
99
83
|
await releaseReservedCharges(reservation);
|
|
100
84
|
}
|
|
@@ -21,6 +21,8 @@ import { type ResolveSessionChannelId } from './RequestState.js';
|
|
|
21
21
|
import { type SettleChargedSessionChannel } from './Settlement.js';
|
|
22
22
|
import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
|
|
23
23
|
import * as Ws from './Ws.js';
|
|
24
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
25
|
+
export declare function verifyStream(method: object, channelId: Hex): Promise<void>;
|
|
24
26
|
/** Server-side automatic settlement schedule. */
|
|
25
27
|
export type { SettlementSchedule } from './Settlement.js';
|
|
26
28
|
/** Server-side settlement event hook types. */
|
|
@@ -131,15 +133,27 @@ export declare namespace session {
|
|
|
131
133
|
serveWebSocket: (options: ServeWebSocketOptions) => Promise<void>;
|
|
132
134
|
};
|
|
133
135
|
/** WebSocket server options supplied after the session binds shared dependencies. */
|
|
134
|
-
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
136
|
+
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'beforeCharge' | 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
135
137
|
/** Request defaults inferred from the Tempo session method schema. */
|
|
136
138
|
type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.session>, 'escrowContract' | 'feePayer' | 'recipient'>;
|
|
137
139
|
/** Partial fee-sponsor policy used for server-submitted session transactions. */
|
|
138
140
|
type FeePayerPolicy = Partial<FeePayer.Policy>;
|
|
139
141
|
/** Parameters accepted by the TIP-1034 server session payment method. */
|
|
140
142
|
type Parameters = {
|
|
141
|
-
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation
|
|
143
|
+
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation, and before a voucher accepted under `voucherStateMaxAge` starts a fresh read. @default 5_000 */
|
|
142
144
|
channelStateTtl?: number | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Maximum age in milliseconds of verified on-chain channel state that a voucher may be validated
|
|
147
|
+
* and accepted against without waiting for a fresh read. The fresh read then finishes in the background and
|
|
148
|
+
* records a pending close, so later vouchers are rejected. `0` reads on-chain state before every
|
|
149
|
+
* acceptance.
|
|
150
|
+
*
|
|
151
|
+
* A payer can withdraw only after the escrow's `CLOSE_GRACE_PERIOD` (900 s on Tempo) following a
|
|
152
|
+
* close request, so vouchers accepted this way stay settleable as long as the payee settles them
|
|
153
|
+
* within the grace period minus this age. Keep it well below that bound.
|
|
154
|
+
* @default 0
|
|
155
|
+
*/
|
|
156
|
+
voucherStateMaxAge?: number | undefined;
|
|
143
157
|
/** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
|
|
144
158
|
feePayerPolicy?: FeePayerPolicy | undefined;
|
|
145
159
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
@@ -159,6 +173,19 @@ export declare namespace session {
|
|
|
159
173
|
* containing the signed highest voucher for client rehydration.
|
|
160
174
|
*/
|
|
161
175
|
bootstrap?: boolean | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* Lets a session challenge pay for later requests to the same route until it expires. The
|
|
178
|
+
* challenge is bound to its route, currency, recipient and channel terms but not to the amount
|
|
179
|
+
* it quoted, and every request is charged its own price from the channel's accepted balance.
|
|
180
|
+
* A client can then send a voucher with a request instead of waiting for a `402`.
|
|
181
|
+
*/
|
|
182
|
+
reusableChallenges?: boolean | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* Charges request-priced content (`unitType: 'request'`) in the same store update that accepts
|
|
185
|
+
* its voucher, before the response, instead of in a second update when the response is
|
|
186
|
+
* delivered. The server must then refund requests it cannot serve.
|
|
187
|
+
*/
|
|
188
|
+
chargeOnAcceptance?: boolean | undefined;
|
|
162
189
|
/**
|
|
163
190
|
* Atomic store backend for channel state.
|
|
164
191
|
*
|
|
@@ -20,17 +20,26 @@ import * as Methods from '../../Methods.js';
|
|
|
20
20
|
import * as ChargeServer from '../../server/Charge.js';
|
|
21
21
|
import { fallbackStore } from '../../server/internal/fallback-store.js';
|
|
22
22
|
import * as Transport from '../../server/internal/transport.js';
|
|
23
|
+
import * as Chain from '../precompile/Chain.js';
|
|
23
24
|
import { tip20ChannelEscrow } from '../precompile/Protocol.js';
|
|
24
25
|
import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot, } from '../Snapshot.js';
|
|
25
26
|
import * as ChannelStore from './ChannelStore.js';
|
|
26
27
|
import { broadcastCredentialPayload, validateCredentialPayload } from './CredentialVerification.js';
|
|
27
|
-
import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
28
|
+
import { requireSessionCredentialPayload, validateChannelState } from './CredentialVerification.js';
|
|
28
29
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
29
30
|
import { respondToSessionCredential } from './RequestState.js';
|
|
30
|
-
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
31
|
+
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, shouldChargeVoucherOnAcceptance, } from './Settlement.js';
|
|
31
32
|
import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
32
33
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
33
34
|
import * as Ws from './Ws.js';
|
|
35
|
+
const streamVerifiers = new WeakMap();
|
|
36
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
37
|
+
export function verifyStream(method, channelId) {
|
|
38
|
+
const verify = streamVerifiers.get(method);
|
|
39
|
+
if (!verify)
|
|
40
|
+
throw new Error('Session stream verifier is not configured');
|
|
41
|
+
return verify(channelId);
|
|
42
|
+
}
|
|
34
43
|
export { settle, settleBatch } from './Settlement.js';
|
|
35
44
|
function deriveServerDefaults(values) {
|
|
36
45
|
// `Method.toServer()` models defaults from request input fields. Tempo session
|
|
@@ -191,6 +200,19 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
191
200
|
}
|
|
192
201
|
return new Response(null, { status: 204, headers });
|
|
193
202
|
}
|
|
203
|
+
/** Binds a reusable session challenge to its route and channel terms but not to its quoted amount. */
|
|
204
|
+
const reusableSessionBinding = (request) => {
|
|
205
|
+
const { methodDetails } = request;
|
|
206
|
+
return {
|
|
207
|
+
chainId: methodDetails.chainId,
|
|
208
|
+
currency: request.currency,
|
|
209
|
+
escrowContract: methodDetails.escrowContract,
|
|
210
|
+
operator: methodDetails.operator,
|
|
211
|
+
recipient: request.recipient,
|
|
212
|
+
sessionProtocol: methodDetails[Constants.MethodDetailKeys.sessionProtocol],
|
|
213
|
+
unitType: request.unitType,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
194
216
|
/** Creates a server-side TIP20EscrowChannel precompile session payment method. */
|
|
195
217
|
export function session(p) {
|
|
196
218
|
const parameters = p;
|
|
@@ -236,8 +258,33 @@ export function session(p) {
|
|
|
236
258
|
// A failed scheduled settlement is reported and retried by the next one; the charged request
|
|
237
259
|
// stays served because the payer's voucher already covers the charge.
|
|
238
260
|
const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
|
|
261
|
+
// Stream checks have a bounded TTL even when callers disable validation caching.
|
|
262
|
+
// Cache only successful reads; RPC failures must stop delivery.
|
|
263
|
+
const beforeStreamCharge = async (channelId) => {
|
|
264
|
+
channelId = ChannelStore.normalizeChannelId(channelId);
|
|
265
|
+
const channel = await store.getChannel(channelId);
|
|
266
|
+
if (!channel || channel.finalized || channel.closeRequestedAt !== 0n)
|
|
267
|
+
throw new Errors.ChannelClosedError({ reason: 'stream channel is closed' });
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const ttl = Math.min(Math.max(channelStateTtl, 0), 5_000);
|
|
270
|
+
const lastVerified = lastOnChainVerified.get(channelId);
|
|
271
|
+
if (lastVerified !== undefined && now >= lastVerified && now - lastVerified < ttl)
|
|
272
|
+
return;
|
|
273
|
+
let state;
|
|
274
|
+
try {
|
|
275
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
276
|
+
state = await Chain.getChannelState(client, channelId, channel.escrowContract);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Transport errors can include private RPC URLs or authentication details.
|
|
280
|
+
throw new Error('Unable to verify stream escrow');
|
|
281
|
+
}
|
|
282
|
+
validateChannelState(state);
|
|
283
|
+
lastOnChainVerified.set(channelId, now);
|
|
284
|
+
};
|
|
239
285
|
const serveWebSocket = (options) => Ws.serve({
|
|
240
286
|
...options,
|
|
287
|
+
beforeCharge: beforeStreamCharge,
|
|
241
288
|
onChargeCommitted: settleCharged,
|
|
242
289
|
store,
|
|
243
290
|
});
|
|
@@ -253,9 +300,11 @@ export function session(p) {
|
|
|
253
300
|
});
|
|
254
301
|
const transport = parameters.sse
|
|
255
302
|
? Transport.sse({
|
|
303
|
+
...(typeof parameters.sse === 'object' ? parameters.sse : {}),
|
|
304
|
+
beforeCharge: beforeStreamCharge,
|
|
305
|
+
reusableChallenges: parameters.reusableChallenges,
|
|
256
306
|
settleCharged,
|
|
257
307
|
store,
|
|
258
|
-
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
259
308
|
})
|
|
260
309
|
: undefined;
|
|
261
310
|
const validateCredential = async ({ credential, operation, request, }) => {
|
|
@@ -267,24 +316,31 @@ export function session(p) {
|
|
|
267
316
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
268
317
|
request,
|
|
269
318
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
319
|
+
// A broadcast voucher is checked against the stored channel by the update that accepts it.
|
|
320
|
+
const details = operation === 'broadcast' && payload.action === 'voucher'
|
|
321
|
+
? {
|
|
322
|
+
action: payload.action,
|
|
323
|
+
channelId: ChannelStore.normalizeChannelId(payload.channelId),
|
|
324
|
+
}
|
|
325
|
+
: await validateCredentialPayload({
|
|
326
|
+
account,
|
|
327
|
+
allowPendingClose: operation === 'broadcast',
|
|
328
|
+
challenge: credential.challenge,
|
|
329
|
+
channelStateTtl,
|
|
330
|
+
chainId: context.chainId,
|
|
331
|
+
client: context.client,
|
|
332
|
+
credentialSource: credential.source,
|
|
333
|
+
escrow: context.escrow,
|
|
334
|
+
expectedOperator: context.methodDetails.operator,
|
|
335
|
+
feePayer: context.feePayer,
|
|
336
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
337
|
+
feeToken: context.methodDetails.feeToken,
|
|
338
|
+
lastOnChainVerified,
|
|
339
|
+
minVoucherDelta: context.minVoucherDelta,
|
|
340
|
+
payload,
|
|
341
|
+
store,
|
|
342
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
343
|
+
});
|
|
288
344
|
return {
|
|
289
345
|
challenge: credential.challenge,
|
|
290
346
|
credential: { ...credential, payload },
|
|
@@ -305,9 +361,34 @@ export function session(p) {
|
|
|
305
361
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
306
362
|
request,
|
|
307
363
|
});
|
|
364
|
+
const chargeAmount = parameters.chargeOnAcceptance &&
|
|
365
|
+
shouldChargeVoucherOnAcceptance({
|
|
366
|
+
capturedRequest: envelope?.capturedRequest,
|
|
367
|
+
payloadAction: payload.action,
|
|
368
|
+
unitType: context.request.unitType,
|
|
369
|
+
})
|
|
370
|
+
? BigInt(context.request.amount ?? challenge.request.amount)
|
|
371
|
+
: undefined;
|
|
372
|
+
const acceptance = {};
|
|
373
|
+
const settleAcceptedCharge = (channel) => maybeSettleScheduled({
|
|
374
|
+
account,
|
|
375
|
+
client: context.client,
|
|
376
|
+
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
377
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
378
|
+
feeToken: parameters.feeToken,
|
|
379
|
+
onSessionSettlement,
|
|
380
|
+
onSessionSettlementFailure,
|
|
381
|
+
schedule: settlementSchedule,
|
|
382
|
+
store,
|
|
383
|
+
channel,
|
|
384
|
+
}).catch(ignoreRetryableSettlementFailure);
|
|
308
385
|
const sessionReceipt = await broadcastCredentialPayload({
|
|
309
386
|
account,
|
|
310
387
|
challenge,
|
|
388
|
+
chargeAmount,
|
|
389
|
+
onCharged: (channel) => {
|
|
390
|
+
acceptance.charged = channel;
|
|
391
|
+
},
|
|
311
392
|
channelStateTtl,
|
|
312
393
|
chainId: context.chainId,
|
|
313
394
|
client: context.client,
|
|
@@ -328,7 +409,16 @@ export function session(p) {
|
|
|
328
409
|
sseEnabled: Boolean(parameters.sse),
|
|
329
410
|
}),
|
|
330
411
|
store,
|
|
412
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
331
413
|
});
|
|
414
|
+
if (acceptance.charged) {
|
|
415
|
+
const settlementTxHash = await settleAcceptedCharge(acceptance.charged);
|
|
416
|
+
const receipt = {
|
|
417
|
+
...sessionReceipt,
|
|
418
|
+
...(settlementTxHash ? { txHash: settlementTxHash } : {}),
|
|
419
|
+
};
|
|
420
|
+
return parameters.sse ? Transport.markPrepaidSessionTick(receipt) : receipt;
|
|
421
|
+
}
|
|
332
422
|
return applyVerifiedHttpAccounting({
|
|
333
423
|
capturedRequest: envelope?.capturedRequest,
|
|
334
424
|
payloadAction: payload.action,
|
|
@@ -337,18 +427,7 @@ export function session(p) {
|
|
|
337
427
|
sseEnabled: Boolean(parameters.sse),
|
|
338
428
|
markPrepaidReceipt: Transport.markPrepaidSessionTick,
|
|
339
429
|
charge: (channelId, requestAmount) => chargeSessionChannel({ store, channelId, amount: requestAmount }),
|
|
340
|
-
settleCharged:
|
|
341
|
-
account,
|
|
342
|
-
client: context.client,
|
|
343
|
-
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
344
|
-
feePayerPolicy: parameters.feePayerPolicy,
|
|
345
|
-
feeToken: parameters.feeToken,
|
|
346
|
-
onSessionSettlement,
|
|
347
|
-
onSessionSettlementFailure,
|
|
348
|
-
schedule: settlementSchedule,
|
|
349
|
-
store,
|
|
350
|
-
channel,
|
|
351
|
-
}).catch(ignoreRetryableSettlementFailure),
|
|
430
|
+
settleCharged: settleAcceptedCharge,
|
|
352
431
|
});
|
|
353
432
|
};
|
|
354
433
|
const method = Method.toServer(Methods.session, {
|
|
@@ -364,6 +443,7 @@ export function session(p) {
|
|
|
364
443
|
unitType,
|
|
365
444
|
}),
|
|
366
445
|
extensions: { serveWebSocket, settleScheduled },
|
|
446
|
+
...(parameters.reusableChallenges ? { stableBinding: reusableSessionBinding } : {}),
|
|
367
447
|
transport: deriveTransport(transport),
|
|
368
448
|
preflight: parameters.bootstrap
|
|
369
449
|
? async ({ capturedRequest, credential, expires, input, options, realm, secretKey }) => {
|
|
@@ -430,6 +510,7 @@ export function session(p) {
|
|
|
430
510
|
});
|
|
431
511
|
},
|
|
432
512
|
});
|
|
513
|
+
streamVerifiers.set(method, beforeStreamCharge);
|
|
433
514
|
return method;
|
|
434
515
|
}
|
|
435
516
|
(function (session) {
|
|
@@ -174,6 +174,15 @@ export type ApplyVerifiedHttpAccountingParameters = {
|
|
|
174
174
|
};
|
|
175
175
|
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
176
176
|
export declare function shouldApplyVerifiedHttpAccounting(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction' | 'sseEnabled'>): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
179
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
180
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
181
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
182
|
+
*/
|
|
183
|
+
export declare function shouldChargeVoucherOnAcceptance(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction'> & {
|
|
184
|
+
unitType: unknown;
|
|
185
|
+
}): boolean;
|
|
177
186
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
178
187
|
export declare function applyVerifiedHttpAccounting(parameters: ApplyVerifiedHttpAccountingParameters): Promise<SessionReceipt>;
|
|
179
188
|
/** Atomically deducts spend from a channel and maps store failures to typed session errors. */
|
|
@@ -171,6 +171,18 @@ export function shouldApplyVerifiedHttpAccounting(parameters) {
|
|
|
171
171
|
return false;
|
|
172
172
|
return isSessionContentRequest(capturedRequest);
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
176
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
177
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
178
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
179
|
+
*/
|
|
180
|
+
export function shouldChargeVoucherOnAcceptance(parameters) {
|
|
181
|
+
const { capturedRequest, payloadAction, unitType } = parameters;
|
|
182
|
+
if (!capturedRequest || payloadAction !== 'voucher' || unitType !== 'request')
|
|
183
|
+
return false;
|
|
184
|
+
return isSessionContentRequest(capturedRequest);
|
|
185
|
+
}
|
|
174
186
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
175
187
|
export async function applyVerifiedHttpAccounting(parameters) {
|
|
176
188
|
const { receipt, sseEnabled } = parameters;
|
|
@@ -27,9 +27,8 @@ export type { SessionController } from './MeteredStream.js';
|
|
|
27
27
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
28
28
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
29
29
|
* and polls store until the client tops up the channel.
|
|
30
|
-
* 4. Commits
|
|
31
|
-
*
|
|
32
|
-
* commits that terminal charge on successful completion.
|
|
30
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
31
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
33
32
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
34
33
|
*
|
|
35
34
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -43,6 +42,8 @@ export declare namespace serve {
|
|
|
43
42
|
challengeId: string;
|
|
44
43
|
tickCost: bigint;
|
|
45
44
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
|
|
45
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
46
|
+
beforeCharge?: MeteredStreamOptions['beforeCharge'];
|
|
46
47
|
/** Callback invoked after each nonzero stream charge is committed. */
|
|
47
48
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
48
49
|
pollIntervalMs?: number | undefined;
|
|
@@ -26,9 +26,8 @@ export { extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptE
|
|
|
26
26
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
27
27
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
28
28
|
* and polls store until the client tops up the channel.
|
|
29
|
-
* 4. Commits
|
|
30
|
-
*
|
|
31
|
-
* commits that terminal charge on successful completion.
|
|
29
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
30
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
32
31
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
33
32
|
*
|
|
34
33
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -44,6 +43,7 @@ export function serve(options) {
|
|
|
44
43
|
channelId,
|
|
45
44
|
tickCost,
|
|
46
45
|
generate,
|
|
46
|
+
beforeCharge: options.beforeCharge,
|
|
47
47
|
onChargeCommitted: options.onChargeCommitted,
|
|
48
48
|
pollIntervalMs,
|
|
49
49
|
prepaidUnits: options.prepaidUnits,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionCredentialContext } from '../precompile/Protocol.js';
|
|
1
2
|
import * as ChannelStore from './ChannelStore.js';
|
|
2
3
|
import type { MeteredStreamOptions, SessionController } from './MeteredStream.js';
|
|
3
4
|
import type { SettleChargedSessionChannel } from './Settlement.js';
|
|
@@ -16,6 +17,11 @@ export type SessionRouteResult = {
|
|
|
16
17
|
};
|
|
17
18
|
/** HTTP route used to perform a WebSocket payment probe. */
|
|
18
19
|
export type SessionRoute = (request: Request) => Promise<SessionRouteResult>;
|
|
20
|
+
type StreamContext = {
|
|
21
|
+
challengeId: string;
|
|
22
|
+
channelId: SessionCredentialContext['channelId'];
|
|
23
|
+
tickCost: bigint;
|
|
24
|
+
};
|
|
19
25
|
/**
|
|
20
26
|
* Bridge a WebSocket connection to a Tempo session payment flow.
|
|
21
27
|
*
|
|
@@ -47,7 +53,9 @@ export declare namespace serve {
|
|
|
47
53
|
/** Payment route handler. Receives synthetic `POST` requests with only
|
|
48
54
|
* the `Authorization` header — no cookies, bodies, or upgrade headers. */
|
|
49
55
|
route: SessionRoute;
|
|
50
|
-
/**
|
|
56
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
57
|
+
beforeCharge?: ((channelId: StreamContext['channelId']) => void | Promise<void>) | undefined;
|
|
58
|
+
/** Callback invoked after each nonzero stream charge is committed. */
|
|
51
59
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
52
60
|
/** @deprecated Use `onChargeCommitted`. */
|
|
53
61
|
settleScheduled?: SettleChargedSessionChannel | undefined;
|
|
@@ -65,6 +65,7 @@ export async function serve(options) {
|
|
|
65
65
|
channelId: context.channelId,
|
|
66
66
|
tickCost: context.tickCost,
|
|
67
67
|
generate,
|
|
68
|
+
beforeCharge: () => options.beforeCharge?.(context.channelId),
|
|
68
69
|
onChargeCommitted: options.onChargeCommitted ?? options.settleScheduled,
|
|
69
70
|
pollIntervalMs,
|
|
70
71
|
signal: abortController.signal,
|