mppx 0.8.12 → 0.8.14

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 (266) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +16 -1
  3. package/dist/Errors.d.ts +10 -0
  4. package/dist/Errors.d.ts.map +1 -1
  5. package/dist/Errors.js +11 -2
  6. package/dist/Errors.js.map +1 -1
  7. package/dist/Method.d.ts +105 -3
  8. package/dist/Method.d.ts.map +1 -1
  9. package/dist/Method.js +102 -2
  10. package/dist/Method.js.map +1 -1
  11. package/dist/cli/account.d.ts +88 -0
  12. package/dist/cli/account.d.ts.map +1 -1
  13. package/dist/cli/account.js +39 -9
  14. package/dist/cli/account.js.map +1 -1
  15. package/dist/cli/cli.d.ts +2 -1
  16. package/dist/cli/cli.d.ts.map +1 -1
  17. package/dist/cli/cli.js +59 -2
  18. package/dist/cli/cli.js.map +1 -1
  19. package/dist/cli/sessions/Manager.d.ts +31 -0
  20. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  21. package/dist/cli/sessions/Manager.js +56 -0
  22. package/dist/cli/sessions/Manager.js.map +1 -0
  23. package/dist/cli/sessions/commands.d.ts +36 -0
  24. package/dist/cli/sessions/commands.d.ts.map +1 -0
  25. package/dist/cli/sessions/commands.js +407 -0
  26. package/dist/cli/sessions/commands.js.map +1 -0
  27. package/dist/cli/sessions/request.d.ts +35 -0
  28. package/dist/cli/sessions/request.d.ts.map +1 -0
  29. package/dist/cli/sessions/request.js +271 -0
  30. package/dist/cli/sessions/request.js.map +1 -0
  31. package/dist/cli/sessions/store.d.ts +134 -0
  32. package/dist/cli/sessions/store.d.ts.map +1 -0
  33. package/dist/cli/sessions/store.js +723 -0
  34. package/dist/cli/sessions/store.js.map +1 -0
  35. package/dist/cli/utils.d.ts +2 -0
  36. package/dist/cli/utils.d.ts.map +1 -1
  37. package/dist/cli/utils.js +12 -0
  38. package/dist/cli/utils.js.map +1 -1
  39. package/dist/cli/validate/index.d.ts.map +1 -1
  40. package/dist/cli/validate/index.js +12 -6
  41. package/dist/cli/validate/index.js.map +1 -1
  42. package/dist/cli/validate/messages.d.ts +2 -0
  43. package/dist/cli/validate/messages.d.ts.map +1 -0
  44. package/dist/cli/validate/messages.js +8 -0
  45. package/dist/cli/validate/messages.js.map +1 -0
  46. package/dist/client/internal/Fetch.d.ts.map +1 -1
  47. package/dist/client/internal/Fetch.js +12 -2
  48. package/dist/client/internal/Fetch.js.map +1 -1
  49. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  50. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  51. package/dist/client/internal/MethodChallenge.js +11 -0
  52. package/dist/client/internal/MethodChallenge.js.map +1 -0
  53. package/dist/client/node.d.ts +29 -0
  54. package/dist/client/node.d.ts.map +1 -0
  55. package/dist/client/node.js +177 -0
  56. package/dist/client/node.js.map +1 -0
  57. package/dist/internal/types.d.ts +24 -0
  58. package/dist/internal/types.d.ts.map +1 -1
  59. package/dist/server/Mppx.d.ts +23 -4
  60. package/dist/server/Mppx.d.ts.map +1 -1
  61. package/dist/server/Mppx.js +91 -82
  62. package/dist/server/Mppx.js.map +1 -1
  63. package/dist/stripe/server/Charge.d.ts +8 -4
  64. package/dist/stripe/server/Charge.d.ts.map +1 -1
  65. package/dist/stripe/server/Charge.js.map +1 -1
  66. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  67. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  68. package/dist/stripe/server/internal/html.gen.js +1 -1
  69. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  70. package/dist/tempo/client/Methods.d.ts +2 -1
  71. package/dist/tempo/client/Methods.d.ts.map +1 -1
  72. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  73. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  74. package/dist/tempo/internal/fee-payer.js +7 -2
  75. package/dist/tempo/internal/fee-payer.js.map +1 -1
  76. package/dist/tempo/internal/types.d.ts +10 -1
  77. package/dist/tempo/internal/types.d.ts.map +1 -1
  78. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  79. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  80. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  81. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  82. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  83. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  84. package/dist/tempo/legacy/server/Session.js.map +1 -1
  85. package/dist/tempo/server/Charge.d.ts +40 -78
  86. package/dist/tempo/server/Charge.d.ts.map +1 -1
  87. package/dist/tempo/server/Charge.js +289 -209
  88. package/dist/tempo/server/Charge.js.map +1 -1
  89. package/dist/tempo/server/Methods.d.ts +13 -23
  90. package/dist/tempo/server/Methods.d.ts.map +1 -1
  91. package/dist/tempo/server/Methods.js +6 -2
  92. package/dist/tempo/server/Methods.js.map +1 -1
  93. package/dist/tempo/server/Relay.d.ts +48 -0
  94. package/dist/tempo/server/Relay.d.ts.map +1 -0
  95. package/dist/tempo/server/Relay.js +177 -0
  96. package/dist/tempo/server/Relay.js.map +1 -0
  97. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  98. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  99. package/dist/tempo/server/SponsorBudget.js +144 -0
  100. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  101. package/dist/tempo/server/Subscription.d.ts +3 -2
  102. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  103. package/dist/tempo/server/Subscription.js.map +1 -1
  104. package/dist/tempo/server/index.d.ts +1 -1
  105. package/dist/tempo/server/index.d.ts.map +1 -1
  106. package/dist/tempo/server/index.js.map +1 -1
  107. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  108. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  109. package/dist/tempo/server/internal/html.gen.js +1 -1
  110. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  111. package/dist/tempo/session/Snapshot.d.ts +9 -0
  112. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  113. package/dist/tempo/session/Snapshot.js +6 -0
  114. package/dist/tempo/session/Snapshot.js.map +1 -1
  115. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  116. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  117. package/dist/tempo/session/client/ChannelOps.js +17 -3
  118. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  119. package/dist/tempo/session/client/CredentialState.d.ts +26 -2
  120. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  121. package/dist/tempo/session/client/CredentialState.js +99 -16
  122. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  123. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  124. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  125. package/dist/tempo/session/client/Runtime.js +17 -0
  126. package/dist/tempo/session/client/Runtime.js.map +1 -1
  127. package/dist/tempo/session/client/Session.d.ts +16 -6
  128. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/session/client/Session.js +93 -36
  130. package/dist/tempo/session/client/Session.js.map +1 -1
  131. package/dist/tempo/session/client/SessionManager.d.ts +10 -3
  132. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/session/client/SessionManager.js +176 -50
  134. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/session/client/Transports.d.ts +18 -7
  136. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  137. package/dist/tempo/session/client/Transports.js +81 -33
  138. package/dist/tempo/session/client/Transports.js.map +1 -1
  139. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  140. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  141. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  142. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  143. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  144. package/dist/tempo/session/precompile/Chain.js +109 -12
  145. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  146. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  147. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  148. package/dist/tempo/session/precompile/Channel.js +4 -0
  149. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  150. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  151. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  152. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  153. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  154. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  155. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  156. package/dist/tempo/session/server/MeteredStream.js +5 -4
  157. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  158. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  159. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  160. package/dist/tempo/session/server/RequestState.js +13 -5
  161. package/dist/tempo/session/server/RequestState.js.map +1 -1
  162. package/dist/tempo/session/server/Session.d.ts +21 -6
  163. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  164. package/dist/tempo/session/server/Session.js +4 -1
  165. package/dist/tempo/session/server/Session.js.map +1 -1
  166. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  167. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  168. package/dist/tempo/session/server/Settlement.js +20 -0
  169. package/dist/tempo/session/server/Settlement.js.map +1 -1
  170. package/dist/tempo/session/server/Ws.d.ts +2 -0
  171. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  172. package/dist/tempo/session/server/Ws.js.map +1 -1
  173. package/dist/tempo/session/server/index.d.ts +1 -1
  174. package/dist/tempo/session/server/index.d.ts.map +1 -1
  175. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  176. package/dist/validation/core.d.ts.map +1 -1
  177. package/dist/validation/core.js +6 -2
  178. package/dist/validation/core.js.map +1 -1
  179. package/dist/viem/Client.d.ts.map +1 -1
  180. package/dist/viem/Client.js +24 -17
  181. package/dist/viem/Client.js.map +1 -1
  182. package/package.json +6 -1
  183. package/src/Errors.test.ts +23 -0
  184. package/src/Errors.ts +21 -2
  185. package/src/Method.test.ts +102 -1
  186. package/src/Method.ts +241 -5
  187. package/src/cli/account.ts +45 -10
  188. package/src/cli/cli.test.ts +165 -70
  189. package/src/cli/cli.ts +67 -2
  190. package/src/cli/mcp.test.ts +11 -0
  191. package/src/cli/sessions/Manager.test.ts +249 -0
  192. package/src/cli/sessions/Manager.ts +93 -0
  193. package/src/cli/sessions/commands.ts +444 -0
  194. package/src/cli/sessions/request.test.ts +51 -0
  195. package/src/cli/sessions/request.ts +353 -0
  196. package/src/cli/sessions/store.test.ts +581 -0
  197. package/src/cli/sessions/store.ts +940 -0
  198. package/src/cli/utils.test.ts +23 -0
  199. package/src/cli/utils.ts +10 -0
  200. package/src/cli/validate/index.ts +12 -14
  201. package/src/cli/validate/messages.ts +7 -0
  202. package/src/cli/validate.test.ts +38 -5
  203. package/src/client/Mppx.test-d.ts +3 -3
  204. package/src/client/internal/Fetch.ts +16 -7
  205. package/src/client/internal/MethodChallenge.ts +30 -0
  206. package/src/client/node.test.ts +115 -0
  207. package/src/client/node.ts +247 -0
  208. package/src/internal/types.test-d.ts +21 -0
  209. package/src/internal/types.ts +33 -0
  210. package/src/server/Mppx.test-d.ts +44 -0
  211. package/src/server/Mppx.test.ts +205 -0
  212. package/src/server/Mppx.ts +167 -103
  213. package/src/stripe/Methods.test.ts +10 -0
  214. package/src/stripe/server/Charge.test-d.ts +66 -0
  215. package/src/stripe/server/Charge.ts +8 -5
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/PublicExports.test-d.ts +20 -0
  218. package/src/tempo/internal/fee-payer.ts +7 -3
  219. package/src/tempo/internal/types.ts +13 -2
  220. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  221. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  222. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  223. package/src/tempo/legacy/server/Session.ts +6 -5
  224. package/src/tempo/server/Charge.test.ts +337 -63
  225. package/src/tempo/server/Charge.ts +440 -270
  226. package/src/tempo/server/Methods.ts +16 -4
  227. package/src/tempo/server/Relay.test.ts +523 -0
  228. package/src/tempo/server/Relay.ts +288 -0
  229. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  230. package/src/tempo/server/SponsorBudget.ts +213 -0
  231. package/src/tempo/server/Subscription.ts +6 -4
  232. package/src/tempo/server/index.ts +5 -1
  233. package/src/tempo/server/internal/html.gen.ts +1 -1
  234. package/src/tempo/session/README.md +201 -0
  235. package/src/tempo/session/Snapshot.ts +21 -0
  236. package/src/tempo/session/client/ChannelOps.test.ts +55 -0
  237. package/src/tempo/session/client/ChannelOps.ts +21 -3
  238. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  239. package/src/tempo/session/client/CredentialState.ts +159 -16
  240. package/src/tempo/session/client/Runtime.test.ts +12 -0
  241. package/src/tempo/session/client/Runtime.ts +22 -0
  242. package/src/tempo/session/client/Session.test.ts +238 -5
  243. package/src/tempo/session/client/Session.ts +130 -41
  244. package/src/tempo/session/client/SessionManager.test.ts +239 -8
  245. package/src/tempo/session/client/SessionManager.ts +204 -54
  246. package/src/tempo/session/client/Transports.test.ts +181 -8
  247. package/src/tempo/session/client/Transports.ts +127 -45
  248. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  249. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  250. package/src/tempo/session/precompile/Chain.test.ts +223 -10
  251. package/src/tempo/session/precompile/Chain.ts +119 -12
  252. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  253. package/src/tempo/session/precompile/Channel.ts +5 -0
  254. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  255. package/src/tempo/session/server/MeteredStream.ts +12 -5
  256. package/src/tempo/session/server/RequestState.test.ts +47 -4
  257. package/src/tempo/session/server/RequestState.ts +15 -8
  258. package/src/tempo/session/server/Session.test.ts +461 -0
  259. package/src/tempo/session/server/Session.ts +31 -8
  260. package/src/tempo/session/server/Settlement.ts +45 -0
  261. package/src/tempo/session/server/Ws.test.ts +113 -0
  262. package/src/tempo/session/server/Ws.ts +2 -0
  263. package/src/tempo/session/server/index.ts +2 -0
  264. package/src/validation/core.ts +7 -1
  265. package/src/viem/Client.test.ts +44 -1
  266. package/src/viem/Client.ts +31 -17
@@ -44,10 +44,16 @@ import {
44
44
  import { respondToSessionCredential } from './RequestState.js'
45
45
  import { applyVerifiedHttpAccounting, chargeSessionChannel } from './Settlement.js'
46
46
  import { maybeSettleScheduled } from './Settlement.js'
47
- import { resolveSettlementSchedule, type SettlementSchedule } from './Settlement.js'
47
+ import {
48
+ resolveSettlementSchedule,
49
+ type OnSessionSettlement,
50
+ type SettlementSchedule,
51
+ } from './Settlement.js'
48
52
 
49
53
  /** Server-side automatic settlement schedule. */
50
54
  export type { SettlementSchedule } from './Settlement.js'
55
+ /** Server-side settlement event hook types. */
56
+ export type { OnSessionSettlement, SessionSettlementContext } from './Settlement.js'
51
57
  /** Server-side hook types for request-identity channel bootstrap. */
52
58
  export type {
53
59
  ResolveSessionChannelId,
@@ -312,6 +318,7 @@ export function session<const parameters extends session.Parameters>(
312
318
  unitType,
313
319
  } = parameters
314
320
  const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals)
321
+ const onSessionSettlement = parameters.onSessionSettlement
315
322
 
316
323
  const store = ChannelStore.fromStore(rawStore)
317
324
  const lastOnChainVerified = new Map<Hex, number>()
@@ -424,6 +431,7 @@ export function session<const parameters extends session.Parameters>(
424
431
  feeToken: parameters.feeToken,
425
432
  lastOnChainVerified,
426
433
  minVoucherDelta: context.minVoucherDelta,
434
+ onSessionSettlement,
427
435
  payload,
428
436
  store,
429
437
  })
@@ -444,6 +452,7 @@ export function session<const parameters extends session.Parameters>(
444
452
  ...(typeof context.feePayer === 'object' ? { feePayer: context.feePayer } : {}),
445
453
  feePayerPolicy: parameters.feePayerPolicy,
446
454
  feeToken: parameters.feeToken,
455
+ onSessionSettlement,
447
456
  schedule: settlementSchedule,
448
457
  store,
449
458
  channel,
@@ -492,9 +501,20 @@ export namespace session {
492
501
  feePayerPolicy?: FeePayerPolicy | undefined
493
502
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
494
503
  minVoucherDelta?: string | undefined
495
- /** Resolve a reusable channel ID from request identity when no credential/request channel ID is present. */
504
+ /**
505
+ * Maps authenticated application identity and payment scope to an existing channel ID.
506
+ * Called only when the request does not already supply a channel ID. MPPx then loads that
507
+ * ID from `store` and validates the channel before including its snapshot in a challenge or
508
+ * bootstrap response. The store is not searched automatically and does not need to implement
509
+ * a secondary-index format.
510
+ */
496
511
  resolveChannelId?: ResolveSessionChannelId | undefined
497
- /** Enables same-route HEAD bootstrap using a zero-amount identity proof. */
512
+ /**
513
+ * Enables same-route `HEAD` recovery before a paid request. The server issues a zero-amount
514
+ * identity challenge, verifies the returned proof, passes its authenticated `source` to
515
+ * `resolveChannelId`, validates the loaded channel's payment scope, and returns a snapshot
516
+ * containing the signed highest voucher for client rehydration.
517
+ */
498
518
  bootstrap?: boolean | undefined
499
519
  /**
500
520
  * Atomic store backend for channel state.
@@ -510,6 +530,8 @@ export namespace session {
510
530
  chainId?: number | undefined
511
531
  /** TIP20EscrowChannel precompile address override. */
512
532
  escrowContract?: Address | undefined
533
+ /** Callback invoked after any on-chain settlement or close transaction is confirmed. */
534
+ onSessionSettlement?: OnSessionSettlement | undefined
513
535
  /** Server-owned automatic settlement cadence. Clients do not receive or control this schedule. */
514
536
  settlementSchedule?: SettlementSchedule | undefined
515
537
 
@@ -522,11 +544,12 @@ export namespace session {
522
544
  /** Defaults derived from `session()` parameters for handler type inference. */
523
545
  export type DeriveDefaults<parameters extends Parameters> = types.DeriveDefaults<
524
546
  parameters,
525
- Defaults
526
- > & {
527
- decimals: number
528
- escrowContract: Address
529
- }
547
+ Defaults,
548
+ {
549
+ currency: string
550
+ decimals: number
551
+ }
552
+ >
530
553
  }
531
554
 
532
555
  /**
@@ -16,6 +16,7 @@ import {
16
16
  InsufficientBalanceError,
17
17
  VerificationFailedError,
18
18
  } from '../../../Errors.js'
19
+ import type { MaybePromise } from '../../../internal/types.js'
19
20
  import type * as Method from '../../../Method.js'
20
21
  import * as Store from '../../../Store.js'
21
22
  import type * as FeePayer from '../../internal/fee-payer.js'
@@ -146,6 +147,23 @@ export type SettlementProgress = {
146
147
  units: number
147
148
  }
148
149
 
150
+ /** Context emitted when an on-chain settlement or close transaction is confirmed. */
151
+ export type SessionSettlementContext = Readonly<{
152
+ /** On-chain transaction hash (or signature on Solana). */
153
+ txHash: Hex
154
+ /** Channel ID that was settled. */
155
+ channelId: Hex
156
+ /** The trigger that caused settlement. */
157
+ trigger: 'settle' | 'close' | 'scheduled'
158
+ /** Cumulative amount settled on-chain to the payee (raw token units). */
159
+ amount: bigint
160
+ /** Incremental amount settled in this transaction (raw token units). */
161
+ delta: bigint
162
+ }>
163
+
164
+ /** Callback invoked after an on-chain settlement or close transaction is confirmed. */
165
+ export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePromise<void>
166
+
149
167
  /** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
150
168
  export type MarkSettlementCompleteParameters = {
151
169
  channelId: ChannelStore.State['channelId']
@@ -340,6 +358,8 @@ export type SettlementTransactionOptions = {
340
358
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
341
359
  /** Optional fee token override for settlement. */
342
360
  feeToken?: Address | undefined
361
+ /** Callback invoked after the settlement transaction is confirmed. */
362
+ onSessionSettlement?: OnSessionSettlement | undefined
343
363
  }
344
364
 
345
365
  /** Inputs for applying a server-owned automatic settlement schedule. */
@@ -356,6 +376,8 @@ export type MaybeSettleScheduledParameters = {
356
376
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
357
377
  /** Optional fee token override for settlement. */
358
378
  feeToken?: Address | undefined
379
+ /** Callback invoked after the scheduled settlement transaction is confirmed. */
380
+ onSessionSettlement?: OnSessionSettlement | undefined
359
381
  /** Resolved server-owned settlement cadence. */
360
382
  schedule: ResolvedSettlementSchedule | undefined
361
383
  /** Server-side channel store. */
@@ -400,6 +422,9 @@ export async function maybeSettleScheduled(
400
422
  ...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
401
423
  ...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
402
424
  ...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
425
+ onSessionSettlement: parameters.onSessionSettlement
426
+ ? (ctx) => parameters.onSessionSettlement!({ ...ctx, trigger: 'scheduled' })
427
+ : undefined,
403
428
  })
404
429
  await markSettlementComplete({ channelId: channel.channelId, store })
405
430
  return txHash
@@ -466,6 +491,15 @@ export async function settle(
466
491
  }
467
492
  : current,
468
493
  )
494
+ if (options?.onSessionSettlement) {
495
+ await emitSessionSettlement(options.onSessionSettlement, {
496
+ txHash,
497
+ channelId,
498
+ trigger: 'settle',
499
+ amount: newSettled,
500
+ delta: newSettled - channel.settledOnChain,
501
+ })
502
+ }
469
503
  return txHash
470
504
  }
471
505
 
@@ -480,3 +514,14 @@ export async function settleBatch(
480
514
  for (const channelId of channelIds) hashes.push(await settle(store, client, channelId, options))
481
515
  return hashes
482
516
  }
517
+
518
+ async function emitSessionSettlement(
519
+ onSessionSettlement: OnSessionSettlement,
520
+ context: SessionSettlementContext,
521
+ ): Promise<void> {
522
+ try {
523
+ await onSessionSettlement(Object.freeze(context))
524
+ } catch {
525
+ // Errors are isolated — observers cannot break the settlement flow.
526
+ }
527
+ }
@@ -408,6 +408,119 @@ describe('isows', () => {
408
408
  expect(channel?.units).toBe(0)
409
409
  })
410
410
 
411
+ test('aborts a blocked application generator before sending close-ready', async () => {
412
+ const socket = new MockSocket()
413
+ const store = memoryChannelStore()
414
+ await seedChannel(store, 1n)
415
+ let generatorAborted = false
416
+
417
+ await Ws.serve({
418
+ socket,
419
+ store,
420
+ url: 'ws://example.test/stream',
421
+ route: async () => ({
422
+ status: 200,
423
+ withReceipt(response = new Response(null, { status: 204 })) {
424
+ response.headers.set(
425
+ 'Payment-Receipt',
426
+ serializeSessionReceipt(
427
+ createSessionReceipt({
428
+ challengeId: challenge.id,
429
+ channelId,
430
+ acceptedCumulative: 1n,
431
+ spent: 0n,
432
+ units: 0,
433
+ }),
434
+ ),
435
+ )
436
+ return response
437
+ },
438
+ }),
439
+ generate: async function* (stream) {
440
+ await new Promise<void>((resolve) => {
441
+ stream.signal.addEventListener('abort', () => resolve(), { once: true })
442
+ })
443
+ generatorAborted = true
444
+ yield* []
445
+ },
446
+ })
447
+
448
+ socket.receive(
449
+ Ws.formatAuthorizationMessage(
450
+ makeCredential({
451
+ action: 'open',
452
+ channelId,
453
+ cumulativeAmount: '1',
454
+ signature: `0x${'77'.repeat(65)}`,
455
+ transaction: '0x01',
456
+ type: 'transaction',
457
+ }),
458
+ ),
459
+ )
460
+ await sleep(10)
461
+ socket.receive(Ws.formatCloseRequestMessage())
462
+ await sleep(10)
463
+
464
+ expect(generatorAborted).toBe(true)
465
+ expect(
466
+ socket.sent
467
+ .map((message) => Ws.parseMessage(message))
468
+ .some((message) => message?.mpp === 'payment-close-ready'),
469
+ ).toBe(true)
470
+ })
471
+
472
+ test('meters an explicit per-message amount independently of the challenge tick cost', async () => {
473
+ const socket = new MockSocket()
474
+ const store = memoryChannelStore()
475
+ await seedChannel(store, 25n)
476
+
477
+ await Ws.serve({
478
+ socket,
479
+ store,
480
+ url: 'ws://example.test/stream',
481
+ route: async () => ({
482
+ status: 200,
483
+ withReceipt(response = new Response(null, { status: 204 })) {
484
+ response.headers.set(
485
+ 'Payment-Receipt',
486
+ serializeSessionReceipt(
487
+ createSessionReceipt({
488
+ challengeId: challenge.id,
489
+ channelId,
490
+ acceptedCumulative: 25n,
491
+ spent: 0n,
492
+ units: 0,
493
+ }),
494
+ ),
495
+ )
496
+ return response
497
+ },
498
+ }),
499
+ generate: async function* (stream) {
500
+ await stream.charge(7n)
501
+ yield 'priced-response'
502
+ },
503
+ })
504
+
505
+ socket.receive(
506
+ Ws.formatAuthorizationMessage(
507
+ makeCredential({
508
+ action: 'open',
509
+ channelId,
510
+ cumulativeAmount: '25',
511
+ signature: `0x${'77'.repeat(65)}`,
512
+ transaction: '0x01',
513
+ type: 'transaction',
514
+ }),
515
+ ),
516
+ )
517
+
518
+ await sleep(10)
519
+
520
+ expect(await store.getChannel(channelId)).toMatchObject({ spent: 7n, units: 1 })
521
+ expect(socket.sent.some((message) => message.includes('priced-response'))).toBe(true)
522
+ })
523
+
411
524
  test('does not meter or emit application messages after close is requested on-chain', async () => {
412
525
  const socket = new MockSocket()
413
526
  const store = memoryChannelStore()
@@ -322,6 +322,8 @@ export declare namespace serve {
322
322
  * multiple offers — otherwise a client can select the cheapest offer
323
323
  * and still receive the same stream. */
324
324
  amount?: string | undefined
325
+ /** Application stream. A manual stream can call `charge(amount)` with a
326
+ * per-message raw-unit amount; omitting it uses the challenge tick cost. */
325
327
  generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>)
326
328
  pollIntervalMs?: number | undefined
327
329
  /** Payment route handler. Receives synthetic `POST` requests with only
@@ -3,8 +3,10 @@ export { charge, session, settle, settleBatch } from './Session.js'
3
3
  export * as Sse from './Sse.js'
4
4
  /** Server-side automatic settlement schedule. */
5
5
  export type {
6
+ OnSessionSettlement,
6
7
  ResolveSessionChannelId,
7
8
  ResolveSessionChannelIdParameters,
8
9
  SessionChannelIdRequest,
10
+ SessionSettlementContext,
9
11
  SettlementSchedule,
10
12
  } from './Session.js'
@@ -14,6 +14,7 @@ import {
14
14
  parseEndpointArg,
15
15
  resolveBodyForEndpoint,
16
16
  } from '../cli/validate/helpers.js'
17
+ import { missingDiscoverySuggestion } from '../cli/validate/messages.js'
17
18
  import { validatePaymentFlow } from '../cli/validate/payment.js'
18
19
  import { validate as validateDiscoveryDoc } from '../discovery/Validate.js'
19
20
 
@@ -237,7 +238,7 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
237
238
 
238
239
  if (!discovery) throw new Error('Discovery phase did not complete')
239
240
 
240
- const suggestions = computeSuggestions(endpointResults, { sawTestnet, sawMainnet })
241
+ const suggestions = computeSuggestions(endpointResults, discovery, { sawTestnet, sawMainnet })
241
242
 
242
243
  return {
243
244
  url: baseUrl,
@@ -250,11 +251,16 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
250
251
 
251
252
  function computeSuggestions(
252
253
  endpoints: EndpointValidationResult[],
254
+ discovery: DiscoveryResult,
253
255
  flags: { sawTestnet: boolean; sawMainnet: boolean },
254
256
  ): string[] {
255
257
  const steps: string[] = []
256
258
  const allResults = endpoints.flatMap((ep) => [...ep.challenge, ...ep.payment])
257
259
 
260
+ if (!discovery.found) {
261
+ steps.push(missingDiscoverySuggestion)
262
+ }
263
+
258
264
  const sawCryptoMainnet = flags.sawMainnet
259
265
  const sawCryptoTestnet = flags.sawTestnet
260
266
  const sawStripeLive = allResults.some(
@@ -3,12 +3,16 @@ import { privateKeyToAccount } from 'viem/accounts'
3
3
  import { signTransaction } from 'viem/actions'
4
4
  import { Account as TempoAccount, Transaction } from 'viem/tempo'
5
5
  import { tempoLocalnet } from 'viem/tempo/chains'
6
- import { describe, expect, test } from 'vp/test'
6
+ import { afterEach, describe, expect, test, vi } from 'vp/test'
7
7
 
8
8
  import * as Client from './Client.js'
9
9
 
10
10
  const rpcUrl = { 42: 'https://rpc.example.com', 99: 'https://rpc2.example.com' } as const
11
11
 
12
+ afterEach(() => {
13
+ vi.unstubAllGlobals()
14
+ })
15
+
12
16
  describe('getResolver', () => {
13
17
  test('behavior: creates client from rpcUrl for given chainId', async () => {
14
18
  const getClient = Client.getResolver({
@@ -61,6 +65,45 @@ describe('getResolver', () => {
61
65
  `[Error: No \`rpcUrl\` configured for \`chainId\` (99).]`,
62
66
  )
63
67
  })
68
+
69
+ test('behavior: wraps custom client transports with a hosted fee payer', async () => {
70
+ const defaultRequests: string[] = []
71
+ const client = createClient({
72
+ chain: tempoLocalnet,
73
+ transport: custom({
74
+ async request({ method }) {
75
+ defaultRequests.push(method)
76
+ return { source: 'default' }
77
+ },
78
+ }),
79
+ })
80
+ const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
81
+ const request = JSON.parse(String(init?.body)) as { id: number; jsonrpc: string }
82
+ return Response.json({
83
+ id: request.id,
84
+ jsonrpc: request.jsonrpc,
85
+ result: { source: 'relay' },
86
+ })
87
+ })
88
+ vi.stubGlobal('fetch', fetchMock)
89
+
90
+ const getClient = Client.getResolver({
91
+ chain: tempoLocalnet,
92
+ feePayerUrl: 'https://sponsor.example',
93
+ getClient: () => client,
94
+ rpcUrl: { [tempoLocalnet.id]: 'https://rpc.example.com' },
95
+ })
96
+ const resolved = await getClient({ chainId: tempoLocalnet.id })
97
+ const result = await resolved.request({
98
+ method: 'eth_fillTransaction',
99
+ params: [{ feePayer: true }],
100
+ } as never)
101
+
102
+ expect(result).toEqual({ source: 'relay' })
103
+ expect(fetchMock).toHaveBeenCalledOnce()
104
+ expect(String(fetchMock.mock.calls[0]?.[0])).toBe('https://sponsor.example/')
105
+ expect(defaultRequests).toEqual([])
106
+ })
64
107
  })
65
108
 
66
109
  // ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
1
- import { type Chain, type Client, createClient, http } from 'viem'
1
+ import { type Chain, type Client, createClient, createTransport, custom, http } from 'viem'
2
2
  import { withFeePayer } from 'viem/tempo'
3
3
 
4
4
  import type { MaybePromise } from '../internal/types.js'
@@ -23,31 +23,45 @@ export function getResolver(
23
23
  if (!chain?.serializers && !feePayerUrl) return getClient
24
24
  return async (params) => {
25
25
  const client = await getClient(params)
26
+ let resolvedClient = client
26
27
 
27
28
  // Wrap the client's transport with `withFeePayer` when a fee payer URL is provided.
28
29
  if (feePayerUrl && client.transport.key !== 'feePayer') {
29
- const url = (client.transport as { url?: string }).url
30
- if (url) {
31
- const wrapped = createClient({
32
- chain: client.chain,
33
- transport: withFeePayer(http(url), http(feePayerUrl)),
34
- })
35
- Object.assign(client, { transport: wrapped.transport, request: wrapped.request })
36
- }
30
+ const request = client.request.bind(client)
31
+ // The supplied client already owns retries. Keep the relay middleware retry-free so
32
+ // failures are not retried once per nested transport layer.
33
+ const feePayerTransport = withFeePayer(
34
+ custom({ request: (args) => request(args as never) }, { retryCount: 0 }),
35
+ http(feePayerUrl, { retryCount: client.transport.retryCount }),
36
+ )({
37
+ account: client.account,
38
+ chain: client.chain,
39
+ pollingInterval: client.pollingInterval,
40
+ retryCount: 0,
41
+ })
42
+ const wrapped = createTransport(
43
+ { ...feePayerTransport.config, retryCount: 0 },
44
+ feePayerTransport.value,
45
+ )
46
+ resolvedClient = Object.assign({}, client, {
47
+ request: wrapped.request,
48
+ transport: { ...wrapped.config, ...wrapped.value },
49
+ })
37
50
  }
38
51
 
39
- if (!chain?.serializers || client.chain?.serializers?.transaction) return client
40
- return Object.assign({}, client, {
52
+ if (!chain?.serializers || resolvedClient.chain?.serializers?.transaction)
53
+ return resolvedClient
54
+ return Object.assign({}, resolvedClient, {
41
55
  chain: {
42
56
  ...chain,
43
- ...client.chain,
44
- formatters: client.chain?.formatters ?? chain.formatters,
57
+ ...resolvedClient.chain,
58
+ formatters: resolvedClient.chain?.formatters ?? chain.formatters,
45
59
  prepareTransactionRequest:
46
- client.chain?.prepareTransactionRequest ?? chain.prepareTransactionRequest,
47
- serializers: client.chain?.serializers?.transaction
48
- ? client.chain.serializers
60
+ resolvedClient.chain?.prepareTransactionRequest ?? chain.prepareTransactionRequest,
61
+ serializers: resolvedClient.chain?.serializers?.transaction
62
+ ? resolvedClient.chain.serializers
49
63
  : chain.serializers,
50
- } as typeof client.chain,
64
+ } as typeof resolvedClient.chain,
51
65
  })
52
66
  }
53
67
  }