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
@@ -67,15 +67,14 @@ export function resolveSettlementSchedule(schedule, decimals) {
67
67
  * Computes the schedule progress for an unsettled precompile-backed channel.
68
68
  *
69
69
  * Returns `undefined` for channels that cannot be scheduled: non-precompile
70
- * records, channels without an accepted voucher, or channels with no unsettled
71
- * voucher amount.
70
+ * records, or channels without an accepted voucher above the settled amount and
71
+ * within `spent` (see {@link ChannelStore.resolveSettleableVoucher}).
72
72
  */
73
73
  export function resolveSettlementProgress(channel) {
74
74
  if (!ChannelStore.isPrecompileState(channel))
75
75
  return undefined;
76
- if (!channel.highestVoucher)
77
- return undefined;
78
- if (channel.highestVoucher.cumulativeAmount <= channel.settledOnChain)
76
+ // A settlement captures a voucher whole, so only a voucher within spend can be scheduled.
77
+ if (!ChannelStore.resolveSettleableVoucher(channel))
79
78
  return undefined;
80
79
  const amountBoundary = channel.lastSettlementSpent ?? channel.settledOnChain;
81
80
  const timestampBoundary = Date.parse(channel.lastSettlementAt ?? channel.createdAt);
@@ -171,6 +170,18 @@ export function shouldApplyVerifiedHttpAccounting(parameters) {
171
170
  return false;
172
171
  return isSessionContentRequest(capturedRequest);
173
172
  }
173
+ /**
174
+ * Returns whether a voucher pays for its request in the store update that accepts it. A
175
+ * request-priced content request is charged exactly once, so this is the charge the HTTP
176
+ * accounting or the streamed response would make, without a second store update; the receipt is
177
+ * then marked prepaid so an SSE response does not charge again.
178
+ */
179
+ export function shouldChargeVoucherOnAcceptance(parameters) {
180
+ const { capturedRequest, payloadAction, unitType } = parameters;
181
+ if (!capturedRequest || payloadAction !== 'voucher' || unitType !== 'request')
182
+ return false;
183
+ return isSessionContentRequest(capturedRequest);
184
+ }
174
185
  /** Applies the default HTTP content charge after a session credential has been accepted. */
175
186
  export async function applyVerifiedHttpAccounting(parameters) {
176
187
  const { receipt, sseEnabled } = parameters;
@@ -191,12 +202,18 @@ export async function applyVerifiedHttpAccounting(parameters) {
191
202
  }
192
203
  /** Atomically deducts spend from a channel and maps store failures to typed session errors. */
193
204
  export async function chargeSessionChannel(parameters) {
194
- const { store, channelId, amount } = parameters;
205
+ const { store, channelId, amount, idempotencyKey, retention } = parameters;
206
+ ChannelStore.assertIdempotencyOptions({ idempotencyKey, retention });
195
207
  let result;
196
208
  try {
197
- result = await ChannelStore.deductFromChannel(store, channelId, amount);
209
+ result = await ChannelStore.deductFromChannel(store, channelId, amount, {
210
+ idempotencyKey,
211
+ retention,
212
+ });
198
213
  }
199
- catch {
214
+ catch (error) {
215
+ if (error instanceof ChannelStore.IdempotencyKeyConflictError)
216
+ throw error;
200
217
  throw new ChannelClosedError({ reason: 'channel not found' });
201
218
  }
202
219
  if (!result.ok) {
@@ -286,7 +303,12 @@ export async function maybeSettleScheduled(parameters) {
286
303
  await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store }).catch(release);
287
304
  return txHash;
288
305
  }
289
- /** Settles the highest accepted voucher for a precompile-backed session channel. */
306
+ /**
307
+ * Settles the largest accepted voucher within the channel's `spent` for a precompile-backed session
308
+ * channel (see {@link ChannelStore.resolveSettleableVoucher}). The escrow captures the voucher's
309
+ * whole cumulative amount, so a voucher above `spent`, such as the highest one after a credit, is
310
+ * never settled. Until the transaction can no longer land, credits keep `spent` at or above it.
311
+ */
290
312
  export async function settle(store_, client, channelId_, options) {
291
313
  const store = resolveChannelStore(store_);
292
314
  const channelId = ChannelStore.normalizeChannelId(channelId_);
@@ -306,20 +328,72 @@ export async function settle(store_, client, channelId_, options) {
306
328
  payee: channel.payee,
307
329
  sender: account?.address,
308
330
  });
309
- const amount = uint96(channel.highestVoucher.cumulativeAmount);
310
- const { signature } = channel.highestVoucher;
311
- // A replica behind the open or top-up that admitted this voucher rejects the settle while
312
- // preparing it (e.g. `ChannelNotFound`); nothing was broadcast, so retry until it catches up.
313
- const txHash = await Chain.readbackWithRetry(() => Chain.settleOnChain(client, channel.descriptor, amount, signature, escrow, account
314
- ? {
315
- account,
316
- ...(options?.feePayer ? { feePayer: options.feePayer } : {}),
317
- ...(options?.feePayerPolicy ? { feePayerPolicy: options.feePayerPolicy } : {}),
318
- ...(options?.feeToken ? { feeToken: options.feeToken } : {}),
319
- candidateFeeTokens: options?.candidateFeeTokens ?? [channel.token],
320
- }
321
- : undefined), { shouldRetry: isExecutionRevert });
322
- const receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
331
+ // Pin the transaction's expiry so the credit floor below can expire once it can no longer land.
332
+ const validBefore = Math.floor(Date.now() / 1000) + ChannelStore.pendingCloseValiditySeconds;
333
+ const pending = {
334
+ amount: 0n,
335
+ expiresAt: (validBefore + ChannelStore.pendingCloseClockSkewSeconds) * 1000,
336
+ };
337
+ const claim = {};
338
+ // Choosing the voucher and raising the credit floor in one update keeps a concurrent credit from
339
+ // lowering spent below what this transaction captures.
340
+ await store.updateChannel(channelId, (current) => {
341
+ claim.channel = current ?? undefined;
342
+ claim.voucher = (current && ChannelStore.resolveSettleableVoucher(current)) ?? undefined;
343
+ if (!current || !claim.voucher)
344
+ return current;
345
+ pending.amount = claim.voucher.cumulativeAmount;
346
+ const live = current.pendingSettlement && current.pendingSettlement.expiresAt > Date.now()
347
+ ? current.pendingSettlement
348
+ : undefined;
349
+ return {
350
+ ...current,
351
+ pendingSettlement: {
352
+ amount: ChannelStore.keepGreater(live?.amount ?? 0n, pending.amount),
353
+ expiresAt: Math.max(live?.expiresAt ?? 0, pending.expiresAt),
354
+ },
355
+ };
356
+ });
357
+ const { voucher } = claim;
358
+ if (!voucher || !claim.channel)
359
+ throw new VerificationFailedError({
360
+ reason: 'nothing to settle: no accepted voucher above the settled amount is within spent',
361
+ });
362
+ const claimed = claim.channel;
363
+ const amount = uint96(voucher.cumulativeAmount);
364
+ const { signature } = voucher;
365
+ // A revert proves the transaction will not land, so its credit floor can go at once.
366
+ const release = () => store
367
+ .updateChannel(channelId, (current) => {
368
+ if (current?.pendingSettlement?.amount !== pending.amount ||
369
+ current.pendingSettlement.expiresAt !== pending.expiresAt)
370
+ return current;
371
+ const { pendingSettlement: _, ...rest } = current;
372
+ return rest;
373
+ })
374
+ .catch(() => undefined);
375
+ let txHash;
376
+ let receipt;
377
+ try {
378
+ // A replica behind the open or top-up that admitted this voucher rejects the settle while
379
+ // preparing it (e.g. `ChannelNotFound`); nothing was broadcast, so retry until it catches up.
380
+ txHash = await Chain.readbackWithRetry(() => Chain.settleOnChain(client, channel.descriptor, amount, signature, escrow, account
381
+ ? {
382
+ account,
383
+ ...(options?.feePayer ? { feePayer: options.feePayer } : {}),
384
+ ...(options?.feePayerPolicy ? { feePayerPolicy: options.feePayerPolicy } : {}),
385
+ ...(options?.feeToken ? { feeToken: options.feeToken } : {}),
386
+ candidateFeeTokens: options?.candidateFeeTokens ?? [channel.token],
387
+ validBefore,
388
+ }
389
+ : { validBefore }), { shouldRetry: isExecutionRevert });
390
+ receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
391
+ }
392
+ catch (error) {
393
+ if (isExecutionRevert(error) || error instanceof VerificationFailedError)
394
+ await release();
395
+ throw error;
396
+ }
323
397
  const settled = readSettledReceiptFields(Chain.getChannelEvent(receipt, 'Settled', channelId));
324
398
  const { deltaPaid, newSettled } = settled;
325
399
  if (newSettled < amount)
@@ -332,16 +406,29 @@ export async function settle(store_, client, channelId_, options) {
332
406
  });
333
407
  let checkpointError;
334
408
  await store
335
- .updateChannel(channelId, (current) => current
336
- ? {
337
- ...current,
338
- settledOnChain: newSettled > current.settledOnChain ? newSettled : current.settledOnChain,
409
+ .updateChannel(channelId, (current) => {
410
+ if (!current)
411
+ return current;
412
+ const settledOnChain = ChannelStore.keepGreater(current.settledOnChain, newSettled);
413
+ const { pendingSettlement, ...rest } = current;
414
+ return {
415
+ ...rest,
416
+ // The recorded settlement now keeps spent at or above what this transaction captured.
417
+ ...(pendingSettlement && pendingSettlement.amount > settledOnChain
418
+ ? { pendingSettlement }
419
+ : {}),
420
+ ...(current.retainedVouchers
421
+ ? {
422
+ retainedVouchers: current.retainedVouchers.filter((retained) => retained.cumulativeAmount > settledOnChain),
423
+ }
424
+ : {}),
425
+ settledOnChain,
339
426
  lastSettlementAt: new Date().toISOString(),
340
- // Charges accepted after the voucher was read are not covered by this transaction.
341
- lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n, channel.spent),
342
- lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0, channel.units),
343
- }
344
- : current)
427
+ // Charges accepted after the voucher was chosen are not covered by this transaction.
428
+ lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n, amount),
429
+ lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0, claimed.units),
430
+ };
431
+ })
345
432
  .catch((cause) => {
346
433
  checkpointError = { cause };
347
434
  });
@@ -0,0 +1,52 @@
1
+ import { Address } from 'ox';
2
+ import type * as Store from '../../../Store.js';
3
+ import type { SessionCredentialPayload } from '../precompile/Protocol.js';
4
+ /**
5
+ * Sponsored opens, and separately sponsored top-ups, each verified payer may make per UTC day. It is
6
+ * tempoxyz/mpp-proxy's `DAILY_CHANNEL_OPEN_LIMIT`, the limit its proxy applied to sponsored channel
7
+ * opens before the limit moved into mppx.
8
+ */
9
+ export declare const defaultPerDay = 5;
10
+ /** Limits on fee-payer-sponsored channel opens and top-ups. */
11
+ export type Parameters = {
12
+ /** Atomic store holding one usage record per payer and chain. */
13
+ store: Store.AtomicStore;
14
+ /** Sponsored opens, and separately sponsored top-ups, per payer per UTC day. @default 5 */
15
+ perDay?: number | undefined;
16
+ };
17
+ /** Validates a configured limit. */
18
+ export declare function assertParameters(parameters: Parameters | undefined): void;
19
+ /**
20
+ * Returns the verified signer of a serialized Tempo transaction, lowercased, or null. A
21
+ * credential's `source` is client-supplied and unverified at this point, and so are the 0x78
22
+ * explicit sender slot and a keychain signature's account, so limits key on what the signatures
23
+ * prove:
24
+ * - a direct signature: its signer;
25
+ * - an access key authorized in the same transaction: the account that signed the authorization,
26
+ * so fresh keys do not get a fresh quota;
27
+ * - an access key authorized earlier: the key itself (its account link is only checked on chain).
28
+ */
29
+ export declare function verifiedPayer(serializedTransaction: string): Address.Address | null;
30
+ /**
31
+ * Records one sponsored `action` for `payer` on the current UTC day. Returns false, recording
32
+ * nothing, once the payer has used `perDay` of them.
33
+ */
34
+ export declare function reserve(store: Store.AtomicStore, parameters: {
35
+ action: 'open' | 'topUp';
36
+ chainId: number;
37
+ payer: string;
38
+ perDay?: number | undefined;
39
+ now?: Date | undefined;
40
+ }): Promise<boolean>;
41
+ /**
42
+ * Returns `feePayer` when the credential's open or top-up may be sponsored, and `undefined` once
43
+ * its verified payer used the day's sponsored transactions, or when no signature proves a payer.
44
+ * The transaction is then broadcast as the payer signed it, paying its own fees.
45
+ */
46
+ export declare function limitFeePayer<feePayer>(parameters: {
47
+ chainId: number;
48
+ feePayer: feePayer | undefined;
49
+ limit: Parameters | undefined;
50
+ payload: SessionCredentialPayload;
51
+ }): Promise<feePayer | undefined>;
52
+ //# sourceMappingURL=SponsorshipLimit.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { Address } from 'ox';
2
+ import { KeyAuthorization, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo';
3
+ /**
4
+ * Sponsored opens, and separately sponsored top-ups, each verified payer may make per UTC day. It is
5
+ * tempoxyz/mpp-proxy's `DAILY_CHANNEL_OPEN_LIMIT`, the limit its proxy applied to sponsored channel
6
+ * opens before the limit moved into mppx.
7
+ */
8
+ export const defaultPerDay = 5;
9
+ /** Validates a configured limit. */
10
+ export function assertParameters(parameters) {
11
+ const perDay = parameters?.perDay;
12
+ if (perDay !== undefined && (!Number.isSafeInteger(perDay) || perDay < 0))
13
+ throw new Error('`sponsorshipLimit.perDay` must be a non-negative integer.');
14
+ }
15
+ /**
16
+ * The address that provably produced `signature` over `payload`, or null. secp256k1 recovery is
17
+ * inherently verified; P256 and WebAuthn envelopes carry a public key, so their signature is
18
+ * checked against it. Multisig has no single signer.
19
+ */
20
+ function verifiedSigner(payload, signature) {
21
+ if (signature.type === 'secp256k1')
22
+ return SignatureEnvelope.extractAddress({ payload, signature });
23
+ if (signature.type === 'p256' || signature.type === 'webAuthn') {
24
+ const { publicKey } = signature;
25
+ return SignatureEnvelope.verify(signature, { payload, publicKey })
26
+ ? Address.fromPublicKey(publicKey)
27
+ : null;
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * Returns the verified signer of a serialized Tempo transaction, lowercased, or null. A
33
+ * credential's `source` is client-supplied and unverified at this point, and so are the 0x78
34
+ * explicit sender slot and a keychain signature's account, so limits key on what the signatures
35
+ * prove:
36
+ * - a direct signature: its signer;
37
+ * - an access key authorized in the same transaction: the account that signed the authorization,
38
+ * so fresh keys do not get a fresh quota;
39
+ * - an access key authorized earlier: the key itself (its account link is only checked on chain).
40
+ */
41
+ export function verifiedPayer(serializedTransaction) {
42
+ if (!/^0x7[68][0-9a-fA-F]+$/.test(serializedTransaction))
43
+ return null;
44
+ try {
45
+ const { from: _claimedSender, ...fields } = TxEnvelopeTempo.deserialize(serializedTransaction);
46
+ const { signature } = fields;
47
+ if (!signature)
48
+ return null;
49
+ const envelope = TxEnvelopeTempo.from(fields);
50
+ if (signature.type !== 'keychain')
51
+ return lower(verifiedSigner(TxEnvelopeTempo.getSignPayload(envelope), signature));
52
+ // Keychain V2 signs over the account too: keccak256(0x04 || sigHash || userAddress).
53
+ const accessKey = verifiedSigner(TxEnvelopeTempo.getSignPayload(envelope, signature.version === 'v2' ? { from: signature.userAddress } : {}), signature.inner);
54
+ if (!accessKey)
55
+ return null;
56
+ const authorization = fields.keyAuthorization;
57
+ if (!authorization)
58
+ return lower(accessKey);
59
+ if (!authorization.signature || !Address.isEqual(authorization.address, accessKey))
60
+ return null;
61
+ return lower(verifiedSigner(KeyAuthorization.getSignPayload(authorization), authorization.signature));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function lower(address) {
68
+ return address ? address.toLowerCase() : null;
69
+ }
70
+ /**
71
+ * Records one sponsored `action` for `payer` on the current UTC day. Returns false, recording
72
+ * nothing, once the payer has used `perDay` of them.
73
+ */
74
+ export async function reserve(store, parameters) {
75
+ const { action, chainId, payer, perDay = defaultPerDay } = parameters;
76
+ const day = (parameters.now ?? new Date()).toISOString().slice(0, 10);
77
+ const key = `mppx:session:sponsorship:${chainId}:${payer.toLowerCase()}`;
78
+ return store.update(key, (current) => {
79
+ const usage = current?.day === day ? current : { day, open: 0, topUp: 0 };
80
+ if (usage[action] >= perDay)
81
+ return { op: 'noop', result: false };
82
+ return { op: 'set', value: { ...usage, [action]: usage[action] + 1 }, result: true };
83
+ });
84
+ }
85
+ /**
86
+ * Returns `feePayer` when the credential's open or top-up may be sponsored, and `undefined` once
87
+ * its verified payer used the day's sponsored transactions, or when no signature proves a payer.
88
+ * The transaction is then broadcast as the payer signed it, paying its own fees.
89
+ */
90
+ export async function limitFeePayer(parameters) {
91
+ const { chainId, feePayer, limit, payload } = parameters;
92
+ if (!feePayer || !limit)
93
+ return feePayer;
94
+ if (payload.action !== 'open' && payload.action !== 'topUp')
95
+ return feePayer;
96
+ const payer = verifiedPayer(payload.transaction);
97
+ if (!payer)
98
+ return undefined;
99
+ const allowed = await reserve(limit.store, {
100
+ action: payload.action,
101
+ chainId,
102
+ payer,
103
+ perDay: limit.perDay,
104
+ });
105
+ return allowed ? feePayer : undefined;
106
+ }
107
+ //# sourceMappingURL=SponsorshipLimit.js.map
@@ -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 the reserved charge immediately before the chunk is emitted. If a
31
- * manual generator completes after charging without yielding another chunk,
32
- * commits that terminal charge on successful completion.
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 the reserved charge immediately before the chunk is emitted. If a
30
- * manual generator completes after charging without yielding another chunk,
31
- * commits that terminal charge on successful completion.
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
- /** Optional low-level hook invoked after each committed stream charge. */
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,
@@ -1,4 +1,12 @@
1
- export { charge, session, settle, settleBatch } from './Session.js';
1
+ export { charge, credit, session, settle, settleBatch } from './Session.js';
2
+ /** Close supervision for payer close requests on idle channels. */
3
+ export { superviseCloses } from './CloseSupervisor.js';
4
+ export type { CloseSupervision, CloseSupervisionOutcome, SuperviseClosesParameters, } from './CloseSupervisor.js';
5
+ /** Idempotent charge and credit options and outcomes, and the voucher a settlement captures. */
6
+ export { IdempotencyKeyConflictError, resolveSettleableVoucher } from './ChannelStore.js';
7
+ export type { ChargeOptions, CreditDeclineReason, CreditOptions, CreditResult, State as ChannelState, } from './ChannelStore.js';
8
+ /** Limits on fee-payer-sponsored channel opens and top-ups. */
9
+ export type { Parameters as SponsorshipLimitParameters } from './SponsorshipLimit.js';
2
10
  /** SSE helpers and types for Tempo session streams. */
3
11
  export * as Sse from './Sse.js';
4
12
  /** Server-side automatic settlement schedule. */
@@ -1,4 +1,8 @@
1
- export { charge, session, settle, settleBatch } from './Session.js';
1
+ export { charge, credit, session, settle, settleBatch } from './Session.js';
2
+ /** Close supervision for payer close requests on idle channels. */
3
+ export { superviseCloses } from './CloseSupervisor.js';
4
+ /** Idempotent charge and credit options and outcomes, and the voucher a settlement captures. */
5
+ export { IdempotencyKeyConflictError, resolveSettleableVoucher } from './ChannelStore.js';
2
6
  /** SSE helpers and types for Tempo session streams. */
3
7
  export * as Sse from './Sse.js';
4
8
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.13.5",
4
+ "version": "0.14.0",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/wevm/mppx#readme",