mppx 0.13.5 → 0.14.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/Credential.d.ts +3 -0
  3. package/dist/Credential.js +9 -0
  4. package/dist/cli/cli.js +131 -131
  5. package/dist/cli/plugins/evm.js +2 -2
  6. package/dist/cli/plugins/index.js +11 -11
  7. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  8. package/dist/client/internal/MethodChallenge.js +4 -0
  9. package/dist/internal/version.d.ts +2 -2
  10. package/dist/internal/version.js +1 -1
  11. package/dist/server/Mppx.js +31 -5
  12. package/dist/stripe/server/Methods.js +4 -1
  13. package/dist/tempo/client/Charge.js +1 -0
  14. package/dist/tempo/internal/auto-swap.d.ts +10 -0
  15. package/dist/tempo/internal/auto-swap.js +35 -8
  16. package/dist/tempo/internal/fee-payer.d.ts +8 -0
  17. package/dist/tempo/internal/fee-payer.js +20 -12
  18. package/dist/tempo/legacy/session/Chain.js +8 -2
  19. package/dist/tempo/server/Methods.d.ts +7 -2
  20. package/dist/tempo/server/Methods.js +20 -2
  21. package/dist/tempo/server/Relay.js +6 -22
  22. package/dist/tempo/server/Subscription.js +3 -0
  23. package/dist/tempo/server/internal/html.gen.js +1 -1
  24. package/dist/tempo/server/internal/transport.d.ts +3 -0
  25. package/dist/tempo/server/internal/transport.js +5 -1
  26. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  27. package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
  28. package/dist/tempo/session/client/ChannelStore.js +47 -0
  29. package/dist/tempo/session/client/CredentialState.js +8 -3
  30. package/dist/tempo/session/client/Session.js +50 -33
  31. package/dist/tempo/session/client/SessionManager.js +186 -65
  32. package/dist/tempo/session/client/Transports.d.ts +6 -0
  33. package/dist/tempo/session/client/Transports.js +52 -7
  34. package/dist/tempo/session/precompile/Chain.d.ts +2 -2
  35. package/dist/tempo/session/precompile/Chain.js +8 -4
  36. package/dist/tempo/session/server/ChannelStore.d.ts +186 -3
  37. package/dist/tempo/session/server/ChannelStore.js +312 -12
  38. package/dist/tempo/session/server/CloseSupervisor.d.ts +98 -0
  39. package/dist/tempo/session/server/CloseSupervisor.js +289 -0
  40. package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
  41. package/dist/tempo/session/server/CredentialVerification.js +220 -48
  42. package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
  43. package/dist/tempo/session/server/MeteredStream.js +10 -26
  44. package/dist/tempo/session/server/Session.d.ts +70 -4
  45. package/dist/tempo/session/server/Session.js +156 -36
  46. package/dist/tempo/session/server/Settlement.d.ts +18 -4
  47. package/dist/tempo/session/server/Settlement.js +119 -32
  48. package/dist/tempo/session/server/SponsorshipLimit.d.ts +52 -0
  49. package/dist/tempo/session/server/SponsorshipLimit.js +107 -0
  50. package/dist/tempo/session/server/Sse.d.ts +4 -3
  51. package/dist/tempo/session/server/Sse.js +3 -3
  52. package/dist/tempo/session/server/Ws.d.ts +9 -1
  53. package/dist/tempo/session/server/Ws.js +1 -0
  54. package/dist/tempo/session/server/index.d.ts +9 -1
  55. package/dist/tempo/session/server/index.js +5 -1
  56. package/package.json +1 -1
@@ -1,38 +1,24 @@
1
- import { commitReservedCharges, releaseReservedCharges, renewReservedCharges, reserveChargeOrWait, streamReservationLeaseMs, } from './Transports.js';
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 controller = new AbortController();
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
  }
@@ -18,9 +18,12 @@ import * as Transport from '../../server/internal/transport.js';
18
18
  import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot } from '../Snapshot.js';
19
19
  import * as ChannelStore from './ChannelStore.js';
20
20
  import { type ResolveSessionChannelId } from './RequestState.js';
21
- import { type SettleChargedSessionChannel } from './Settlement.js';
21
+ import { type SessionStoreInput, type SettleChargedSessionChannel } from './Settlement.js';
22
22
  import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
23
+ import * as SponsorshipLimit from './SponsorshipLimit.js';
23
24
  import * as Ws from './Ws.js';
25
+ /** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
26
+ export declare function verifyStream(method: object, channelId: Hex): Promise<void>;
24
27
  /** Server-side automatic settlement schedule. */
25
28
  export type { SettlementSchedule } from './Settlement.js';
26
29
  /** Server-side settlement event hook types. */
@@ -131,15 +134,27 @@ export declare namespace session {
131
134
  serveWebSocket: (options: ServeWebSocketOptions) => Promise<void>;
132
135
  };
133
136
  /** WebSocket server options supplied after the session binds shared dependencies. */
134
- type ServeWebSocketOptions = Omit<Ws.serve.Options, 'onChargeCommitted' | 'settleScheduled' | 'store'>;
137
+ type ServeWebSocketOptions = Omit<Ws.serve.Options, 'beforeCharge' | 'onChargeCommitted' | 'settleScheduled' | 'store'>;
135
138
  /** Request defaults inferred from the Tempo session method schema. */
136
139
  type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.session>, 'escrowContract' | 'feePayer' | 'recipient'>;
137
140
  /** Partial fee-sponsor policy used for server-submitted session transactions. */
138
141
  type FeePayerPolicy = Partial<FeePayer.Policy>;
139
142
  /** Parameters accepted by the TIP-1034 server session payment method. */
140
143
  type Parameters = {
141
- /** TTL in milliseconds for cached on-chain channel state during non-mutating validation. Voucher acceptance always refreshes on-chain state. @default 5_000 */
144
+ /** 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
145
  channelStateTtl?: number | undefined;
146
+ /**
147
+ * Maximum age in milliseconds of verified on-chain channel state that a voucher may be validated
148
+ * and accepted against without waiting for a fresh read. The fresh read then finishes in the background and
149
+ * records a pending close, so later vouchers are rejected. `0` reads on-chain state before every
150
+ * acceptance.
151
+ *
152
+ * A payer can withdraw only after the escrow's `CLOSE_GRACE_PERIOD` (900 s on Tempo) following a
153
+ * close request, so vouchers accepted this way stay settleable as long as the payee settles them
154
+ * within the grace period minus this age. Keep it well below that bound.
155
+ * @default 0
156
+ */
157
+ voucherStateMaxAge?: number | undefined;
143
158
  /** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
144
159
  feePayerPolicy?: FeePayerPolicy | undefined;
145
160
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
@@ -159,6 +174,19 @@ export declare namespace session {
159
174
  * containing the signed highest voucher for client rehydration.
160
175
  */
161
176
  bootstrap?: boolean | undefined;
177
+ /**
178
+ * Lets a session challenge pay for later requests to the same route until it expires. The
179
+ * challenge is bound to its route, currency, recipient and channel terms but not to the amount
180
+ * it quoted, and every request is charged its own price from the channel's accepted balance.
181
+ * A client can then send a voucher with a request instead of waiting for a `402`.
182
+ */
183
+ reusableChallenges?: boolean | undefined;
184
+ /**
185
+ * Charges request-priced content (`unitType: 'request'`) in the same store update that accepts
186
+ * its voucher, before the response, instead of in a second update when the response is
187
+ * delivered. The server must then refund requests it cannot serve.
188
+ */
189
+ chargeOnAcceptance?: boolean | undefined;
162
190
  /**
163
191
  * Atomic store backend for channel state.
164
192
  *
@@ -181,6 +209,14 @@ export declare namespace session {
181
209
  settlementSchedule?: SettlementSchedule | undefined;
182
210
  /** Optional fee token for management and server-driven settle/close transactions. */
183
211
  feeToken?: Address | undefined;
212
+ /**
213
+ * Limits fee-payer-sponsored channel opens and top-ups to `perDay` of each per payer per UTC
214
+ * day (default 5), counted in `store`. The payer is the verified signer of the transaction, never
215
+ * the credential's client-supplied `source`. A transaction over the limit, or one whose
216
+ * signatures prove no payer, is broadcast unsponsored, so it lands only if the payer pays its
217
+ * own fees.
218
+ */
219
+ sponsorshipLimit?: SponsorshipLimit.Parameters | undefined;
184
220
  } & Account.resolve.Parameters & Client.getResolver.Parameters & Defaults & Method.ComposableHooks<typeof Methods.session>;
185
221
  /** Defaults derived from `session()` parameters for handler type inference. */
186
222
  type DeriveDefaults<parameters extends Parameters> = types.DeriveDefaults<parameters, Defaults, {
@@ -196,6 +232,36 @@ export declare namespace session {
196
232
  *
197
233
  * Delegates to the shared `deductFromChannel` atomic helper and translates
198
234
  * failure modes into typed errors (`InsufficientBalanceError`, `ChannelClosedError`).
235
+ *
236
+ * With `options.idempotencyKey` the channel is charged at most once per key: a retry with the same
237
+ * key and amount returns the channel without charging again, and the same key with a different
238
+ * amount throws `IdempotencyKeyConflictError`.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * // One durable job per voucher, retried under its operation ID.
243
+ * const channel = await Session.Server.charge(store, channelId, jobCost, {
244
+ * idempotencyKey: operationId,
245
+ * })
246
+ * ```
247
+ */
248
+ export declare function charge(store: SessionStoreInput, channelId: Hex, amount: bigint, options?: ChannelStore.ChargeOptions): Promise<ChannelStore.State>;
249
+ /**
250
+ * Returns an unused amount to a channel's `spent` at most once per idempotency key.
251
+ *
252
+ * The first outcome for a key is final. A credit is `declined` when the channel is closing or
253
+ * finalized, or when it would take `spent` below zero or below what settled, or may still settle,
254
+ * on-chain, because a close captures `max(spent, settled)`; the caller then refunds another way.
255
+ * `unknown` reports a store write that failed and may have committed; retrying with the same key
256
+ * returns the outcome that was recorded.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const result = await Session.Server.credit(store, channelId, unused, {
261
+ * idempotencyKey: `refund:${flowId}`,
262
+ * })
263
+ * if (result.status === 'declined') await refundAnotherWay()
264
+ * ```
199
265
  */
200
- export declare function charge(store: ChannelStore.ChannelStore, channelId: Hex, amount: bigint): Promise<ChannelStore.State>;
266
+ export declare function credit(store: SessionStoreInput, channelId: Hex, amount: bigint, options: ChannelStore.CreditOptions): Promise<ChannelStore.CreditResult>;
201
267
  //# sourceMappingURL=Session.d.ts.map
@@ -20,17 +20,27 @@ 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, resolveChannelStore, shouldApplyVerifiedHttpAccounting, shouldChargeVoucherOnAcceptance, } from './Settlement.js';
31
32
  import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
32
33
  import { resolveSettlementSchedule, } from './Settlement.js';
34
+ import * as SponsorshipLimit from './SponsorshipLimit.js';
33
35
  import * as Ws from './Ws.js';
36
+ const streamVerifiers = new WeakMap();
37
+ /** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
38
+ export function verifyStream(method, channelId) {
39
+ const verify = streamVerifiers.get(method);
40
+ if (!verify)
41
+ throw new Error('Session stream verifier is not configured');
42
+ return verify(channelId);
43
+ }
34
44
  export { settle, settleBatch } from './Settlement.js';
35
45
  function deriveServerDefaults(values) {
36
46
  // `Method.toServer()` models defaults from request input fields. Tempo session
@@ -191,11 +201,25 @@ async function handleBootstrapPreflight(parameters) {
191
201
  }
192
202
  return new Response(null, { status: 204, headers });
193
203
  }
204
+ /** Binds a reusable session challenge to its route and channel terms but not to its quoted amount. */
205
+ const reusableSessionBinding = (request) => {
206
+ const { methodDetails } = request;
207
+ return {
208
+ chainId: methodDetails.chainId,
209
+ currency: request.currency,
210
+ escrowContract: methodDetails.escrowContract,
211
+ operator: methodDetails.operator,
212
+ recipient: request.recipient,
213
+ sessionProtocol: methodDetails[Constants.MethodDetailKeys.sessionProtocol],
214
+ unitType: request.unitType,
215
+ };
216
+ };
194
217
  /** Creates a server-side TIP20EscrowChannel precompile session payment method. */
195
218
  export function session(p) {
196
219
  const parameters = p;
197
220
  const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = fallbackStore('tempo.session'), suggestedDeposit, unitType, } = parameters;
198
221
  const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
222
+ SponsorshipLimit.assertParameters(parameters.sponsorshipLimit);
199
223
  const onSessionSettlement = parameters.onSessionSettlement;
200
224
  const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
201
225
  const store = ChannelStore.fromStore(rawStore);
@@ -236,8 +260,33 @@ export function session(p) {
236
260
  // A failed scheduled settlement is reported and retried by the next one; the charged request
237
261
  // stays served because the payer's voucher already covers the charge.
238
262
  const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
263
+ // Stream checks have a bounded TTL even when callers disable validation caching.
264
+ // Cache only successful reads; RPC failures must stop delivery.
265
+ const beforeStreamCharge = async (channelId) => {
266
+ channelId = ChannelStore.normalizeChannelId(channelId);
267
+ const channel = await store.getChannel(channelId);
268
+ if (!channel || channel.finalized || channel.closeRequestedAt !== 0n)
269
+ throw new Errors.ChannelClosedError({ reason: 'stream channel is closed' });
270
+ const now = Date.now();
271
+ const ttl = Math.min(Math.max(channelStateTtl, 0), 5_000);
272
+ const lastVerified = lastOnChainVerified.get(channelId);
273
+ if (lastVerified !== undefined && now >= lastVerified && now - lastVerified < ttl)
274
+ return;
275
+ let state;
276
+ try {
277
+ const client = await getClient({ chainId: channel.chainId });
278
+ state = await Chain.getChannelState(client, channelId, channel.escrowContract);
279
+ }
280
+ catch {
281
+ // Transport errors can include private RPC URLs or authentication details.
282
+ throw new Error('Unable to verify stream escrow');
283
+ }
284
+ validateChannelState(state);
285
+ lastOnChainVerified.set(channelId, now);
286
+ };
239
287
  const serveWebSocket = (options) => Ws.serve({
240
288
  ...options,
289
+ beforeCharge: beforeStreamCharge,
241
290
  onChargeCommitted: settleCharged,
242
291
  store,
243
292
  });
@@ -253,9 +302,11 @@ export function session(p) {
253
302
  });
254
303
  const transport = parameters.sse
255
304
  ? Transport.sse({
305
+ ...(typeof parameters.sse === 'object' ? parameters.sse : {}),
306
+ beforeCharge: beforeStreamCharge,
307
+ reusableChallenges: parameters.reusableChallenges,
256
308
  settleCharged,
257
309
  store,
258
- ...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
259
310
  })
260
311
  : undefined;
261
312
  const validateCredential = async ({ credential, operation, request, }) => {
@@ -267,24 +318,31 @@ export function session(p) {
267
318
  minVoucherDelta: parameters.minVoucherDelta,
268
319
  request,
269
320
  });
270
- const details = await validateCredentialPayload({
271
- account,
272
- allowPendingClose: operation === 'broadcast',
273
- challenge: credential.challenge,
274
- channelStateTtl,
275
- chainId: context.chainId,
276
- client: context.client,
277
- credentialSource: credential.source,
278
- escrow: context.escrow,
279
- expectedOperator: context.methodDetails.operator,
280
- feePayer: context.feePayer,
281
- feePayerPolicy: parameters.feePayerPolicy,
282
- feeToken: context.methodDetails.feeToken,
283
- lastOnChainVerified,
284
- minVoucherDelta: context.minVoucherDelta,
285
- payload,
286
- store,
287
- });
321
+ // A broadcast voucher is checked against the stored channel by the update that accepts it.
322
+ const details = operation === 'broadcast' && payload.action === 'voucher'
323
+ ? {
324
+ action: payload.action,
325
+ channelId: ChannelStore.normalizeChannelId(payload.channelId),
326
+ }
327
+ : await validateCredentialPayload({
328
+ account,
329
+ allowPendingClose: operation === 'broadcast',
330
+ challenge: credential.challenge,
331
+ channelStateTtl,
332
+ chainId: context.chainId,
333
+ client: context.client,
334
+ credentialSource: credential.source,
335
+ escrow: context.escrow,
336
+ expectedOperator: context.methodDetails.operator,
337
+ feePayer: context.feePayer,
338
+ feePayerPolicy: parameters.feePayerPolicy,
339
+ feeToken: context.methodDetails.feeToken,
340
+ lastOnChainVerified,
341
+ minVoucherDelta: context.minVoucherDelta,
342
+ payload,
343
+ store,
344
+ voucherStateMaxAge: parameters.voucherStateMaxAge,
345
+ });
288
346
  return {
289
347
  challenge: credential.challenge,
290
348
  credential: { ...credential, payload },
@@ -305,16 +363,46 @@ export function session(p) {
305
363
  minVoucherDelta: parameters.minVoucherDelta,
306
364
  request,
307
365
  });
366
+ const chargeAmount = parameters.chargeOnAcceptance &&
367
+ shouldChargeVoucherOnAcceptance({
368
+ capturedRequest: envelope?.capturedRequest,
369
+ payloadAction: payload.action,
370
+ unitType: context.request.unitType,
371
+ })
372
+ ? BigInt(context.request.amount ?? challenge.request.amount)
373
+ : undefined;
374
+ const acceptance = {};
375
+ const settleAcceptedCharge = (channel) => maybeSettleScheduled({
376
+ account,
377
+ client: context.client,
378
+ ...(context.feePayer ? { feePayer: context.feePayer } : {}),
379
+ feePayerPolicy: parameters.feePayerPolicy,
380
+ feeToken: parameters.feeToken,
381
+ onSessionSettlement,
382
+ onSessionSettlementFailure,
383
+ schedule: settlementSchedule,
384
+ store,
385
+ channel,
386
+ }).catch(ignoreRetryableSettlementFailure);
308
387
  const sessionReceipt = await broadcastCredentialPayload({
309
388
  account,
310
389
  challenge,
390
+ chargeAmount,
391
+ onCharged: (channel) => {
392
+ acceptance.charged = channel;
393
+ },
311
394
  channelStateTtl,
312
395
  chainId: context.chainId,
313
396
  client: context.client,
314
397
  credentialSource: credential.source,
315
398
  escrow: context.escrow,
316
399
  expectedOperator: context.methodDetails.operator,
317
- feePayer: context.feePayer,
400
+ feePayer: await SponsorshipLimit.limitFeePayer({
401
+ chainId: context.chainId,
402
+ feePayer: context.feePayer,
403
+ limit: parameters.sponsorshipLimit,
404
+ payload,
405
+ }),
318
406
  feePayerPolicy: parameters.feePayerPolicy,
319
407
  feeToken: context.methodDetails.feeToken,
320
408
  lastOnChainVerified,
@@ -328,7 +416,16 @@ export function session(p) {
328
416
  sseEnabled: Boolean(parameters.sse),
329
417
  }),
330
418
  store,
419
+ voucherStateMaxAge: parameters.voucherStateMaxAge,
331
420
  });
421
+ if (acceptance.charged) {
422
+ const settlementTxHash = await settleAcceptedCharge(acceptance.charged);
423
+ const receipt = {
424
+ ...sessionReceipt,
425
+ ...(settlementTxHash ? { txHash: settlementTxHash } : {}),
426
+ };
427
+ return parameters.sse ? Transport.markPrepaidSessionTick(receipt) : receipt;
428
+ }
332
429
  return applyVerifiedHttpAccounting({
333
430
  capturedRequest: envelope?.capturedRequest,
334
431
  payloadAction: payload.action,
@@ -337,18 +434,7 @@ export function session(p) {
337
434
  sseEnabled: Boolean(parameters.sse),
338
435
  markPrepaidReceipt: Transport.markPrepaidSessionTick,
339
436
  charge: (channelId, requestAmount) => chargeSessionChannel({ store, channelId, amount: requestAmount }),
340
- settleCharged: (channel) => maybeSettleScheduled({
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),
437
+ settleCharged: settleAcceptedCharge,
352
438
  });
353
439
  };
354
440
  const method = Method.toServer(Methods.session, {
@@ -364,6 +450,7 @@ export function session(p) {
364
450
  unitType,
365
451
  }),
366
452
  extensions: { serveWebSocket, settleScheduled },
453
+ ...(parameters.reusableChallenges ? { stableBinding: reusableSessionBinding } : {}),
367
454
  transport: deriveTransport(transport),
368
455
  preflight: parameters.bootstrap
369
456
  ? async ({ capturedRequest, credential, expires, input, options, realm, secretKey }) => {
@@ -430,6 +517,7 @@ export function session(p) {
430
517
  });
431
518
  },
432
519
  });
520
+ streamVerifiers.set(method, beforeStreamCharge);
433
521
  return method;
434
522
  }
435
523
  (function (session) {
@@ -444,8 +532,40 @@ export function session(p) {
444
532
  *
445
533
  * Delegates to the shared `deductFromChannel` atomic helper and translates
446
534
  * failure modes into typed errors (`InsufficientBalanceError`, `ChannelClosedError`).
535
+ *
536
+ * With `options.idempotencyKey` the channel is charged at most once per key: a retry with the same
537
+ * key and amount returns the channel without charging again, and the same key with a different
538
+ * amount throws `IdempotencyKeyConflictError`.
539
+ *
540
+ * @example
541
+ * ```ts
542
+ * // One durable job per voucher, retried under its operation ID.
543
+ * const channel = await Session.Server.charge(store, channelId, jobCost, {
544
+ * idempotencyKey: operationId,
545
+ * })
546
+ * ```
547
+ */
548
+ export async function charge(store, channelId, amount, options) {
549
+ return chargeSessionChannel({ ...options, store: resolveChannelStore(store), channelId, amount });
550
+ }
551
+ /**
552
+ * Returns an unused amount to a channel's `spent` at most once per idempotency key.
553
+ *
554
+ * The first outcome for a key is final. A credit is `declined` when the channel is closing or
555
+ * finalized, or when it would take `spent` below zero or below what settled, or may still settle,
556
+ * on-chain, because a close captures `max(spent, settled)`; the caller then refunds another way.
557
+ * `unknown` reports a store write that failed and may have committed; retrying with the same key
558
+ * returns the outcome that was recorded.
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const result = await Session.Server.credit(store, channelId, unused, {
563
+ * idempotencyKey: `refund:${flowId}`,
564
+ * })
565
+ * if (result.status === 'declined') await refundAnotherWay()
566
+ * ```
447
567
  */
448
- export async function charge(store, channelId, amount) {
449
- return chargeSessionChannel({ store, channelId, amount });
568
+ export async function credit(store, channelId, amount, options) {
569
+ return ChannelStore.creditChannel(resolveChannelStore(store), channelId, amount, options);
450
570
  }
451
571
  //# sourceMappingURL=Session.js.map
@@ -112,8 +112,8 @@ export declare function resolveSettlementSchedule(schedule: SettlementSchedule |
112
112
  * Computes the schedule progress for an unsettled precompile-backed channel.
113
113
  *
114
114
  * Returns `undefined` for channels that cannot be scheduled: non-precompile
115
- * records, channels without an accepted voucher, or channels with no unsettled
116
- * voucher amount.
115
+ * records, or channels without an accepted voucher above the settled amount and
116
+ * within `spent` (see {@link ChannelStore.resolveSettleableVoucher}).
117
117
  */
118
118
  export declare function resolveSettlementProgress(channel: ChannelStore.State): SettlementProgress | undefined;
119
119
  /** Returns whether the precompile channel has crossed any configured settlement threshold. */
@@ -145,7 +145,7 @@ export type ChargeSessionChannel = (channelId: Hex, amount: bigint) => Promise<C
145
145
  /** Callback used by post-verification accounting to run server-owned settlement policy. */
146
146
  export type SettleChargedSessionChannel = (channel: ChannelStore.State) => Promise<Hex | undefined>;
147
147
  /** Inputs for charging a precompile-backed session channel. */
148
- export type ChargeParameters = {
148
+ export type ChargeParameters = ChannelStore.ChargeOptions & {
149
149
  /** Server-side channel store. */
150
150
  store: ChannelStore.ChannelStore;
151
151
  /** Channel ID to deduct from. */
@@ -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. */
@@ -238,7 +247,12 @@ export declare function getClientAccount(client: {
238
247
  export declare function assertSettlementSender(parameters: SettlementSenderParameters): void;
239
248
  /** Applies automatic settlement when the server-owned schedule is due. */
240
249
  export declare function maybeSettleScheduled(parameters: MaybeSettleScheduledParameters): Promise<Hex | undefined>;
241
- /** Settles the highest accepted voucher for a precompile-backed session channel. */
250
+ /**
251
+ * Settles the largest accepted voucher within the channel's `spent` for a precompile-backed session
252
+ * channel (see {@link ChannelStore.resolveSettleableVoucher}). The escrow captures the voucher's
253
+ * whole cumulative amount, so a voucher above `spent`, such as the highest one after a credit, is
254
+ * never settled. Until the transaction can no longer land, credits keep `spent` at or above it.
255
+ */
242
256
  export declare function settle(store_: SessionStoreInput, client: Chain.TransactionClient, channelId_: Hex, options?: SettlementTransactionOptions): Promise<Hex>;
243
257
  /** Raised when a settlement transaction confirmed but the channel store could not record it. */
244
258
  export declare class SettlementCheckpointError extends Error {