mppx 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +1 -1
  3. package/dist/Expires.js +1 -1
  4. package/dist/Store.d.ts +2 -2
  5. package/dist/Store.js +13 -3
  6. package/dist/cli/cli.js +137 -131
  7. package/dist/cli/plugins/index.js +11 -11
  8. package/dist/cli/validate/payment.d.ts +12 -0
  9. package/dist/cli/validate/payment.js +113 -14
  10. package/dist/client/Mppx.d.ts +13 -3
  11. package/dist/client/Mppx.js +47 -22
  12. package/dist/client/internal/Fetch.d.ts +11 -0
  13. package/dist/client/internal/Fetch.js +46 -8
  14. package/dist/client/internal/ProofRealm.d.ts +21 -0
  15. package/dist/client/internal/ProofRealm.js +55 -0
  16. package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
  17. package/dist/{attestation → extensions/identity}/Constants.js +2 -2
  18. package/dist/extensions/identity/Types.d.ts +21 -0
  19. package/dist/extensions/identity/index.d.ts +7 -0
  20. package/dist/extensions/identity/index.js +6 -0
  21. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
  22. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +12 -10
  23. package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
  24. package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
  25. package/dist/extensions/identity/internal/Verification.d.ts +9 -0
  26. package/dist/extensions/identity/internal/Verification.js +10 -0
  27. package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
  28. package/dist/extensions/identity/tap/client/index.d.ts +32 -0
  29. package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
  30. package/dist/extensions/identity/tap/server/index.d.ts +27 -0
  31. package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
  32. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
  33. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
  34. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
  35. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
  36. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
  37. package/dist/identity/Client.d.ts +34 -0
  38. package/dist/identity/Client.js +133 -0
  39. package/dist/identity/Server.d.ts +46 -0
  40. package/dist/identity/Server.js +145 -0
  41. package/dist/identity/Types.d.ts +90 -0
  42. package/dist/identity/Types.js +2 -0
  43. package/dist/{attestation → identity}/index.d.ts +2 -2
  44. package/dist/{attestation → identity}/index.js +1 -2
  45. package/dist/identity/internal/SigningContext.d.ts +6 -0
  46. package/dist/identity/internal/SigningContext.js +11 -0
  47. package/dist/integrations/x402/Evm.d.ts +2 -0
  48. package/dist/internal/version.d.ts +2 -2
  49. package/dist/internal/version.js +1 -1
  50. package/dist/mcp/client/McpClient.d.ts +5 -0
  51. package/dist/mcp/client/McpClient.js +2 -0
  52. package/dist/middlewares/express.js +18 -6
  53. package/dist/proxy/Proxy.js +10 -5
  54. package/dist/proxy/Service.d.ts +42 -4
  55. package/dist/proxy/Service.js +21 -5
  56. package/dist/server/Mppx.d.ts +32 -8
  57. package/dist/server/Mppx.js +144 -54
  58. package/dist/stripe/server/internal/html.gen.js +1 -1
  59. package/dist/tempo/Methods.d.ts +5 -0
  60. package/dist/tempo/Methods.js +1 -0
  61. package/dist/tempo/client/Subscription.d.ts +5 -0
  62. package/dist/tempo/internal/charge.js +2 -1
  63. package/dist/tempo/internal/fee-payer.d.ts +0 -2
  64. package/dist/tempo/internal/fee-payer.js +0 -4
  65. package/dist/tempo/server/Charge.js +4 -0
  66. package/dist/tempo/server/Methods.d.ts +26 -0
  67. package/dist/tempo/server/Subscription.d.ts +5 -0
  68. package/dist/tempo/server/Subscription.js +8 -5
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/session/client/Runtime.d.ts +2 -0
  71. package/dist/tempo/session/client/Session.js +1 -1
  72. package/dist/tempo/session/client/SessionManager.d.ts +6 -0
  73. package/dist/tempo/session/client/SessionManager.js +14 -7
  74. package/dist/tempo/session/client/Transports.d.ts +12 -3
  75. package/dist/tempo/session/client/Transports.js +37 -11
  76. package/dist/tempo/session/precompile/Chain.js +0 -3
  77. package/dist/tempo/session/server/Ws.d.ts +5 -2
  78. package/dist/tempo/session/server/Ws.js +11 -2
  79. package/dist/tempo/subscription/Store.d.ts +7 -1
  80. package/dist/tempo/subscription/Store.js +66 -13
  81. package/dist/tempo/subscription/Types.d.ts +2 -0
  82. package/package.json +15 -11
  83. package/dist/attestation/Client.d.ts +0 -17
  84. package/dist/attestation/Client.js +0 -28
  85. package/dist/attestation/Server.d.ts +0 -4
  86. package/dist/attestation/Server.js +0 -8
  87. package/dist/attestation/Types.d.ts +0 -76
  88. package/dist/attestation/internal/SigningContext.d.ts +0 -4
  89. package/dist/tap/client/index.d.ts +0 -26
  90. package/dist/tap/server/index.d.ts +0 -20
  91. /package/dist/{attestation → extensions/identity}/Types.js +0 -0
  92. /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
  93. /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
  94. /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
  95. /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
  96. /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
  97. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
  98. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
  99. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
  100. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
  101. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
  102. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
  103. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
  104. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
  105. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
@@ -377,6 +377,8 @@ export type SessionManagerRuntime = RuntimeState & {
377
377
  lastChallenge: TempoSessionChallenge | null;
378
378
  /** Last HTTP resource URL usable for management POSTs. */
379
379
  lastUrl: RequestInfo | URL | null;
380
+ /** Headers of the last HTTP request, whose application Authorization management POSTs keep. */
381
+ lastHeaders?: HeadersInit | undefined;
380
382
  /** Active WebSocket payment session bookkeeping, when a socket is open. */
381
383
  socketSession: ActiveSocketSession | null;
382
384
  };
@@ -323,7 +323,7 @@ export function session(parameters = {}) {
323
323
  },
324
324
  };
325
325
  return wrapSseResponse({
326
- onNeedVoucher: (event, streamSignal) => handleSseNeedVoucher({ challenge, driver, input, signal: streamSignal }, event),
326
+ onNeedVoucher: (event, streamSignal) => handleSseNeedVoucher({ challenge, driver, headers, input, signal: streamSignal }, event),
327
327
  onReceipt() { },
328
328
  response,
329
329
  signal,
@@ -1,5 +1,6 @@
1
1
  import type { Hex } from 'ox';
2
2
  import { type Address } from 'viem';
3
+ import * as ProofRealm from '../../../client/internal/ProofRealm.js';
3
4
  import * as Account from '../../../viem/Account.js';
4
5
  import * as Client from '../../../viem/Client.js';
5
6
  import { type ChannelStore } from '../client/ChannelStore.js';
@@ -73,6 +74,11 @@ export declare namespace sessionManager {
73
74
  autoSwap?: sessionPlugin.Parameters['autoSwap'];
74
75
  /** Enables same-route HEAD bootstrap from a server session snapshot before opening a new channel. */
75
76
  bootstrap?: boolean | undefined;
77
+ /**
78
+ * Server origins allowed to request bootstrap proofs for a realm other than their hostname,
79
+ * mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
80
+ */
81
+ trustedTempoProofRealms?: ProofRealm.TrustedRealms | undefined;
76
82
  /** Viem client instance. Shorthand for `getClient: () => client`. */
77
83
  client?: import('viem').Client | undefined;
78
84
  /** Token decimals used to convert `maxDeposit` to raw units. Defaults to `6`. */
@@ -4,6 +4,8 @@ import * as Challenge from '../../../Challenge.js';
4
4
  import * as Fetch from '../../../client/internal/Fetch.js';
5
5
  import * as MethodChallenge from '../../../client/internal/MethodChallenge.js';
6
6
  import * as MethodResponse from '../../../client/internal/MethodResponse.js';
7
+ import * as ProofRealm from '../../../client/internal/ProofRealm.js';
8
+ import { setCredentialHeader } from '../../../client/internal/protocols/Shared.js';
7
9
  import * as Constants from '../../../Constants.js';
8
10
  import * as Account from '../../../viem/Account.js';
9
11
  import * as Client from '../../../viem/Client.js';
@@ -353,17 +355,19 @@ export function sessionManager(parameters) {
353
355
  return undefined;
354
356
  if (!isZeroAmountChargeChallenge(challenge))
355
357
  return undefined;
358
+ // The proof authenticates the payer at the challenge realm: sign only for the server it is
359
+ // sent to, and never after a redirect to another origin.
360
+ const target = new URL(String(bootstrapInput), typeof location !== 'undefined' ? location.href : undefined);
361
+ if (challengeResponse.url && new URL(challengeResponse.url).origin !== target.origin)
362
+ return undefined;
363
+ ProofRealm.assert(challenge, target, parameters.trustedTempoProofRealms);
356
364
  const credential = await chargeMethod.createCredential({
357
365
  challenge: challenge,
358
366
  context: {},
359
367
  });
360
- const response = await config.fetch(bootstrapInput, {
361
- ...headInit,
362
- headers: {
363
- ...Fetch.normalizeHeaders(headInit.headers),
364
- [Constants.Headers.authorization]: credential,
365
- },
366
- });
368
+ const response = await config.fetch(bootstrapInput,
369
+ // Runtimes do not strip custom payment headers on cross-origin redirects.
370
+ setCredentialHeader({ ...headInit, redirect: 'manual' }, Challenge.credentialHeader(challenge), credential));
367
371
  if (response.ok)
368
372
  return await storeSnapshotHeader(response);
369
373
  return undefined;
@@ -643,6 +647,7 @@ export function sessionManager(parameters) {
643
647
  if (channelUse)
644
648
  throw new Error('SessionManager: a request is already in flight; concurrent requests on one manager are not supported');
645
649
  runtime.lastUrl = input;
650
+ runtime.lastHeaders = init?.headers;
646
651
  const previous = captureRuntimeStateSnapshot({
647
652
  channel: runtime.channel,
648
653
  spent: runtime.spent,
@@ -732,6 +737,7 @@ export function sessionManager(parameters) {
732
737
  createSessionCredential,
733
738
  fetch: config.fetch,
734
739
  getMinimumSpent: () => runtime.spent,
740
+ headers: runtime.lastHeaders,
735
741
  lastUrl: runtime.lastUrl,
736
742
  resolveSignedCloseAmount: (challenge) => getValidatedFallbackCloseAmount(target, challenge, true),
737
743
  signedCloseAmount: getValidatedFallbackCloseAmount(target),
@@ -799,6 +805,7 @@ export function sessionManager(parameters) {
799
805
  additionalDeposit: target.additionalDeposit,
800
806
  challenge: target.challenge,
801
807
  channelId: target.channelId,
808
+ headers: runtime.lastHeaders,
802
809
  input: target.input,
803
810
  });
804
811
  });
@@ -78,6 +78,8 @@ export type TopUpRequirement = {
78
78
  deposit: bigint;
79
79
  /** Original paid resource URL; converted to a management URL by the caller. */
80
80
  input: RequestInfo | URL;
81
+ /** Headers of the paid request; management posts keep its application `Authorization`. */
82
+ headers?: HeadersInit | undefined;
81
83
  /** Minimum cumulative voucher amount the server needs. */
82
84
  requiredCumulative: bigint;
83
85
  /** Cancels nested management requests for this top-up. */
@@ -158,6 +160,8 @@ export type ResolveNeedVoucherContextParameters = {
158
160
  getChannel(): ChannelEntry | null;
159
161
  /** Original paid resource URL; converted to management URL by the caller. */
160
162
  input: RequestInfo | URL;
163
+ /** Headers of the paid request; management posts keep its application `Authorization`. */
164
+ headers?: HeadersInit | undefined;
161
165
  /** Cancels top-up work triggered by this event. */
162
166
  signal?: AbortSignal | undefined;
163
167
  /** Performs the deposit top-up when server-required cumulative exceeds deposit. */
@@ -211,6 +215,8 @@ export type PostTopUpParameters = {
211
215
  fetch: typeof globalThis.fetch;
212
216
  /** Original paid resource URL; normalized to a management URL before posting. */
213
217
  input: RequestInfo | URL;
218
+ /** Headers of the paid request; management posts keep its application `Authorization`. */
219
+ headers?: HeadersInit | undefined;
214
220
  /** Cancels the top-up management request. */
215
221
  signal?: AbortSignal | undefined;
216
222
  /** Persists an unresolved funding outcome before another caller can reuse the channel. */
@@ -254,6 +260,8 @@ export type CloseHttpSessionParameters = {
254
260
  fetch: typeof globalThis.fetch;
255
261
  /** Last paid resource URL; used as the management endpoint base. */
256
262
  lastUrl: RequestInfo | URL | null;
263
+ /** Headers of the last paid request; the close post keeps its application `Authorization`. */
264
+ headers?: HeadersInit | undefined;
257
265
  /** Returns the latest cumulative spend already confirmed by the client. */
258
266
  getMinimumSpent?: (() => bigint) | undefined;
259
267
  /** Resolves the close amount again when the server refreshes the challenge. */
@@ -271,8 +279,8 @@ export declare function isTempoSessionChallenge(challenge: Challenge.Challenge):
271
279
  export declare function isTip1034SessionChallenge(challenge: Challenge.Challenge): challenge is TempoSessionChallenge;
272
280
  /** Reads a server-provided session snapshot from challenge method details. */
273
281
  export declare function getSessionSnapshot(challenge: TempoSessionChallenge): SessionSnapshot | undefined;
274
- /** Merges request headers and sets the payment authorization header for a retry. */
275
- export declare function requestInitWithAuthorization(input: RequestInfo | URL, init: RequestInit | undefined, credential: string): RequestInit;
282
+ /** Merges request headers and sets the challenge's credential header for a retry. */
283
+ export declare function requestInitWithAuthorization(input: RequestInfo | URL, init: RequestInit | undefined, credential: string, challenge: TempoSessionChallenge): RequestInit;
276
284
  /** Returns the exact resource URL used for out-of-band management POSTs, without its fragment. */
277
285
  export declare function managementInput(input: RequestInfo | URL): RequestInfo | URL;
278
286
  /** Converts a WebSocket URL into the HTTP URL used for its payment challenge probe. */
@@ -335,7 +343,7 @@ export type SsePaymentDriver = Pick<OpenSseSessionParameters, 'assertVoucherWith
335
343
  */
336
344
  export declare function openSseSession(input: RequestInfo | URL, init: SseDriverOptions | undefined, driver: OpenSseSessionParameters): Promise<AsyncIterable<string>>;
337
345
  /** @internal Consumes an already-paid SSE response with the session transport driver. */
338
- export declare function consumeSseSessionResponse(input: RequestInfo | URL, response: Response, options: SseResponseOptions | undefined, driver: OpenSseSessionParameters): AsyncIterable<string>;
346
+ export declare function consumeSseSessionResponse(input: RequestInfo | URL, response: Response, options: SseResponseOptions | undefined, driver: OpenSseSessionParameters, headers?: HeadersInit | undefined): AsyncIterable<string>;
339
347
  /** Application SSE frame emitted after session payment events are handled. */
340
348
  export type SseResponseFrame = {
341
349
  /** Parsed application data, when present. */
@@ -362,6 +370,7 @@ export declare function wrapSseResponse(parameters: DriveSseResponseParameters):
362
370
  export declare function handleSseNeedVoucher(parameters: {
363
371
  challenge: TempoSessionChallenge | null;
364
372
  driver: SsePaymentDriver;
373
+ headers?: HeadersInit | undefined;
365
374
  input: RequestInfo | URL;
366
375
  signal?: AbortSignal | undefined;
367
376
  }, event: NeedVoucherEvent): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Hex } from 'ox';
2
2
  import * as Challenge from '../../../Challenge.js';
3
3
  import * as Fetch from '../../../client/internal/Fetch.js';
4
+ import { setCredentialHeader } from '../../../client/internal/protocols/Shared.js';
4
5
  import * as Constants from '../../../Constants.js';
5
6
  import * as PaymentCredential from '../../../Credential.js';
6
7
  import { deserializeSessionReceipt, isEventStream, parseEvent, readSessionChallengeAmount, uint96, } from '../precompile/Protocol.js';
@@ -211,6 +212,7 @@ export async function resolveNeedVoucherContext(parameters) {
211
212
  await parameters.topUpIfNeeded({
212
213
  challenge: parameters.challenge,
213
214
  input: parameters.input,
215
+ headers: parameters.headers,
214
216
  channelId: parameters.expectedChannelId,
215
217
  deposit: eventAmounts.deposit,
216
218
  requiredCumulative: eventAmounts.requiredCumulative,
@@ -246,17 +248,18 @@ export function isTip1034SessionChallenge(challenge) {
246
248
  export function getSessionSnapshot(challenge) {
247
249
  return Constants.getMethodDetail(challenge.request.methodDetails, Constants.MethodDetailKeys.sessionSnapshot);
248
250
  }
249
- /** Merges request headers and sets the payment authorization header for a retry. */
250
- export function requestInitWithAuthorization(input, init, credential) {
251
+ /** Merges request headers and sets the challenge's credential header for a retry. */
252
+ export function requestInitWithAuthorization(input, init, credential, challenge) {
251
253
  const requestHeaders = input instanceof Request ? input.headers : undefined;
252
- return {
254
+ return setCredentialHeader({
253
255
  ...init,
256
+ // Runtimes do not strip custom payment headers on cross-origin redirects.
257
+ redirect: 'manual',
254
258
  headers: {
255
259
  ...Fetch.normalizeHeaders(requestHeaders),
256
260
  ...Fetch.normalizeHeaders(init?.headers),
257
- [Constants.Headers.authorization]: credential,
258
261
  },
259
- };
262
+ }, Challenge.credentialHeader(challenge), credential);
260
263
  }
261
264
  /** Returns the exact resource URL used for out-of-band management POSTs, without its fragment. */
262
265
  export function managementInput(input) {
@@ -324,6 +327,7 @@ export async function postTopUp(parameters) {
324
327
  },
325
328
  createSessionCredential: parameters.createSessionCredential,
326
329
  fetch: parameters.fetch,
330
+ headers: parameters.headers,
327
331
  input: parameters.input,
328
332
  signal: parameters.signal,
329
333
  }).catch(async (error) => {
@@ -360,7 +364,8 @@ async function postManagementCredential(parameters) {
360
364
  parameters.fetch(managementInput(parameters.input), {
361
365
  method: 'POST',
362
366
  signal: controller.signal,
363
- headers: { [Constants.Headers.authorization]: authorization },
367
+ redirect: 'manual',
368
+ headers: managementHeaders(challenge, authorization, parameters.input, parameters.headers),
364
369
  }),
365
370
  aborted,
366
371
  ]);
@@ -397,6 +402,7 @@ export async function retryHttpPaymentRequired(parameters) {
397
402
  await parameters.topUpIfNeeded({
398
403
  challenge,
399
404
  input: parameters.input,
405
+ headers: parameters.init?.headers,
400
406
  channelId: snapshot.channelId,
401
407
  deposit: BigInt(snapshot.deposit),
402
408
  requiredCumulative,
@@ -419,7 +425,7 @@ export async function retryHttpPaymentRequired(parameters) {
419
425
  descriptor: currentChannel.descriptor,
420
426
  cumulativeAmountRaw: cumulativeAmount.toString(),
421
427
  });
422
- retry = await parameters.fetch(parameters.input, requestInitWithAuthorization(parameters.input, parameters.init, credential));
428
+ retry = await parameters.fetch(parameters.input, requestInitWithAuthorization(parameters.input, parameters.init, credential, challenge));
423
429
  }
424
430
  catch (error) {
425
431
  await restore();
@@ -463,7 +469,8 @@ export async function closeHttpSession(parameters) {
463
469
  });
464
470
  return parameters.fetch(parameters.lastUrl, {
465
471
  method: 'POST',
466
- headers: { [Constants.Headers.authorization]: credential },
472
+ redirect: 'manual',
473
+ headers: managementHeaders(challenge, credential, parameters.lastUrl, parameters.headers),
467
474
  });
468
475
  };
469
476
  let response = await postClose(parameters.target.challenge);
@@ -516,17 +523,17 @@ export async function openSseSession(input, init, driver) {
516
523
  let response = await driver.doFetch(input, sseInit);
517
524
  if (!isEventStream(response) && driver.getChannel()?.opened)
518
525
  response = await driver.doFetch(input, sseInit);
519
- return consumeSseSessionResponse(input, response, { onReceipt, signal }, driver);
526
+ return consumeSseSessionResponse(input, response, { onReceipt, signal }, driver, sseInit.headers);
520
527
  }
521
528
  /** @internal Consumes an already-paid SSE response with the session transport driver. */
522
- export function consumeSseSessionResponse(input, response, options, driver) {
529
+ export function consumeSseSessionResponse(input, response, options, driver, headers) {
523
530
  if (!isEventStream(response))
524
531
  throw new Error('SSE response is not an event stream.');
525
532
  if (!response.body)
526
533
  throw new Error('Response has no body.');
527
534
  const challenge = driver.getChallenge();
528
535
  return iterateSseMessages({
529
- onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input, signal: options?.signal }, event),
536
+ onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, headers, input, signal: options?.signal }, event),
530
537
  onReceipt(receipt) {
531
538
  driver.acceptReceipt(receipt);
532
539
  options?.onReceipt?.(receipt);
@@ -682,6 +689,7 @@ export async function handleSseNeedVoucher(parameters, event) {
682
689
  event,
683
690
  expectedChannelId: channel.channelId,
684
691
  getChannel: parameters.driver.getChannel,
692
+ headers: parameters.headers,
685
693
  input: parameters.input,
686
694
  signal: parameters.signal,
687
695
  topUpIfNeeded: parameters.driver.topUpIfNeeded,
@@ -693,6 +701,7 @@ export async function handleSseNeedVoucher(parameters, event) {
693
701
  context: resolution.context,
694
702
  createSessionCredential: parameters.driver.createSessionCredential,
695
703
  fetch: parameters.driver.fetch,
704
+ headers: parameters.headers,
696
705
  input: parameters.input,
697
706
  signal: parameters.signal,
698
707
  });
@@ -979,4 +988,21 @@ function handleReceipt(parameters, receipt) {
979
988
  parameters.driver.options?.onReceipt?.(receipt);
980
989
  parameters.driver.settleReceipt(receipt);
981
990
  }
991
+ /**
992
+ * Builds the headers of a management or close post: the session credential in the header the
993
+ * challenge selects, plus the paid request's application `Authorization` when the credential
994
+ * goes elsewhere (`requiresAuth`), so the post passes the same application authentication.
995
+ */
996
+ function managementHeaders(challenge, credential, input, headers) {
997
+ const header = Challenge.credentialHeader(challenge);
998
+ const result = new Headers();
999
+ if (header.toLowerCase() !== Constants.Headers.authorization.toLowerCase()) {
1000
+ const authorization = new Headers(headers).get(Constants.Headers.authorization) ??
1001
+ (input instanceof Request ? input.headers.get(Constants.Headers.authorization) : null);
1002
+ if (authorization && !authorization.startsWith(`${Constants.Schemes.payment} `))
1003
+ result.set(Constants.Headers.authorization, authorization);
1004
+ }
1005
+ result.set(header, credential);
1006
+ return result;
1007
+ }
982
1008
  //# sourceMappingURL=Transports.js.map
@@ -300,9 +300,6 @@ function validateCredentialSponsorship(parameters) {
300
300
  return;
301
301
  if (feePayer === true) {
302
302
  assertPrecompileFeePayerPolicy({ prepared: transaction, policy: feePayerPolicy });
303
- if (feePayerPolicy?.allowKeyAuthorization === false &&
304
- transaction.keyAuthorization !== undefined)
305
- throw new BadRequestError({ reason: 'fee-payer policy keyAuthorization is not allowed' });
306
303
  FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens);
307
304
  FeePayer.assertTransactionPolicy({
308
305
  challengeExpires,
@@ -1,3 +1,4 @@
1
+ import type * as Challenge from '../../../Challenge.js';
1
2
  import { type SessionCredentialContext } from '../precompile/Protocol.js';
2
3
  import * as ChannelStore from './ChannelStore.js';
3
4
  import type { MeteredStreamOptions, SessionController } from './MeteredStream.js';
@@ -13,6 +14,7 @@ export type SessionRouteResult = {
13
14
  challenge: Response;
14
15
  } | {
15
16
  status: 200;
17
+ challenge?: Challenge.Challenge | undefined;
16
18
  withReceipt(response?: Response): Response | Promise<Response>;
17
19
  };
18
20
  /** HTTP route used to perform a WebSocket payment probe. */
@@ -36,14 +38,15 @@ export declare function serve(options: serve.Options): Promise<void>;
36
38
  /** Type helpers for {@link serve}. */
37
39
  export declare namespace serve {
38
40
  type Options = {
39
- /** Expected per-tick amount in raw units. When set, credentials whose
41
+ /** Expected per-tick amount in raw units. Required for custom routes that
42
+ * do not return their canonical challenge. When set, credentials whose
40
43
  * challenge `request.amount` does not match are rejected. Use this to
41
44
  * pin the price when the route is backed by `Mppx.compose()` with
42
45
  * multiple offers — otherwise a client can select the cheapest offer
43
46
  * and still receive the same stream. */
44
47
  amount?: string | undefined;
45
48
  /** Application stream. A manual stream can call `charge(amount)` with a
46
- * per-message raw-unit amount; omitting it uses the challenge tick cost. */
49
+ * per-message raw-unit amount; omitting it uses the canonical route tick cost. */
47
50
  generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
48
51
  /** Maximum accepted inbound WebSocket frame size in bytes. @default 65536 */
49
52
  maxIncomingMessageBytes?: number | undefined;
@@ -133,6 +133,11 @@ export async function serve(options) {
133
133
  return;
134
134
  }
135
135
  const { receipt } = authorizationResult;
136
+ const tickCost = expectedAmount !== undefined ? BigInt(expectedAmount) : authorizationResult.amount;
137
+ if (tickCost === undefined) {
138
+ await close(1008, 'WebSocket route must provide canonical pricing or an explicit amount');
139
+ return;
140
+ }
136
141
  if (payload.action === 'voucher' && runtime.streamContext) {
137
142
  runtime.streamContext.challengeId = credential.challenge.id;
138
143
  }
@@ -149,7 +154,7 @@ export async function serve(options) {
149
154
  runtime.streamContext = {
150
155
  challengeId: credential.challenge.id,
151
156
  channelId: payload.channelId,
152
- tickCost: readSessionChallengeAmount(credential.challenge),
157
+ tickCost,
153
158
  };
154
159
  // Defer the first application frame until after the client receives the
155
160
  // auth receipt and has a chance to install its own message listeners.
@@ -299,6 +304,10 @@ async function authorizePaymentFrame(parameters) {
299
304
  const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
300
305
  if (!receiptHeader)
301
306
  throw new Error('management response missing Payment-Receipt header');
302
- return { status: 'accepted', receipt: deserializeSessionReceipt(receiptHeader) };
307
+ return {
308
+ status: 'accepted',
309
+ amount: result.challenge ? readSessionChallengeAmount(result.challenge) : undefined,
310
+ receipt: deserializeSessionReceipt(receiptHeader),
311
+ };
303
312
  }
304
313
  //# sourceMappingURL=Ws.js.map
@@ -13,9 +13,13 @@ export type SubscriptionStore = {
13
13
  /** Looks up a generated access key for a resolved request key. */
14
14
  getAccessKey(key: string): Promise<SubscriptionAccessKeyRecord | null>;
15
15
  /** Looks up a generated access key by its public access key address. */
16
- getAccessKeyByAddress(address: string): Promise<SubscriptionAccessKeyRecord | null>;
16
+ getAccessKeyByAddress(address: string, keyToken?: `0x${string}`): Promise<SubscriptionAccessKeyRecord | null>;
17
17
  /** Looks up the active subscription for a resolved request key. */
18
18
  getByKey(key: string): Promise<SubscriptionRecord | null>;
19
+ /** Creates a key sealed in a client-carried token, without retaining per-challenge state. */
20
+ createChallengeAccessKey(): Promise<SubscriptionAccessKeyRecord>;
21
+ /** Retains a key after verified payment submission for subsequent subscription billing. */
22
+ retainAccessKey(key: string, record: SubscriptionAccessKeyRecord): Promise<void>;
19
23
  /** Gets or creates the server-owned access key for a resolved request key. */
20
24
  getOrCreateAccessKey(key: string): Promise<SubscriptionAccessKeyRecord>;
21
25
  /** Upserts a subscription record and marks it as active for its lookup key. */
@@ -85,6 +89,8 @@ export declare namespace fromStore {
85
89
  type Options = {
86
90
  /** Key prefix for server-owned subscription access keys. @default `'tempo:subscription:access-key:'` */
87
91
  accessKeyPrefix?: string | undefined;
92
+ /** Namespace for the persistent anonymous-challenge sealing key. */
93
+ pendingAccessKeyPrefix?: string | undefined;
88
94
  /** Key prefix for resolved subscription activation locks. @default `'tempo:subscription:activation:'` */
89
95
  activationPrefix?: string | undefined;
90
96
  /** Milliseconds before a stuck activation lock can be replaced. @default `900000` */
@@ -1,15 +1,16 @@
1
- import { Secp256k1 } from 'ox';
1
+ import { Hex, Secp256k1 } from 'ox';
2
2
  import { Account as TempoAccount } from 'viem/tempo';
3
3
  const defaultRecordPrefix = 'tempo:subscription:record:';
4
4
  const defaultKeyPrefix = 'tempo:subscription:key:';
5
5
  const defaultActivationPrefix = 'tempo:subscription:activation:';
6
6
  const defaultAccessKeyPrefix = 'tempo:subscription:access-key:';
7
+ const defaultPendingAccessKeyPrefix = 'tempo:subscription:pending-access-key:';
7
8
  const defaultCredentialPrefix = 'tempo:subscription:credential:';
8
9
  const defaultActivationTimeoutMs = 15 * 60 * 1_000;
9
10
  const defaultRenewalTimeoutMs = 15 * 60 * 1_000;
10
11
  /** Wraps a generic key-value {@link Store.Store} with subscription-specific accessors. */
11
12
  export function fromStore(store, options) {
12
- const { accessKeyPrefix = defaultAccessKeyPrefix, activationPrefix = defaultActivationPrefix, activationTimeoutMs = defaultActivationTimeoutMs, credentialPrefix = defaultCredentialPrefix, keyPrefix = defaultKeyPrefix, recordPrefix = defaultRecordPrefix, renewalTimeoutMs = defaultRenewalTimeoutMs, } = options ?? {};
13
+ const { accessKeyPrefix = defaultAccessKeyPrefix, activationPrefix = defaultActivationPrefix, activationTimeoutMs = defaultActivationTimeoutMs, credentialPrefix = defaultCredentialPrefix, keyPrefix = defaultKeyPrefix, recordPrefix = defaultRecordPrefix, pendingAccessKeyPrefix = defaultPendingAccessKeyPrefix, renewalTimeoutMs = defaultRenewalTimeoutMs, } = options ?? {};
13
14
  function recordKey(subscriptionId) {
14
15
  return `${recordPrefix}${subscriptionId}`;
15
16
  }
@@ -34,6 +35,14 @@ export function fromStore(store, options) {
34
35
  return null;
35
36
  return (await store.get(recordKey(subscriptionId)));
36
37
  }
38
+ async function ensureAccessKeyAddressIndex(record) {
39
+ await store.update(accessKeyAddressKey(record.accessKeyAddress), (current) => {
40
+ if (current)
41
+ return { op: 'noop', result: undefined };
42
+ return { op: 'set', value: record, result: undefined };
43
+ });
44
+ return record;
45
+ }
37
46
  async function clearRenewalState(subscriptionId, periodIndex, attempt) {
38
47
  await store.update(recordKey(subscriptionId), (current) => {
39
48
  const subscription = current;
@@ -61,7 +70,35 @@ export function fromStore(store, options) {
61
70
  const marker = (await store.get(activationKey(lookupKey)));
62
71
  return marker?.challengeId === challengeId;
63
72
  }
73
+ const sealingKey = `${pendingAccessKeyPrefix}sealing-key`;
74
+ // One shared secret survives across workers; anonymous issuance never grows the store.
75
+ async function getSealingKey() {
76
+ const candidate = Hex.fromBytes(crypto.getRandomValues(new Uint8Array(32)));
77
+ const secret = await store.update(sealingKey, (current) => current
78
+ ? { op: 'noop', result: current }
79
+ : { op: 'set', value: candidate, result: candidate });
80
+ return crypto.subtle.importKey('raw', new Uint8Array(Hex.toBytes(secret)), 'AES-GCM', false, [
81
+ 'encrypt',
82
+ 'decrypt',
83
+ ]);
84
+ }
64
85
  return {
86
+ async createChallengeAccessKey() {
87
+ const privateKey = Secp256k1.randomPrivateKey();
88
+ const account = TempoAccount.fromSecp256k1(privateKey);
89
+ const iv = crypto.getRandomValues(new Uint8Array(12));
90
+ const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, await getSealingKey(), new Uint8Array(Hex.toBytes(privateKey))));
91
+ return {
92
+ accessKeyAddress: account.address.toLowerCase(),
93
+ keyType: account.keyType,
94
+ privateKey,
95
+ keyToken: Hex.fromBytes(new Uint8Array([...iv, ...sealed])),
96
+ };
97
+ },
98
+ async retainAccessKey(key, record) {
99
+ await store.put(accessKeyKey(key), record);
100
+ await store.put(accessKeyAddressKey(record.accessKeyAddress), record);
101
+ },
65
102
  async activate({ challengeId, create, isReusable, lookupKey }) {
66
103
  const claimed = await store.update(credentialKey(challengeId), (current) => {
67
104
  if (current)
@@ -155,8 +192,31 @@ export function fromStore(store, options) {
155
192
  async getAccessKey(key) {
156
193
  return (await store.get(accessKeyKey(key)));
157
194
  },
158
- async getAccessKeyByAddress(address) {
159
- return (await store.get(accessKeyAddressKey(address)));
195
+ async getAccessKeyByAddress(address, keyToken) {
196
+ const stored = (await store.get(accessKeyAddressKey(address)));
197
+ if (stored)
198
+ return stored;
199
+ if (!keyToken)
200
+ return null;
201
+ try {
202
+ const bytes = Hex.toBytes(keyToken);
203
+ // 12-byte nonce, 32-byte private key, and 16-byte authentication tag.
204
+ if (bytes.length !== 60)
205
+ return null;
206
+ const privateKey = Hex.fromBytes(new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bytes.slice(0, 12) }, await getSealingKey(), bytes.slice(12))));
207
+ const account = TempoAccount.fromSecp256k1(privateKey);
208
+ if (account.address.toLowerCase() !== address.toLowerCase())
209
+ return null;
210
+ return {
211
+ accessKeyAddress: account.address.toLowerCase(),
212
+ keyType: account.keyType,
213
+ privateKey,
214
+ keyToken,
215
+ };
216
+ }
217
+ catch {
218
+ return null;
219
+ }
160
220
  },
161
221
  async getByKey(key) {
162
222
  return getByLookupKey(key);
@@ -164,7 +224,7 @@ export function fromStore(store, options) {
164
224
  async getOrCreateAccessKey(key) {
165
225
  const existing = (await store.get(accessKeyKey(key)));
166
226
  if (existing)
167
- return existing;
227
+ return ensureAccessKeyAddressIndex(existing);
168
228
  const privateKey = Secp256k1.randomPrivateKey();
169
229
  const account = TempoAccount.fromSecp256k1(privateKey);
170
230
  const candidate = {
@@ -179,14 +239,7 @@ export function fromStore(store, options) {
179
239
  }
180
240
  return { op: 'set', value: candidate, result: candidate };
181
241
  })
182
- .then(async (record) => {
183
- await store.update(accessKeyAddressKey(record.accessKeyAddress), (current) => {
184
- if (current)
185
- return { op: 'noop', result: undefined };
186
- return { op: 'set', value: record, result: undefined };
187
- });
188
- return record;
189
- });
242
+ .then(ensureAccessKeyAddressIndex);
190
243
  },
191
244
  async put(record) {
192
245
  await store.put(recordKey(record.subscriptionId), record);
@@ -5,6 +5,8 @@ export type SubscriptionPeriodUnit = 'dev_second' | 'day' | 'week';
5
5
  export type SubscriptionAccessKey = {
6
6
  accessKeyAddress: Address;
7
7
  keyType: 'p256' | 'secp256k1' | 'webAuthn';
8
+ /** Opaque server-sealed key for stateless anonymous setup. */
9
+ keyToken?: `0x${string}` | undefined;
8
10
  };
9
11
  /** Server-owned subscription access key persisted for automatic billing. */
10
12
  export type SubscriptionAccessKeyRecord = SubscriptionAccessKey & {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.14.1",
4
+ "version": "0.16.0",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/wevm/mppx#readme",
@@ -35,9 +35,9 @@
35
35
  "types": "./dist/client/node.d.ts",
36
36
  "default": "./dist/client/node.js"
37
37
  },
38
- "./attestation": {
39
- "types": "./dist/attestation/index.d.ts",
40
- "default": "./dist/attestation/index.js"
38
+ "./identity": {
39
+ "types": "./dist/identity/index.d.ts",
40
+ "default": "./dist/identity/index.js"
41
41
  },
42
42
  "./discovery": {
43
43
  "types": "./dist/discovery/index.d.ts",
@@ -123,13 +123,17 @@
123
123
  "types": "./dist/tempo/index.d.ts",
124
124
  "default": "./dist/tempo/index.js"
125
125
  },
126
- "./attestation/tap": {
127
- "types": "./dist/tap/index.d.ts",
128
- "default": "./dist/tap/index.js"
126
+ "./extensions/identity": {
127
+ "types": "./dist/extensions/identity/index.d.ts",
128
+ "default": "./dist/extensions/identity/index.js"
129
129
  },
130
- "./attestation/web-bot-auth": {
131
- "types": "./dist/web-bot-auth/index.d.ts",
132
- "default": "./dist/web-bot-auth/index.js"
130
+ "./extensions/identity/tap": {
131
+ "types": "./dist/extensions/identity/tap/index.d.ts",
132
+ "default": "./dist/extensions/identity/tap/index.js"
133
+ },
134
+ "./extensions/identity/web-bot-auth": {
135
+ "types": "./dist/extensions/identity/web-bot-auth/index.d.ts",
136
+ "default": "./dist/extensions/identity/web-bot-auth/index.js"
133
137
  },
134
138
  "./html": {
135
139
  "types": "./dist/Html.d.ts",
@@ -206,7 +210,7 @@
206
210
  "eventsource-parser": "4.1.1",
207
211
  "ox": "0.14.45",
208
212
  "structured-headers": "2.1.0",
209
- "zod": "^4.6.5"
213
+ "zod": "^4.6.7"
210
214
  },
211
215
  "repository": {
212
216
  "type": "git",
@@ -1,17 +0,0 @@
1
- import type * as Types from './Types.js';
2
- /**
3
- * Combines request signers using one shared context per call.
4
- *
5
- * The returned signer creates a fresh context unless its caller supplies one,
6
- * allowing every signature on one HTTP attempt to share a nonce and timestamp.
7
- */
8
- export declare function composeSigners(...signers: readonly [Types.Signer, ...Types.Signer[]]): Types.Signer<'composed'>;
9
- /**
10
- * Wraps a fetch implementation so every outbound request is attested.
11
- *
12
- * Configure this wrapper as the underlying `fetch` passed to `mppx/client`
13
- * `Fetch.from()`. MPPX then uses it for both the initial request and its
14
- * automatic request containing an `Authorization: Payment` credential.
15
- */
16
- export declare function wrapFetch(fetch: typeof globalThis.fetch, signer: Types.Signer): typeof globalThis.fetch;
17
- //# sourceMappingURL=Client.d.ts.map
@@ -1,28 +0,0 @@
1
- import * as SigningContext from './internal/SigningContext.js';
2
- /**
3
- * Combines request signers using one shared context per call.
4
- *
5
- * The returned signer creates a fresh context unless its caller supplies one,
6
- * allowing every signature on one HTTP attempt to share a nonce and timestamp.
7
- */
8
- export function composeSigners(...signers) {
9
- return {
10
- protocol: 'composed',
11
- async sign(request, context = SigningContext.create()) {
12
- for (const signer of signers)
13
- request = await signer.sign(request, context);
14
- return request;
15
- },
16
- };
17
- }
18
- /**
19
- * Wraps a fetch implementation so every outbound request is attested.
20
- *
21
- * Configure this wrapper as the underlying `fetch` passed to `mppx/client`
22
- * `Fetch.from()`. MPPX then uses it for both the initial request and its
23
- * automatic request containing an `Authorization: Payment` credential.
24
- */
25
- export function wrapFetch(fetch, signer) {
26
- return async (input, init) => fetch(await signer.sign(new Request(input, init)));
27
- }
28
- //# sourceMappingURL=Client.js.map
@@ -1,4 +0,0 @@
1
- import type * as Types from './Types.js';
2
- /** Verifies every configured request-attestation protocol. */
3
- export declare function verify<const verifiers extends Types.VerifierMap>(request: Request, verifiers: verifiers): Promise<Types.VerificationsFrom<verifiers>>;
4
- //# sourceMappingURL=Server.d.ts.map