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
@@ -0,0 +1,201 @@
1
+ # Tempo Sessions design
2
+
3
+ Tempo Sessions implements TIP-1034 payment channels for repeated HTTP and
4
+ streaming payments. A session is a channel, not an application login session.
5
+
6
+ A payer authorizes a cumulative amount with a signed voucher. The server
7
+ accepts that authorization, records delivered spend, and settles on-chain under
8
+ server-owned policy.
9
+
10
+ ## Design principles
11
+
12
+ 1. A channel has distinct on-chain, server, and client state. No copy replaces
13
+ another authority.
14
+ 2. Vouchers authorize value; server accounting records delivered value; chain
15
+ settlement captures value. These are separate operations.
16
+ 3. The server atomically accepts vouchers and records charges. Multiple server
17
+ instances share one linearizable channel store.
18
+ 4. Client persistence and server snapshots accelerate recovery. Neither is
19
+ proof of a reusable channel until cryptographic and chain validation pass.
20
+ 5. Management credentials never invoke application content handlers.
21
+ 6. Settlement policy belongs to the server. A client authorizes value but does
22
+ not choose when it is settled.
23
+
24
+ ```mermaid
25
+ flowchart LR
26
+ client["Client"]
27
+ cache["Client ChannelStore\nrecovery cache"]
28
+ server["Server session method\nverification and accounting"]
29
+ ledger["Server AtomicStore\naccepted vouchers and spend"]
30
+ chain["Tempo escrow precompile\ndeposit, settlement, close state"]
31
+
32
+ client <--> cache
33
+ client -->|"transactions and vouchers"| server
34
+ client -->|"channel state reads"| chain
35
+ server <--> ledger
36
+ server -->|"validates, funds, settles"| chain
37
+ ```
38
+
39
+ ## Authority model
40
+
41
+ | State | Meaning | Authority |
42
+ | ---------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
43
+ | Channel descriptor and `channelId` | Immutable payer, payee, token, signer, operator, nonce, and salt identity. | Derived from descriptor, chain ID, and escrow address. Every boundary validates it. |
44
+ | On-chain channel state | Deposit, settled amount, and close state. | Tempo escrow precompile. |
45
+ | Signed voucher | Payer authorization of a cumulative amount for one channel. | Client signer; server verifies before accepting. |
46
+ | Server channel record | Highest accepted signed voucher, delivered `spent`, `units`, and settlement progress. | Server `AtomicStore`. |
47
+ | Client channel entry | Latest locally usable channel descriptor, deposit, and cumulative authorization. | Client `ChannelStore`; a cache. |
48
+ | `SessionSnapshot` | Server-provided recovery hint. | Untrusted until client validation. |
49
+
50
+ The amounts below have intentionally different meanings.
51
+
52
+ | Amount | Meaning | Rule |
53
+ | --------------------------------------------- | -------------------------------------------- | -------------------------------------------------------- |
54
+ | `acceptedCumulative` / `highestVoucherAmount` | Highest voucher the server accepted. | Never decreases. |
55
+ | `spent` | Value charged for content. | Never decreases; does not exceed accepted authorization. |
56
+ | `settled` / `settledOnChain` | Value captured by escrow. | Never decreases; chain-authoritative. |
57
+ | `requiredCumulative` | Authorization required for the next request. | It is a boundary, not a signed voucher. |
58
+
59
+ ## Request lifecycle
60
+
61
+ The session method owns payment control flow. The route handler owns content.
62
+
63
+ ```mermaid
64
+ sequenceDiagram
65
+ participant C as Client
66
+ participant S as Server session
67
+ participant L as AtomicStore
68
+ participant T as Tempo chain
69
+ participant H as Route handler
70
+
71
+ C->>S: protected request
72
+ S-->>C: 402 session challenge
73
+ C->>S: open or voucher credential
74
+ S->>T: validate channel state; broadcast management transaction when needed
75
+ S->>L: atomically accept voucher and record channel state
76
+ S->>L: charge billable HTTP content
77
+ S->>H: invoke only for a content credential
78
+ H-->>C: response with Payment-Receipt
79
+ ```
80
+
81
+ `open` and `voucher` credentials may pay for a billable request. `topUp` and
82
+ `close` are management credentials: they return `204` and never invoke the
83
+ route handler. `open` and `voucher` used on non-billable requests are also
84
+ management updates.
85
+
86
+ The current HTTP contract is deliberately pre-handler: voucher acceptance and
87
+ default request charging occur before the handler runs. A handler failure can
88
+ therefore follow an accepted voucher and recorded `spent`. Sessions does not
89
+ promise an atomic “charge only after a successful handler” transaction.
90
+
91
+ Applications with irreversible work define their own idempotency and failure
92
+ behavior. SDK changes must preserve this boundary or introduce an explicit
93
+ prepare/commit contract rather than changing it implicitly.
94
+
95
+ ## Recovery lifecycle
96
+
97
+ The application owns durable mapping from its authenticated request identity to
98
+ a channel ID. The SDK owns channel lookup, snapshot construction, and recovery
99
+ validation.
100
+
101
+ `resolveChannelId` is the only application-defined recovery boundary. It maps a
102
+ verified request identity to an existing channel ID. It does not authorize a
103
+ channel, recreate accounting, or trust a client-provided channel ID.
104
+
105
+ ### Bootstrap configuration
106
+
107
+ Enable `bootstrap` when a client should recover an existing channel before its
108
+ first paid request:
109
+
110
+ ```ts
111
+ tempo.session({
112
+ bootstrap: true,
113
+ resolveChannelId({ source, paymentRequest }) {
114
+ return db.findChannelId({
115
+ payer: parseSource(source),
116
+ payee: paymentRequest.recipient,
117
+ token: paymentRequest.currency,
118
+ // Include chain and escrow when the application supports more than one.
119
+ })
120
+ },
121
+ })
122
+ ```
123
+
124
+ The hook resolves application identity and payment scope. The channel store
125
+ loads the returned primary key only; MPPx does not scan the store or define a
126
+ secondary-index format. If the request supplies a channel ID, MPPx uses it and
127
+ does not call the hook.
128
+
129
+ ```mermaid
130
+ sequenceDiagram
131
+ participant C as Cold client
132
+ participant S as Server session
133
+ participant R as resolveChannelId
134
+ participant L as AtomicStore
135
+ participant T as Tempo chain
136
+
137
+ C->>S: HEAD protected resource
138
+ S-->>C: 402 zero-amount identity challenge
139
+ C->>S: HEAD with signed proof
140
+ S->>S: verify proof and recover source
141
+ S->>R: resolve source and payment scope to channelId
142
+ R-->>S: channelId or no result
143
+ S->>L: load compatible channel record
144
+ S->>S: validate chain, token, escrow, payee, and signed voucher
145
+ S-->>C: 204 with session snapshot when available
146
+ C->>T: read live channel state
147
+ C->>C: validate descriptor, signer, voucher, and state
148
+ C->>C: hydrate Client ChannelStore
149
+ ```
150
+
151
+ A snapshot is never authorization. Before caching it, the client validates the
152
+ descriptor-derived ID, payer, authorized signer, signed voucher when present,
153
+ and live on-chain state. A snapshot that lacks a usable signed voucher cannot
154
+ create higher client authorization; the client reuses persisted state or opens
155
+ a channel instead.
156
+
157
+ ## Streaming lifecycle
158
+
159
+ SSE and WebSocket are stream-metered. They do not reuse normal HTTP response
160
+ accounting. The server emits a voucher boundary as content is consumed; the
161
+ client submits the next signed cumulative voucher; the server accepts it and
162
+ emits a receipt before content continues.
163
+
164
+ ```mermaid
165
+ sequenceDiagram
166
+ participant S as Metered server stream
167
+ participant C as Client transport driver
168
+ participant L as AtomicStore
169
+
170
+ S->>C: content
171
+ S->>C: need-voucher(requiredCumulative)
172
+ C->>C: enforce max deposit; top up when required
173
+ C->>S: signed voucher
174
+ S->>L: atomically accept voucher
175
+ S->>C: receipt
176
+ S->>C: continue content
177
+ ```
178
+
179
+ The initial SSE POST is excluded from default HTTP charging to prevent double
180
+ counting. Stream accounting remains owned by the transport driver.
181
+
182
+ ## Extension boundaries
183
+
184
+ | Change | Preserve |
185
+ | ------------------------------ | ----------------------------------------------------------------------------------------------- |
186
+ | Credential action or payload | Descriptor validation, source binding, action gate, receipt semantics, and atomic store update. |
187
+ | Voucher or accounting rule | Monotonic accepted/spent/settled values and linearizable updates. |
188
+ | Client planning or persistence | Channel scope key, recovery validation, max-deposit enforcement, and manual context behavior. |
189
+ | Bootstrap or snapshot | `resolveChannelId` ownership and client-side validation before cache hydration. |
190
+ | SSE or WebSocket transport | Transport-owned metering and receipt coordination; no HTTP double charge. |
191
+
192
+ The module boundaries implement these contracts:
193
+
194
+ | Module | Responsibility |
195
+ | --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
196
+ | `precompile/` | Channel identity, chain calls, vouchers, and wire primitives. |
197
+ | `client/CredentialState.ts` and `client/ChannelOps.ts` | Credential planning, recovery validation, and client cache updates. |
198
+ | `server/CredentialVerification.ts` and `server/ChannelStore.ts` | Credential verification and authoritative atomic channel state. |
199
+ | `server/Settlement.ts` | Content accounting and server-owned settlement cadence. |
200
+ | `server/RequestState.ts` | Challenge context, bootstrap identity lookup, snapshots, and response gating. |
201
+ | `client/Transports.ts` and `server/Transports.ts` | SSE and WebSocket payment control messages. |
@@ -22,6 +22,17 @@ export type SessionSnapshot = {
22
22
  escrow: Address
23
23
  /** Minimum cumulative authorization needed for the challenged request or stream continuation. */
24
24
  requiredCumulative: RawAmountString
25
+ /** Highest client-signed voucher accepted by the server. */
26
+ highestVoucher?:
27
+ | {
28
+ /** Channel identifier bound into the voucher signature. */
29
+ channelId: Hex
30
+ /** Cumulative authorization bound into the voucher signature. */
31
+ cumulativeAmount: RawAmountString
32
+ /** Original client signature proving the accepted cumulative authorization. */
33
+ signature: Hex
34
+ }
35
+ | undefined
25
36
  /** Amount already settled on-chain. */
26
37
  settled: RawAmountString
27
38
  /** Amount consumed by delivered content according to server accounting. */
@@ -36,6 +47,9 @@ const addressSchema = z.custom<Address>(
36
47
  const hashSchema = z.custom<Hex>(
37
48
  (value) => typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value),
38
49
  )
50
+ const hexSchema = z.custom<Hex>(
51
+ (value) => typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value),
52
+ )
39
53
 
40
54
  const channelDescriptorSchema = z.object({
41
55
  authorizedSigner: addressSchema,
@@ -55,6 +69,13 @@ const sessionSnapshotSchema = z.object({
55
69
  deposit: z.string(),
56
70
  descriptor: channelDescriptorSchema,
57
71
  escrow: addressSchema,
72
+ highestVoucher: z.optional(
73
+ z.object({
74
+ channelId: hashSchema,
75
+ cumulativeAmount: z.string(),
76
+ signature: hexSchema,
77
+ }),
78
+ ),
58
79
  requiredCumulative: z.string(),
59
80
  settled: z.string(),
60
81
  spent: z.string(),
@@ -9,6 +9,17 @@ import * as Types from '../precompile/Protocol.js'
9
9
  import * as Voucher from '../precompile/Voucher.js'
10
10
  import * as ChannelOps from './ChannelOps.js'
11
11
 
12
+ const mocks = vi.hoisted(() => ({
13
+ prepareTransactionRequest: vi.fn(async (_client: unknown, request: unknown) => request),
14
+ signTransaction: vi.fn(async () => '0x1234'),
15
+ }))
16
+
17
+ vi.mock('viem/actions', async (importOriginal) => ({
18
+ ...(await importOriginal<typeof import('viem/actions')>()),
19
+ prepareTransactionRequest: mocks.prepareTransactionRequest,
20
+ signTransaction: mocks.signTransaction,
21
+ }))
22
+
12
23
  const account = privateKeyToAccount(
13
24
  '0xac0974bec39a17e36ba6a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
14
25
  )
@@ -160,4 +171,48 @@ describe('precompile client ChannelOps credential builders', () => {
160
171
  ),
161
172
  ).toBe(true)
162
173
  })
174
+
175
+ test('distinguishes otherwise-identical fee-sponsored management transactions', async () => {
176
+ mocks.prepareTransactionRequest.mockClear()
177
+ let randomValue = 1
178
+ const random = vi.spyOn(globalThis.crypto, 'getRandomValues').mockImplementation((array) => {
179
+ if (!array) return array
180
+ new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(randomValue++)
181
+ return array
182
+ })
183
+ try {
184
+ await ChannelOps.createTopUpPayload(client, account, descriptor, 10n, chainId, true)
185
+ await ChannelOps.createTopUpPayload(client, account, descriptor, 10n, chainId, true)
186
+ } finally {
187
+ random.mockRestore()
188
+ }
189
+
190
+ const requests = mocks.prepareTransactionRequest.mock.calls.map(([, request]) => request)
191
+ const validAfter = requests.map((request) =>
192
+ Number((request as { validAfter?: number }).validAfter),
193
+ )
194
+ const now = Math.floor(Date.now() / 1_000)
195
+
196
+ expect(requests).toMatchObject([
197
+ { feePayer: true, nonceKey: 'expiring', validAfter: expect.any(Number) },
198
+ { feePayer: true, nonceKey: 'expiring', validAfter: expect.any(Number) },
199
+ ])
200
+ expect(new Set(validAfter).size).toBe(2)
201
+ expect(validAfter.every((value) => value >= 0 && value < now)).toBe(true)
202
+ })
203
+
204
+ test('uses expiring nonces and entropy for unsponsored management transactions', async () => {
205
+ mocks.prepareTransactionRequest.mockClear()
206
+
207
+ await ChannelOps.createTopUpPayload(client, account, descriptor, 10n, chainId, false)
208
+
209
+ expect(mocks.prepareTransactionRequest).toHaveBeenCalledWith(
210
+ client,
211
+ expect.objectContaining({
212
+ nonceKey: 'expiring',
213
+ validAfter: expect.any(Number),
214
+ }),
215
+ )
216
+ expect(mocks.prepareTransactionRequest.mock.calls[0]?.[1]).not.toHaveProperty('feePayer')
217
+ })
163
218
  })
@@ -28,7 +28,7 @@ import type {
28
28
  import { uint96 } from '../precompile/Protocol.js'
29
29
  import * as Voucher from '../precompile/Voucher.js'
30
30
 
31
- type TempoChannelCall = {
31
+ export type TempoChannelCall = {
32
32
  to: Address
33
33
  data: Hex.Hex
34
34
  }
@@ -67,6 +67,14 @@ function readAccessKeyAddress(account: Account): Address | undefined {
67
67
  return readOptionalAddress((account as AccountWithAccessKey).accessKeyAddress)
68
68
  }
69
69
 
70
+ /** Returns random past seconds to distinguish otherwise-identical expiring transactions. */
71
+ function randomValidAfter(): number {
72
+ const now = BigInt(Math.floor(Date.now() / 1_000))
73
+ const latest = now - 60n
74
+ if (latest <= 0n) return 0
75
+ return Number(BigInt(Hex.random(8)) % latest)
76
+ }
77
+
70
78
  /** Resolves the voucher authority address for a client account. */
71
79
  export function resolveAuthorizedSigner(account: Account): Address {
72
80
  return readAccessKeyAddress(account) ?? account.address
@@ -79,17 +87,22 @@ async function prepareTempoChannelTransaction(
79
87
  calls: readonly TempoChannelCall[]
80
88
  feePayer?: boolean | undefined
81
89
  feeToken: Address
90
+ validAfter?: number | undefined
82
91
  },
83
92
  ) {
84
- const { account, calls, feePayer, feeToken } = parameters
93
+ const { account, calls, feePayer, feeToken, validAfter } = parameters
94
+ // Session management transactions are independent and short-lived, so they
95
+ // always use TIP-1009 expiring nonces. `feePayer` controls sponsorship only.
85
96
  // viem's stable transaction request type does not yet expose Tempo's
86
97
  // `calls`, `feePayer`, and `feeToken` fields together. Keep the cast at
87
98
  // this boundary so session credential builders stay typed.
88
99
  return prepareTransactionRequest(client, {
89
100
  account,
90
101
  calls,
102
+ nonceKey: 'expiring',
91
103
  ...(feePayer ? { feePayer: true } : {}),
92
104
  feeToken,
105
+ ...(validAfter !== undefined ? { validAfter } : {}),
93
106
  } as never)
94
107
  }
95
108
 
@@ -218,6 +231,8 @@ export async function createOpenPayload(
218
231
  initialAmount: bigint
219
232
  operator?: Address | undefined
220
233
  payee: Address
234
+ /** Calls executed atomically before the channel is opened. */
235
+ prefixCalls?: readonly TempoChannelCall[] | undefined
221
236
  token: Address
222
237
  },
223
238
  ): Promise<OpenCredentialPayload> {
@@ -235,7 +250,7 @@ export async function createOpenPayload(
235
250
  })
236
251
  const prepared = await prepareTempoChannelTransaction(client, {
237
252
  account,
238
- calls: [{ to: escrow, data: openData }],
253
+ calls: [...(parameters.prefixCalls ?? []), { to: escrow, data: openData }],
239
254
  feePayer: parameters.feePayer,
240
255
  feeToken: parameters.token,
241
256
  })
@@ -295,6 +310,7 @@ export async function createTopUpPayload(
295
310
  chainId: number,
296
311
  feePayer?: boolean | undefined,
297
312
  escrow: Address = tip20ChannelEscrow,
313
+ prefixCalls: readonly TempoChannelCall[] = [],
298
314
  ): Promise<TopUpCredentialPayload> {
299
315
  const channelId = Channel.computeId({
300
316
  ...descriptor,
@@ -305,6 +321,7 @@ export async function createTopUpPayload(
305
321
  const prepared = await prepareTempoChannelTransaction(client, {
306
322
  account,
307
323
  calls: [
324
+ ...prefixCalls,
308
325
  {
309
326
  to: escrow,
310
327
  data: encodeFunctionData({
@@ -316,6 +333,7 @@ export async function createTopUpPayload(
316
333
  ],
317
334
  feePayer,
318
335
  feeToken: descriptor.token,
336
+ validAfter: randomValidAfter(),
319
337
  })
320
338
  const transaction = await signPreparedTempoTransaction(client, prepared)
321
339
 
@@ -443,7 +443,7 @@ describe('CredentialPlan', () => {
443
443
  })
444
444
  })
445
445
 
446
- test('recovery cumulative ignores server-advertised unused voucher headroom', () => {
446
+ test('recovery cumulative never decreases below the server-accepted voucher', () => {
447
447
  expect(
448
448
  resolveRecoveredCumulative({
449
449
  context: { descriptor, channelId },
@@ -456,7 +456,7 @@ describe('CredentialPlan', () => {
456
456
  spent: '10',
457
457
  }),
458
458
  }),
459
- ).toBe(15n)
459
+ ).toBe(1_000_000n)
460
460
  })
461
461
 
462
462
  test('plans manual credentials only when an explicit action includes descriptor', () => {