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
package/src/Method.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type * as Challenge from './Challenge.js'
2
- import type * as Credential from './Credential.js'
2
+ import * as Constants from './Constants.js'
3
+ import * as Credential from './Credential.js'
4
+ import * as Errors from './Errors.js'
5
+ import * as Expires from './Expires.js'
3
6
  import type { ExactPartial, LooseOmit, MaybePromise } from './internal/types.js'
4
7
  import type * as Receipt from './Receipt.js'
5
8
  import type * as Html from './server/internal/html/config.js'
@@ -117,12 +120,72 @@ export type VerifyContext<method extends Method> = {
117
120
  request: z.input<method['schema']['request']>
118
121
  }
119
122
 
123
+ /** Validation hook parameters for a single method. */
124
+ export type ValidateContext<method extends Method> = VerifyContext<method>
125
+
120
126
  /** Response hook parameters for a single method. */
121
127
  export type RespondContext<method extends Method> = VerifyContext<method> & {
122
128
  input: globalThis.Request
123
129
  receipt: Receipt.Receipt
124
130
  }
125
131
 
132
+ /**
133
+ * Non-mutating method-specific validation result.
134
+ *
135
+ * Returned by {@link ValidateFn} and exposed from `mppx.validateCredential()`.
136
+ * This confirms that a credential is currently acceptable to the payment
137
+ * method; it does not settle, reserve, or otherwise consume the payment.
138
+ */
139
+ export type Validation<method extends Method = Method, details = unknown> = Readonly<{
140
+ /**
141
+ * The challenge echoed by the credential and accepted by the method.
142
+ *
143
+ * Callers using `Mppx.validateCredential()` receive a challenge whose HMAC,
144
+ * expiry, route binding, and method identity have already been checked.
145
+ * Lower-level `Method.validateCredential()` callers must verify challenge
146
+ * issuance and route binding themselves.
147
+ */
148
+ challenge: Challenge.Challenge<
149
+ z.output<method['schema']['request']>,
150
+ method['intent'],
151
+ method['name']
152
+ >
153
+ /**
154
+ * The submitted credential with its method-specific payload parsed by the
155
+ * selected method schema. This is the exact credential validation examined.
156
+ */
157
+ credential: Credential.Credential<
158
+ z.output<method['schema']['credential']['payload']>,
159
+ Challenge.Challenge<z.output<method['schema']['request']>, method['intent'], method['name']>
160
+ >
161
+ /**
162
+ * Method-defined, non-mutating validation data.
163
+ *
164
+ * Its shape is intentionally method-specific—for example, a recovered payer
165
+ * address or a simulation result. Consumers must not assume a particular
166
+ * shape or use it as a settlement receipt.
167
+ */
168
+ details: details
169
+ /** The validated payment intent, repeated for convenient dispatch. */
170
+ intent: method['intent']
171
+ /** The validated payment method, repeated for convenient dispatch. */
172
+ method: method['name']
173
+ /**
174
+ * The validated method request after the method's schema transforms.
175
+ *
176
+ * For `Mppx.validateCredential()` with route options, this is the
177
+ * authoritative route request after defaults and request-hook transforms.
178
+ */
179
+ request: z.output<method['schema']['request']>
180
+ /**
181
+ * Optional payer identity declared by the credential.
182
+ *
183
+ * This is an asserted identity, not independent proof of control; methods
184
+ * that rely on it must validate the relationship to the credential payload.
185
+ */
186
+ source?: string | undefined
187
+ }>
188
+
126
189
  /**
127
190
  * A server-side configured method with verification logic.
128
191
  */
@@ -141,8 +204,15 @@ export type Server<
141
204
  preflight?: PreflightFn<method> | undefined
142
205
  request?: RequestFn<method> | undefined
143
206
  respond?: RespondFn<method> | undefined
207
+ broadcast?: BroadcastFn<method> | undefined
144
208
  stableBinding?: StableBindingFn<method> | undefined
145
209
  transport?: transportOverride | undefined
210
+ validate?: ValidateFn<method> | undefined
211
+ /**
212
+ * @deprecated Use `validate` for the non-mutating pre-check and `broadcast`
213
+ * for settlement. `verify` combines both operations and may consume payment
214
+ * state, so it cannot support a safe pre-check endpoint.
215
+ */
146
216
  verify: VerifyFn<method>
147
217
  }
148
218
  export type AnyServer = Server<any, any, any, any, any>
@@ -221,11 +291,148 @@ export type StableBindingFn<method extends Method> = (
221
291
  request: z.output<method['schema']['request']>,
222
292
  ) => Record<string, unknown>
223
293
 
224
- /** Verification function for a single method. */
294
+ /**
295
+ * Legacy combined validation and settlement function for a single method.
296
+ *
297
+ * @deprecated Implement `validate` and `broadcast` instead. `validate` must
298
+ * be non-mutating; `broadcast` performs the terminal payment operation. This
299
+ * hook remains only for existing methods that cannot yet split those phases.
300
+ */
225
301
  export type VerifyFn<method extends Method> = (
226
302
  parameters: VerifyContext<method>,
227
303
  ) => Promise<Receipt.Receipt>
228
304
 
305
+ /**
306
+ * Non-mutating validation function for a single method.
307
+ *
308
+ * @returns A {@link Validation} record describing the credential that was
309
+ * accepted. It must not settle, reserve, or otherwise consume payment state.
310
+ */
311
+ export type ValidateFn<method extends Method> = (
312
+ parameters: ValidateContext<method>,
313
+ ) => Promise<Validation<method>>
314
+
315
+ /** Terminal payment function for a single method. */
316
+ export type BroadcastFn<method extends Method> = (
317
+ parameters: VerifyContext<method>,
318
+ ) => Promise<Receipt.Receipt>
319
+
320
+ /**
321
+ * Validates a credential against one of the configured methods.
322
+ *
323
+ * This checks credential structure, challenge expiry, and method-specific
324
+ * validation. It does not prove that the challenge was issued by a particular
325
+ * server; hosts that issue challenges must verify that binding separately.
326
+ */
327
+ export async function validateCredential<const methods extends readonly AnyServer[]>(
328
+ methods: methods,
329
+ input: string | Credential.Credential,
330
+ ): Promise<Validation<methods[number]>> {
331
+ const prepared = prepareCredential(methods, input)
332
+ if (!prepared.method.validate)
333
+ throw new Errors.VerificationFailedError({
334
+ details: { intent: prepared.method.intent, method: prepared.method.name },
335
+ reason: `${prepared.method.name}/${prepared.method.intent} does not support non-mutating credential validation`,
336
+ })
337
+ return prepared.method.validate({
338
+ credential: prepared.credential,
339
+ request: prepared.request,
340
+ } as never) as Promise<Validation<methods[number]>>
341
+ }
342
+
343
+ /**
344
+ * Re-validates and performs the terminal payment operation for a credential.
345
+ *
346
+ * This does not prove that the challenge was issued by a particular server;
347
+ * hosts that issue challenges must verify that binding separately.
348
+ */
349
+ export async function broadcastCredential<const methods extends readonly AnyServer[]>(
350
+ methods: methods,
351
+ input: string | Credential.Credential,
352
+ ): Promise<Receipt.Receipt> {
353
+ const prepared = prepareCredential(methods, input)
354
+ const { method } = prepared
355
+
356
+ if (method.broadcast && method.validate)
357
+ await method.validate({ credential: prepared.credential, request: prepared.request } as never)
358
+
359
+ const broadcast = method.broadcast ?? method.verify
360
+ return broadcast({ credential: prepared.credential, request: prepared.request } as never)
361
+ }
362
+
363
+ /**
364
+ * Parses a submitted credential into the inputs required for method execution.
365
+ *
366
+ * Dispatch is based on the challenge method and intent. When more than one
367
+ * server method handles the same wire identity, session protocol details select
368
+ * the appropriate implementation. The helper then asserts challenge expiry and
369
+ * parses the method-specific credential payload before returning the selected
370
+ * method and the unmodified challenge request.
371
+ *
372
+ * This intentionally does not verify that the challenge was issued by a
373
+ * particular host, authorize the caller or requested resource, validate the
374
+ * method request, or invoke method lifecycle hooks. Hosts that issue challenges
375
+ * must verify their challenge binding before accepting the credential.
376
+ */
377
+ function prepareCredential(
378
+ methods: readonly AnyServer[],
379
+ input: string | Credential.Credential,
380
+ ): {
381
+ credential: Credential.Credential
382
+ method: AnyServer
383
+ request: Record<string, unknown>
384
+ } {
385
+ const credential = typeof input === 'string' ? Credential.deserialize(input) : input
386
+ const candidates = methods.filter(
387
+ (method) =>
388
+ method.name === credential.challenge.method && method.intent === credential.challenge.intent,
389
+ )
390
+ const method = selectServerMethod(candidates, credential.challenge)
391
+ if (!method)
392
+ throw new Errors.InvalidChallengeError({
393
+ id: credential.challenge.id,
394
+ reason: `no registered method for ${credential.challenge.method}/${credential.challenge.intent}`,
395
+ })
396
+
397
+ Expires.assert(credential.challenge.expires, credential.challenge.id)
398
+
399
+ let payload: unknown
400
+ try {
401
+ payload = method.schema.credential.payload.parse(credential.payload)
402
+ } catch (error) {
403
+ throw new Errors.InvalidPayloadError(error instanceof Error ? { reason: error.message } : {})
404
+ }
405
+
406
+ return {
407
+ credential: { ...credential, payload },
408
+ method,
409
+ request: credential.challenge.request,
410
+ }
411
+ }
412
+
413
+ /** @internal */
414
+ export function selectServerMethod(
415
+ methods: readonly AnyServer[],
416
+ challenge: Challenge.Challenge,
417
+ ): AnyServer | undefined {
418
+ if (methods.length <= 1) return methods[0]
419
+ if (
420
+ challenge.method !== Constants.Methods.tempo ||
421
+ challenge.intent !== Constants.Intents.session
422
+ )
423
+ return methods[0]
424
+
425
+ const sessionProtocol = Constants.getMethodDetail(
426
+ challenge.request.methodDetails,
427
+ Constants.MethodDetailKeys.sessionProtocol,
428
+ )
429
+ if (sessionProtocol === undefined || sessionProtocol === Constants.SessionProtocols.v1)
430
+ return methods.find((method) => method.alias === 'sessionLegacy') ?? methods[0]
431
+ if (sessionProtocol === Constants.SessionProtocols.v2)
432
+ return methods.find((method) => method.alias === undefined) ?? methods[0]
433
+ return undefined
434
+ }
435
+
229
436
  /**
230
437
  * Optional respond function for a server-side method.
231
438
  *
@@ -336,10 +543,22 @@ export function toServer<
336
543
  preflight,
337
544
  request,
338
545
  respond,
546
+ broadcast,
339
547
  stableBinding,
340
548
  transport,
549
+ validate,
341
550
  verify,
342
551
  } = options
552
+ const effectiveVerify =
553
+ verify ??
554
+ (async (parameters: VerifyContext<method>) => {
555
+ if (validate) await validate(parameters)
556
+ if (!broadcast)
557
+ throw new Errors.VerificationFailedError({
558
+ reason: `${method.name}/${method.intent} does not support credential broadcast`,
559
+ })
560
+ return broadcast(parameters)
561
+ })
343
562
  return {
344
563
  ...method,
345
564
  alias,
@@ -350,9 +569,11 @@ export function toServer<
350
569
  preflight,
351
570
  request,
352
571
  respond,
572
+ broadcast,
353
573
  stableBinding,
354
574
  transport,
355
- verify,
575
+ validate,
576
+ verify: effectiveVerify,
356
577
  } as Server<method, defaults, transportOverride, extensions, toServer.Alias<options>>
357
578
  }
358
579
 
@@ -376,6 +597,21 @@ export declare namespace toServer {
376
597
  respond?: RespondFn<method> | undefined
377
598
  stableBinding?: StableBindingFn<method> | undefined
378
599
  transport?: transportOverride | Transport.AnyTransport | undefined
379
- verify: VerifyFn<method>
380
- }
600
+ } & (
601
+ | {
602
+ broadcast: BroadcastFn<method>
603
+ validate?: ValidateFn<method> | undefined
604
+ verify?: undefined
605
+ }
606
+ | {
607
+ broadcast?: undefined
608
+ validate?: undefined
609
+ /**
610
+ * @deprecated Use `validate` and `broadcast` for new methods. This
611
+ * combined hook may mutate payment state and cannot power a safe
612
+ * validation-only endpoint.
613
+ */
614
+ verify: VerifyFn<method>
615
+ }
616
+ )
381
617
  }
@@ -5,6 +5,8 @@ import * as path from 'node:path'
5
5
 
6
6
  import { Errors } from 'incur'
7
7
 
8
+ import { isTempoAccount } from './utils.js'
9
+
8
10
  const SERVICE_NAME = 'mppx'
9
11
  const defaultCommandTimeoutMs = 10_000
10
12
 
@@ -203,6 +205,48 @@ export function createKeychain(account = 'main') {
203
205
  }
204
206
  }
205
207
 
208
+ /** Resolves a local CLI signer together with its durable account reference. */
209
+ export async function resolveLocalAccount(name?: string) {
210
+ const { privateKeyToAccount } = await import('viem/accounts')
211
+
212
+ const envKey = process.env.MPPX_PRIVATE_KEY?.trim()
213
+ if (envKey)
214
+ return {
215
+ account: privateKeyToAccount(envKey as `0x${string}`),
216
+ source: 'environment' as const,
217
+ }
218
+
219
+ const accountName = resolveAccountName(name)
220
+ const key = await createKeychain(accountName).get()
221
+ if (key)
222
+ return {
223
+ account: privateKeyToAccount(key as `0x${string}`),
224
+ accountName,
225
+ source: 'keychain' as const,
226
+ }
227
+
228
+ throw new Error(`Account "${accountName}" not found.`)
229
+ }
230
+
231
+ /** Resolves an account supported by persistent CLI sessions. */
232
+ export async function resolvePersistentAccount(name?: string) {
233
+ const accountName = resolveAccountName(name)
234
+ if (!process.env.MPPX_PRIVATE_KEY?.trim() && isTempoAccount(accountName))
235
+ throw new Errors.IncurError({
236
+ code: 'UNSUPPORTED_ACCOUNT',
237
+ message: 'Persistent sessions require an mppx account or MPPX_PRIVATE_KEY.',
238
+ exitCode: 2,
239
+ })
240
+ return resolveLocalAccount(name).catch((cause: unknown) => {
241
+ throw new Errors.IncurError({
242
+ code: 'ACCOUNT_NOT_FOUND',
243
+ message: cause instanceof Error ? cause.message : 'No account found.',
244
+ exitCode: 69,
245
+ ...(cause instanceof Error && { cause }),
246
+ })
247
+ })
248
+ }
249
+
206
250
  /**
207
251
  * Resolve a CLI account to a viem `LocalAccount`.
208
252
  *
@@ -221,14 +265,5 @@ export function createKeychain(account = 'main') {
221
265
  * ```
222
266
  */
223
267
  export async function resolveAccount(name?: string) {
224
- const { privateKeyToAccount } = await import('viem/accounts')
225
-
226
- const envKey = process.env.MPPX_PRIVATE_KEY?.trim()
227
- if (envKey) return privateKeyToAccount(envKey as `0x${string}`)
228
-
229
- const accountName = resolveAccountName(name)
230
- const key = await createKeychain(accountName).get()
231
- if (key) return privateKeyToAccount(key as `0x${string}`)
232
-
233
- throw new Error(`Account "${accountName}" not found.`)
268
+ return (await resolveLocalAccount(name)).account
234
269
  }