@nevermined-io/payments 1.6.0 → 1.8.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 (88) hide show
  1. package/dist/a2a/agent-card.d.ts +26 -0
  2. package/dist/a2a/agent-card.d.ts.map +1 -1
  3. package/dist/a2a/agent-card.js +36 -1
  4. package/dist/a2a/agent-card.js.map +1 -1
  5. package/dist/a2a/paymentsClient.d.ts +41 -1
  6. package/dist/a2a/paymentsClient.d.ts.map +1 -1
  7. package/dist/a2a/paymentsClient.js +120 -8
  8. package/dist/a2a/paymentsClient.js.map +1 -1
  9. package/dist/a2a/paymentsRequestHandler.d.ts +25 -2
  10. package/dist/a2a/paymentsRequestHandler.d.ts.map +1 -1
  11. package/dist/a2a/paymentsRequestHandler.js +240 -20
  12. package/dist/a2a/paymentsRequestHandler.js.map +1 -1
  13. package/dist/a2a/server.d.ts +2 -2
  14. package/dist/a2a/server.d.ts.map +1 -1
  15. package/dist/a2a/server.js +70 -20
  16. package/dist/a2a/server.js.map +1 -1
  17. package/dist/a2a/types.d.ts +31 -1
  18. package/dist/a2a/types.d.ts.map +1 -1
  19. package/dist/a2a/types.js.map +1 -1
  20. package/dist/a2a/x402-a2a.d.ts +142 -0
  21. package/dist/a2a/x402-a2a.d.ts.map +1 -0
  22. package/dist/a2a/x402-a2a.js +254 -0
  23. package/dist/a2a/x402-a2a.js.map +1 -0
  24. package/dist/api/agents-api.d.ts +19 -0
  25. package/dist/api/agents-api.d.ts.map +1 -1
  26. package/dist/api/agents-api.js +30 -3
  27. package/dist/api/agents-api.js.map +1 -1
  28. package/dist/api/base-payments.d.ts +6 -4
  29. package/dist/api/base-payments.d.ts.map +1 -1
  30. package/dist/api/base-payments.js +10 -0
  31. package/dist/api/base-payments.js.map +1 -1
  32. package/dist/api/contracts-api.js +1 -1
  33. package/dist/api/contracts-api.js.map +1 -1
  34. package/dist/api/nvm-api.d.ts +1 -0
  35. package/dist/api/nvm-api.d.ts.map +1 -1
  36. package/dist/api/nvm-api.js +4 -0
  37. package/dist/api/nvm-api.js.map +1 -1
  38. package/dist/api/plans-api.d.ts +12 -2
  39. package/dist/api/plans-api.d.ts.map +1 -1
  40. package/dist/api/plans-api.js +12 -12
  41. package/dist/api/plans-api.js.map +1 -1
  42. package/dist/common/api-version.d.ts +24 -0
  43. package/dist/common/api-version.d.ts.map +1 -0
  44. package/dist/common/api-version.js +24 -0
  45. package/dist/common/api-version.js.map +1 -0
  46. package/dist/common/types.d.ts +73 -18
  47. package/dist/common/types.d.ts.map +1 -1
  48. package/dist/common/types.js.map +1 -1
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/core/auth.d.ts +14 -0
  54. package/dist/mcp/core/auth.d.ts.map +1 -1
  55. package/dist/mcp/core/auth.js +56 -23
  56. package/dist/mcp/core/auth.js.map +1 -1
  57. package/dist/mcp/core/paywall.d.ts +6 -0
  58. package/dist/mcp/core/paywall.d.ts.map +1 -1
  59. package/dist/mcp/core/paywall.js +170 -84
  60. package/dist/mcp/core/paywall.js.map +1 -1
  61. package/dist/mcp/utils/errors.d.ts +26 -0
  62. package/dist/mcp/utils/errors.d.ts.map +1 -1
  63. package/dist/mcp/utils/errors.js +32 -0
  64. package/dist/mcp/utils/errors.js.map +1 -1
  65. package/dist/mcp/utils/meta.d.ts +54 -0
  66. package/dist/mcp/utils/meta.d.ts.map +1 -0
  67. package/dist/mcp/utils/meta.js +72 -0
  68. package/dist/mcp/utils/meta.js.map +1 -0
  69. package/dist/payments.d.ts +4 -3
  70. package/dist/payments.d.ts.map +1 -1
  71. package/dist/payments.js +5 -3
  72. package/dist/payments.js.map +1 -1
  73. package/dist/utils.d.ts +27 -0
  74. package/dist/utils.d.ts.map +1 -1
  75. package/dist/utils.js +34 -0
  76. package/dist/utils.js.map +1 -1
  77. package/dist/x402/facilitator-api.d.ts +21 -0
  78. package/dist/x402/facilitator-api.d.ts.map +1 -1
  79. package/dist/x402/facilitator-api.js +39 -0
  80. package/dist/x402/facilitator-api.js.map +1 -1
  81. package/dist/x402/index.d.ts +1 -1
  82. package/dist/x402/index.d.ts.map +1 -1
  83. package/dist/x402/index.js.map +1 -1
  84. package/dist/x402/token.d.ts +13 -10
  85. package/dist/x402/token.d.ts.map +1 -1
  86. package/dist/x402/token.js +46 -16
  87. package/dist/x402/token.js.map +1 -1
  88. package/package.json +2 -2
@@ -28,6 +28,20 @@ export declare class PaywallAuthenticator {
28
28
  * Tries logical URL first, falls back to HTTP URL if available.
29
29
  */
30
30
  private verifyWithFallback;
31
+ /**
32
+ * Build a spec-shaped {@link PaymentRequiredError} from the agent's plans.
33
+ *
34
+ * Fetches the agent's plans (best-effort) to populate the `accepts` array of
35
+ * the `PaymentRequired` object and a human-readable list of plan names in the
36
+ * error message. Falls back to an empty plan id when no plans can be resolved
37
+ * so the structured shape is still valid.
38
+ *
39
+ * @param agentId - Agent identifier used to look up purchasable plans.
40
+ * @param endpoint - Logical resource URL placed in `PaymentRequired.resource`.
41
+ * @param message - Leading human-readable message (e.g. "Authorization required.").
42
+ * @returns A `PaymentRequiredError` carrying the `PaymentRequired` object.
43
+ */
44
+ private buildPaymentRequiredError;
31
45
  /**
32
46
  * Verify permissions against a single endpoint URL.
33
47
  * Resolves planId from the token or from the agent's plans as fallback.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAgBtD;;GAEG;AACH,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAEtC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IAepC;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAuB/B;;;OAGG;YACW,kBAAkB;IAoEhC;;;OAGG;YACW,kBAAkB;IAuDhC;;OAEG;IACG,YAAY,CAChB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,YAAK,EACrD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,EACpC,UAAU,EAAE,GAAG,GACd,OAAO,CAAC,UAAU,CAAC;IAkBtB;;;OAGG;IACG,gBAAgB,CACpB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,YAAK,EACrD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,CAAC;CAiBvB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAoBtD;;GAEG;AACH,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,QAAQ;IAEtC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IAepC;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAuB/B;;;OAGG;YACW,kBAAkB;IAsDhC;;;;;;;;;;;;OAYG;YACW,yBAAyB;IA2CvC;;;OAGG;YACW,kBAAkB;IAyDhC;;OAEG;IACG,YAAY,CAChB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,YAAK,EACrD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,EACpC,UAAU,EAAE,GAAG,GACd,OAAO,CAAC,UAAU,CAAC;IAkBtB;;;OAGG;IACG,gBAAgB,CACpB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,YAAK,EACrD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,CAAC;CAiBvB"}
@@ -1,10 +1,10 @@
1
1
  import { decodeAccessToken } from '../../utils.js';
2
2
  import { getCurrentRequestContext } from '../http/mcp-handler.js';
3
- import { ERROR_CODES, createRpcError } from '../utils/errors.js';
3
+ import { PaymentRequiredError } from '../utils/errors.js';
4
4
  import { isValidScheme } from '../../common/types.js';
5
5
  import { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js';
6
6
  import { extractAuthHeader, stripBearer } from '../utils/request.js';
7
- import { buildPaymentRequired } from '../../x402/facilitator-api.js';
7
+ import { buildPaymentRequired, buildPaymentRequiredForPlans, } from '../../x402/facilitator-api.js';
8
8
  /**
9
9
  * Handles authentication and authorization for MCP requests
10
10
  */
@@ -95,24 +95,57 @@ export class PaywallAuthenticator {
95
95
  // HTTP fallback also failed
96
96
  }
97
97
  }
98
- // Both attempts failed — enrich denial with suggested plans (best-effort)
99
- let plansMsg = '';
98
+ // Both attempts failed — surface a spec-shaped PaymentRequired error
99
+ // (converted in-band to a tool-result error for tools; propagates as a
100
+ // JSON-RPC error for resources/prompts).
101
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl);
102
+ }
103
+ /**
104
+ * Build a spec-shaped {@link PaymentRequiredError} from the agent's plans.
105
+ *
106
+ * Fetches the agent's plans (best-effort) to populate the `accepts` array of
107
+ * the `PaymentRequired` object and a human-readable list of plan names in the
108
+ * error message. Falls back to an empty plan id when no plans can be resolved
109
+ * so the structured shape is still valid.
110
+ *
111
+ * @param agentId - Agent identifier used to look up purchasable plans.
112
+ * @param endpoint - Logical resource URL placed in `PaymentRequired.resource`.
113
+ * @param message - Leading human-readable message (e.g. "Authorization required.").
114
+ * @returns A `PaymentRequiredError` carrying the `PaymentRequired` object.
115
+ */
116
+ async buildPaymentRequiredError(agentId, endpoint, message = 'Payment required.') {
117
+ const planIds = [];
118
+ const names = [];
119
+ let plansLookupFailed = false;
100
120
  try {
101
121
  const plans = await this.payments.agents.getAgentPlans(agentId);
102
- if (plans && Array.isArray(plans.plans) && plans.plans.length > 0) {
103
- const top = plans.plans.slice(0, 3);
104
- const summary = top
105
- .map((p) => `${p.planId || p.id || 'plan'}${p.name ? ` (${p.name})` : ''}`)
106
- .join(', ');
107
- plansMsg = summary ? ` Available plans: ${summary}...` : '';
122
+ if (plans && Array.isArray(plans.plans)) {
123
+ for (const p of plans.plans) {
124
+ const pid = p.planId || p.id;
125
+ if (pid)
126
+ planIds.push(pid);
127
+ if (pid)
128
+ names.push(`${pid}${p.name ? ` (${p.name})` : ''}`);
129
+ }
108
130
  }
109
131
  }
110
- catch {
111
- // Ignore errors fetching plans - best effort only
132
+ catch (error) {
133
+ // Best-effort: a backend failure must not look like a clean "unpaid".
134
+ plansLookupFailed = true;
135
+ console.error(`[x402] Failed to fetch agent plans while building payment-required (agentId=${agentId}): ${error instanceof Error ? error.message : String(error)}`);
112
136
  }
113
- throw createRpcError(ERROR_CODES.PaymentRequired, `Payment required.${plansMsg}`, {
114
- reason: 'invalid',
137
+ const plansMsg = names.length > 0 ? ` Available plans: ${names.slice(0, 3).join(', ')}...` : '';
138
+ const paymentRequired = buildPaymentRequiredForPlans(planIds, {
139
+ endpoint,
140
+ agentId,
141
+ httpVerb: 'POST',
142
+ environment: this.payments.getEnvironmentName(),
115
143
  });
144
+ // When the plans lookup itself failed (backend outage) the `accepts` array
145
+ // falls back to an empty plan id; flag it so a client can't mistake the
146
+ // resulting payment-required for a clean "free / no plan needed" response.
147
+ paymentRequired.error = plansLookupFailed ? 'plans unavailable' : 'payment required';
148
+ return new PaymentRequiredError(paymentRequired, `${message}${plansMsg}`);
116
149
  }
117
150
  /**
118
151
  * Verify permissions against a single endpoint URL.
@@ -140,7 +173,9 @@ export class PaywallAuthenticator {
140
173
  if (!planId || !subscriberAddress) {
141
174
  throw new Error('Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)');
142
175
  }
143
- const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme) ? decodedAccessToken.accepted.scheme : 'nvm:erc4337';
176
+ const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)
177
+ ? decodedAccessToken.accepted.scheme
178
+ : 'nvm:erc4337';
144
179
  const paymentRequired = buildPaymentRequired(planId, {
145
180
  endpoint,
146
181
  agentId,
@@ -162,15 +197,14 @@ export class PaywallAuthenticator {
162
197
  * Authenticate an MCP request
163
198
  */
164
199
  async authenticate(extra, options = {}, agentId, serverName, name, kind, argsOrVars) {
200
+ const logicalUrl = buildLogicalUrl({ kind, serverName, name, argsOrVars });
165
201
  const authHeader = this.extractAuthHeaderFromContext(extra);
166
202
  if (!authHeader) {
167
- throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {
168
- reason: 'missing',
169
- });
203
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.');
170
204
  }
171
205
  return this.verifyWithFallback({
172
206
  accessToken: stripBearer(authHeader),
173
- logicalUrl: buildLogicalUrl({ kind, serverName, name, argsOrVars }),
207
+ logicalUrl,
174
208
  httpUrl: this.buildHttpUrlFromContext(),
175
209
  maxAmount: options.maxAmount ?? 1n,
176
210
  agentId,
@@ -182,15 +216,14 @@ export class PaywallAuthenticator {
182
216
  * Returns an AuthResult compatible with paywall flows (without redeem step).
183
217
  */
184
218
  async authenticateMeta(extra, options = {}, agentId, serverName, method) {
219
+ const logicalUrl = buildLogicalMetaUrl(serverName, method);
185
220
  const authHeader = this.extractAuthHeaderFromContext(extra);
186
221
  if (!authHeader) {
187
- throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {
188
- reason: 'missing',
189
- });
222
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.');
190
223
  }
191
224
  return this.verifyWithFallback({
192
225
  accessToken: stripBearer(authHeader),
193
- logicalUrl: buildLogicalMetaUrl(serverName, method),
226
+ logicalUrl,
194
227
  httpUrl: this.buildHttpUrlFromContext(),
195
228
  maxAmount: options.maxAmount ?? 1n,
196
229
  agentId,
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authentication handler for MCP paywall using X402 tokens\n */\nimport type { Payments } from '../../payments.js'\nimport { decodeAccessToken } from '../../utils.js'\nimport { getCurrentRequestContext } from '../http/mcp-handler.js'\nimport { AuthResult } from '../types/paywall.types.js'\nimport { ERROR_CODES, createRpcError } from '../utils/errors.js'\nimport { Address, isValidScheme } from '../../common/types.js'\nimport { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js'\nimport { extractAuthHeader, stripBearer } from '../utils/request.js'\nimport { buildPaymentRequired, type X402PaymentRequired } from '../../x402/facilitator-api.js'\n\ninterface VerifyContext {\n accessToken: string\n logicalUrl: string\n httpUrl: string | undefined\n maxAmount: bigint\n agentId: string\n planIdOverride?: string\n}\n\n/**\n * Handles authentication and authorization for MCP requests\n */\nexport class PaywallAuthenticator {\n constructor(private payments: Payments) {}\n\n /**\n * Extract authorization header from extra context or AsyncLocalStorage.\n * Tries SDK's extra context first, then falls back to HTTP request context.\n *\n * @param extra - MCP extra context from SDK\n * @returns Authorization header value or undefined\n */\n private extractAuthHeaderFromContext(extra: any): string | undefined {\n // Try to extract auth header from SDK's extra context first\n let authHeader = extractAuthHeader(extra)\n\n if (!authHeader) {\n const requestContext = getCurrentRequestContext()\n if (requestContext?.headers) {\n // Build an extra-like object for extractAuthHeader\n authHeader = extractAuthHeader({ requestInfo: { headers: requestContext.headers } })\n }\n }\n\n return authHeader\n }\n\n /**\n * Build HTTP endpoint URL from request context.\n *\n * @returns HTTP endpoint URL or undefined if context is not available\n */\n private buildHttpUrlFromContext(): string | undefined {\n const requestContext = getCurrentRequestContext()\n if (!requestContext) {\n return undefined\n }\n\n try {\n const host = requestContext.headers?.['host'] || requestContext.headers?.['x-forwarded-host']\n if (!host || typeof host !== 'string') {\n return undefined\n }\n\n const protocol = requestContext.headers?.['x-forwarded-proto'] || 'http'\n const baseUrl = `${protocol}://${host}`\n\n // Use requestContext.url if available (e.g., '/mcp'), otherwise default to '/mcp'\n const path = requestContext.url || '/mcp'\n return `${baseUrl}${path}`\n } catch {\n return undefined\n }\n }\n\n /**\n * Core verification logic shared by authenticate and authenticateMeta.\n * Tries logical URL first, falls back to HTTP URL if available.\n */\n private async verifyWithFallback(ctx: VerifyContext): Promise<AuthResult> {\n const { accessToken, logicalUrl, httpUrl, maxAmount, agentId, planIdOverride } = ctx\n\n // Try logical URL first\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n logicalUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // If logical URL fails and we have an HTTP URL, try that\n }\n\n if (httpUrl) {\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n httpUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // HTTP fallback also failed\n }\n }\n\n // Both attempts failed — enrich denial with suggested plans (best-effort)\n let plansMsg = ''\n try {\n const plans = await this.payments.agents.getAgentPlans(agentId)\n if (plans && Array.isArray(plans.plans) && plans.plans.length > 0) {\n const top = plans.plans.slice(0, 3)\n const summary = top\n .map((p: any) => `${p.planId || p.id || 'plan'}${p.name ? ` (${p.name})` : ''}`)\n .join(', ')\n plansMsg = summary ? ` Available plans: ${summary}...` : ''\n }\n } catch {\n // Ignore errors fetching plans - best effort only\n }\n\n throw createRpcError(ERROR_CODES.PaymentRequired, `Payment required.${plansMsg}`, {\n reason: 'invalid',\n })\n }\n\n /**\n * Verify permissions against a single endpoint URL.\n * Resolves planId from the token or from the agent's plans as fallback.\n */\n private async verifyWithEndpoint(\n accessToken: string,\n endpoint: string,\n agentId: string,\n maxAmount: bigint,\n planIdOverride?: string,\n ): Promise<{ planId: string; subscriberAddress: Address; agentRequest?: any }> {\n const decodedAccessToken = decodeAccessToken(accessToken)\n if (!decodedAccessToken) {\n throw new Error('Invalid access token')\n }\n\n let planId = planIdOverride ?? decodedAccessToken.accepted?.planId\n const subscriberAddress = decodedAccessToken.payload?.authorization?.from\n\n // If planId is not available, try to get it from the agent's plans\n if (!planId) {\n try {\n const agentPlans = await this.payments.agents.getAgentPlans(agentId)\n if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {\n planId = agentPlans.plans[0].planId || agentPlans.plans[0].id\n }\n } catch {\n // Ignore errors fetching plans\n }\n }\n\n if (!planId || !subscriberAddress) {\n throw new Error(\n 'Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)',\n )\n }\n\n const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme) ? decodedAccessToken.accepted.scheme : 'nvm:erc4337'\n const paymentRequired: X402PaymentRequired = buildPaymentRequired(planId, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n scheme,\n environment: this.payments.getEnvironmentName(),\n })\n\n const result = await this.payments.facilitator.verifyPermissions({\n paymentRequired,\n x402AccessToken: accessToken,\n maxAmount,\n })\n\n if (!result.isValid) {\n throw new Error('Permission verification failed')\n }\n\n return { planId, subscriberAddress, agentRequest: result.agentRequest }\n }\n\n /**\n * Authenticate an MCP request\n */\n async authenticate(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n name: string,\n kind: 'tool' | 'resource' | 'prompt',\n argsOrVars: any,\n ): Promise<AuthResult> {\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {\n reason: 'missing',\n })\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl: buildLogicalUrl({ kind, serverName, name, argsOrVars }),\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n\n /**\n * Authenticate generic MCP meta operations (e.g., initialize, tools/list, resources/list, prompts/list).\n * Returns an AuthResult compatible with paywall flows (without redeem step).\n */\n async authenticateMeta(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n method: string,\n ): Promise<AuthResult> {\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {\n reason: 'missing',\n })\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl: buildLogicalMetaUrl(serverName, method),\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n}\n"]}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authentication handler for MCP paywall using X402 tokens\n */\nimport type { Payments } from '../../payments.js'\nimport { decodeAccessToken } from '../../utils.js'\nimport { getCurrentRequestContext } from '../http/mcp-handler.js'\nimport { AuthResult } from '../types/paywall.types.js'\nimport { PaymentRequiredError } from '../utils/errors.js'\nimport { Address, isValidScheme } from '../../common/types.js'\nimport { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js'\nimport { extractAuthHeader, stripBearer } from '../utils/request.js'\nimport {\n buildPaymentRequired,\n buildPaymentRequiredForPlans,\n type X402PaymentRequired,\n} from '../../x402/facilitator-api.js'\n\ninterface VerifyContext {\n accessToken: string\n logicalUrl: string\n httpUrl: string | undefined\n maxAmount: bigint\n agentId: string\n planIdOverride?: string\n}\n\n/**\n * Handles authentication and authorization for MCP requests\n */\nexport class PaywallAuthenticator {\n constructor(private payments: Payments) {}\n\n /**\n * Extract authorization header from extra context or AsyncLocalStorage.\n * Tries SDK's extra context first, then falls back to HTTP request context.\n *\n * @param extra - MCP extra context from SDK\n * @returns Authorization header value or undefined\n */\n private extractAuthHeaderFromContext(extra: any): string | undefined {\n // Try to extract auth header from SDK's extra context first\n let authHeader = extractAuthHeader(extra)\n\n if (!authHeader) {\n const requestContext = getCurrentRequestContext()\n if (requestContext?.headers) {\n // Build an extra-like object for extractAuthHeader\n authHeader = extractAuthHeader({ requestInfo: { headers: requestContext.headers } })\n }\n }\n\n return authHeader\n }\n\n /**\n * Build HTTP endpoint URL from request context.\n *\n * @returns HTTP endpoint URL or undefined if context is not available\n */\n private buildHttpUrlFromContext(): string | undefined {\n const requestContext = getCurrentRequestContext()\n if (!requestContext) {\n return undefined\n }\n\n try {\n const host = requestContext.headers?.['host'] || requestContext.headers?.['x-forwarded-host']\n if (!host || typeof host !== 'string') {\n return undefined\n }\n\n const protocol = requestContext.headers?.['x-forwarded-proto'] || 'http'\n const baseUrl = `${protocol}://${host}`\n\n // Use requestContext.url if available (e.g., '/mcp'), otherwise default to '/mcp'\n const path = requestContext.url || '/mcp'\n return `${baseUrl}${path}`\n } catch {\n return undefined\n }\n }\n\n /**\n * Core verification logic shared by authenticate and authenticateMeta.\n * Tries logical URL first, falls back to HTTP URL if available.\n */\n private async verifyWithFallback(ctx: VerifyContext): Promise<AuthResult> {\n const { accessToken, logicalUrl, httpUrl, maxAmount, agentId, planIdOverride } = ctx\n\n // Try logical URL first\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n logicalUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // If logical URL fails and we have an HTTP URL, try that\n }\n\n if (httpUrl) {\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n httpUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // HTTP fallback also failed\n }\n }\n\n // Both attempts failed — surface a spec-shaped PaymentRequired error\n // (converted in-band to a tool-result error for tools; propagates as a\n // JSON-RPC error for resources/prompts).\n throw await this.buildPaymentRequiredError(agentId, logicalUrl)\n }\n\n /**\n * Build a spec-shaped {@link PaymentRequiredError} from the agent's plans.\n *\n * Fetches the agent's plans (best-effort) to populate the `accepts` array of\n * the `PaymentRequired` object and a human-readable list of plan names in the\n * error message. Falls back to an empty plan id when no plans can be resolved\n * so the structured shape is still valid.\n *\n * @param agentId - Agent identifier used to look up purchasable plans.\n * @param endpoint - Logical resource URL placed in `PaymentRequired.resource`.\n * @param message - Leading human-readable message (e.g. \"Authorization required.\").\n * @returns A `PaymentRequiredError` carrying the `PaymentRequired` object.\n */\n private async buildPaymentRequiredError(\n agentId: string,\n endpoint: string,\n message = 'Payment required.',\n ): Promise<PaymentRequiredError> {\n const planIds: string[] = []\n const names: string[] = []\n let plansLookupFailed = false\n try {\n const plans = await this.payments.agents.getAgentPlans(agentId)\n if (plans && Array.isArray(plans.plans)) {\n for (const p of plans.plans) {\n const pid = p.planId || p.id\n if (pid) planIds.push(pid)\n if (pid) names.push(`${pid}${p.name ? ` (${p.name})` : ''}`)\n }\n }\n } catch (error) {\n // Best-effort: a backend failure must not look like a clean \"unpaid\".\n plansLookupFailed = true\n console.error(\n `[x402] Failed to fetch agent plans while building payment-required (agentId=${agentId}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n\n const plansMsg = names.length > 0 ? ` Available plans: ${names.slice(0, 3).join(', ')}...` : ''\n\n const paymentRequired = buildPaymentRequiredForPlans(planIds, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n environment: this.payments.getEnvironmentName(),\n }) as X402PaymentRequired & { error?: string }\n // When the plans lookup itself failed (backend outage) the `accepts` array\n // falls back to an empty plan id; flag it so a client can't mistake the\n // resulting payment-required for a clean \"free / no plan needed\" response.\n paymentRequired.error = plansLookupFailed ? 'plans unavailable' : 'payment required'\n\n return new PaymentRequiredError(paymentRequired, `${message}${plansMsg}`)\n }\n\n /**\n * Verify permissions against a single endpoint URL.\n * Resolves planId from the token or from the agent's plans as fallback.\n */\n private async verifyWithEndpoint(\n accessToken: string,\n endpoint: string,\n agentId: string,\n maxAmount: bigint,\n planIdOverride?: string,\n ): Promise<{ planId: string; subscriberAddress: Address; agentRequest?: any }> {\n const decodedAccessToken = decodeAccessToken(accessToken)\n if (!decodedAccessToken) {\n throw new Error('Invalid access token')\n }\n\n let planId = planIdOverride ?? decodedAccessToken.accepted?.planId\n const subscriberAddress = decodedAccessToken.payload?.authorization?.from\n\n // If planId is not available, try to get it from the agent's plans\n if (!planId) {\n try {\n const agentPlans = await this.payments.agents.getAgentPlans(agentId)\n if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {\n planId = agentPlans.plans[0].planId || agentPlans.plans[0].id\n }\n } catch {\n // Ignore errors fetching plans\n }\n }\n\n if (!planId || !subscriberAddress) {\n throw new Error(\n 'Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)',\n )\n }\n\n const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)\n ? decodedAccessToken.accepted.scheme\n : 'nvm:erc4337'\n const paymentRequired: X402PaymentRequired = buildPaymentRequired(planId, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n scheme,\n environment: this.payments.getEnvironmentName(),\n })\n\n const result = await this.payments.facilitator.verifyPermissions({\n paymentRequired,\n x402AccessToken: accessToken,\n maxAmount,\n })\n\n if (!result.isValid) {\n throw new Error('Permission verification failed')\n }\n\n return { planId, subscriberAddress, agentRequest: result.agentRequest }\n }\n\n /**\n * Authenticate an MCP request\n */\n async authenticate(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n name: string,\n kind: 'tool' | 'resource' | 'prompt',\n argsOrVars: any,\n ): Promise<AuthResult> {\n const logicalUrl = buildLogicalUrl({ kind, serverName, name, argsOrVars })\n\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.')\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl,\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n\n /**\n * Authenticate generic MCP meta operations (e.g., initialize, tools/list, resources/list, prompts/list).\n * Returns an AuthResult compatible with paywall flows (without redeem step).\n */\n async authenticateMeta(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n method: string,\n ): Promise<AuthResult> {\n const logicalUrl = buildLogicalMetaUrl(serverName, method)\n\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.')\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl,\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n}\n"]}
@@ -24,6 +24,12 @@ export declare class PaywallDecorator {
24
24
  * Internal method to create the wrapped handler
25
25
  */
26
26
  private createWrappedHandler;
27
+ /**
28
+ * Build a spec-shaped `PaymentRequired` dict for a settlement failure, from
29
+ * the authenticated request context. Surfaced (with tool content suppressed)
30
+ * when settlement fails after the tool has executed.
31
+ */
32
+ private buildPaymentRequiredFromAuth;
27
33
  /**
28
34
  * Redeem credits after successful request
29
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/paywall.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAOjD,OAAO,EACL,SAAS,EAET,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAE7D;;GAEG;AACH,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,cAAc;IARxB,OAAO,CAAC,MAAM,CAGb;gBAGS,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,oBAAoB,EACnC,cAAc,EAAE,sBAAsB;IAGhD;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IAOnC;;OAEG;IAEH,OAAO,CAAC,KAAK,GAAG,GAAG,EACjB,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACzD,OAAO,EAAE,WAAW,GAAG,aAAa,GACnC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAC7C,OAAO,CACL,OAAO,EAAE,CACP,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAC5C,KAAK,CAAC,EAAE,GAAG,KACR,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACvB,OAAO,EAAE,eAAe,GACvB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAKxF;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiH5B;;OAEG;YACW,aAAa;CAoE5B"}
1
+ {"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/paywall.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAQjD,OAAO,EAEL,SAAS,EAET,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAalC,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAM7D;;GAEG;AACH,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,cAAc;IARxB,OAAO,CAAC,MAAM,CAGb;gBAGS,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,oBAAoB,EACnC,cAAc,EAAE,sBAAsB;IAGhD;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IAOnC;;OAEG;IAEH,OAAO,CAAC,KAAK,GAAG,GAAG,EACjB,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACzD,OAAO,EAAE,WAAW,GAAG,aAAa,GACnC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAC7C,OAAO,CACL,OAAO,EAAE,CACP,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAC5C,KAAK,CAAC,EAAE,GAAG,KACR,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACvB,OAAO,EAAE,eAAe,GACvB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAKxF;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA0K5B;;;;OAIG;IACH,OAAO,CAAC,4BAA4B;IAYpC;;OAEG;YACW,aAAa;CAyE5B"}
@@ -2,9 +2,13 @@
2
2
  * Main paywall decorator for MCP handlers (tools, resources, prompts)
3
3
  */
4
4
  import { isValidScheme } from '../../common/types.js';
5
- import { decodeAccessToken } from '../../utils.js';
6
- import { buildPaymentRequired, } from '../../x402/facilitator-api.js';
7
- import { ERROR_CODES, createRpcError } from '../utils/errors.js';
5
+ import { decodeAccessToken, encodeAccessToken } from '../../utils.js';
6
+ import { buildPaymentRequired, buildPaymentRequiredForPlans, } from '../../x402/facilitator-api.js';
7
+ import { ERROR_CODES, PaymentRequiredError, SettlementFailedError, createRpcError, } from '../utils/errors.js';
8
+ import { NEVERMINED_CREDITS_META_KEY, X402_PAYMENT_RESPONSE_META_KEY, paymentRequiredResult, readPaymentPayload, } from '../utils/meta.js';
9
+ // Emit the Authorization-header deprecation notice at most once per process to
10
+ // avoid log spam on high-traffic servers still using the legacy header path.
11
+ let authHeaderDeprecationWarned = false;
8
12
  /**
9
13
  * Main class for creating paywall-protected MCP handlers
10
14
  */
@@ -46,81 +50,150 @@ export class PaywallDecorator {
46
50
  const isResource = allArgs.length >= 2 && allArgs[0] instanceof URL;
47
51
  const extra = isResource ? allArgs[2] : allArgs[1];
48
52
  const argsOrVars = isResource ? allArgs[1] : allArgs[0];
49
- // 1. Authenticate request
50
- const authResult = await this.authenticator.authenticate(extra, { planId: options?.planId, maxAmount: options?.maxAmount }, this.config.agentId, this.config.serverName, name, kind, argsOrVars);
51
- // 2. Pre-calculate credits if they are fixed (not a function)
52
- // This allows handlers to access credits during execution
53
- const creditsOption = options?.credits;
54
- const isFixedCredits = typeof creditsOption === 'bigint' || creditsOption === undefined;
55
- const preCalculatedCredits = isFixedCredits
56
- ? this.creditsContext.resolve(creditsOption, argsOrVars, null, authResult)
57
- : undefined;
58
- // Determine effective planId: explicit option overrides token-derived value
59
- const effectivePlanId = options?.planId ?? authResult.planId;
60
- // 3. Build PaywallContext for handler (with extra wrapper for backward compatibility)
61
- const paywallContext = {
62
- authResult,
63
- credits: preCalculatedCredits,
64
- planId: authResult.planId,
65
- subscriberAddress: authResult.subscriberAddress,
66
- agentRequest: authResult.agentRequest,
67
- };
68
- // 4. Execute original handler with context
69
- const result = await handler(...allArgs, paywallContext);
70
- // 5. Resolve final credits to burn (may be different if credits are dynamic)
71
- const credits = isFixedCredits
72
- ? (preCalculatedCredits ?? 1n)
73
- : this.creditsContext.resolve(creditsOption, argsOrVars, result, authResult);
74
- // Update context with final resolved credits
75
- paywallContext.credits = credits;
76
- // 6. If the result is an AsyncIterable (stream), redeem on completion
77
- if (isAsyncIterable(result)) {
78
- const onFinally = async () => {
79
- return await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, authResult.httpUrl, 'POST');
53
+ try {
54
+ // x402 v2 MCP transport: prefer the in-band payment payload from
55
+ // params._meta["x402/payment"]. Re-encode it into the access token
56
+ // string the verify/settle path expects and present it via the same
57
+ // extra/headers shape the auth flow reads, so the in-band payload takes
58
+ // precedence over the Authorization header (kept as a deprecated
59
+ // fallback when the in-band payload is absent). The RAW extra is still
60
+ // forwarded to the user handler below.
61
+ const paymentPayload = readPaymentPayload(extra);
62
+ let authExtra = extra;
63
+ if (paymentPayload) {
64
+ const token = encodeAccessToken(paymentPayload);
65
+ // Synthesize an auth-only extra carrying the in-band token. This
66
+ // intentionally drops the rest of `extra` for the AUTH call only; the
67
+ // RAW `extra` (with `_meta`) is still forwarded to the user handler below.
68
+ authExtra = { requestInfo: { headers: { authorization: `Bearer ${token}` } } };
69
+ }
70
+ else if (!authHeaderDeprecationWarned) {
71
+ authHeaderDeprecationWarned = true;
72
+ console.warn('[x402] No _meta["x402/payment"] on the MCP request; falling back to the ' +
73
+ 'Authorization header (deprecated under the x402 v2 MCP transport).');
74
+ }
75
+ // 1. Authenticate request
76
+ const authResult = await this.authenticator.authenticate(authExtra, { planId: options?.planId, maxAmount: options?.maxAmount }, this.config.agentId, this.config.serverName, name, kind, argsOrVars);
77
+ // 2. Pre-calculate credits if they are fixed (not a function)
78
+ // This allows handlers to access credits during execution
79
+ const creditsOption = options?.credits;
80
+ const isFixedCredits = typeof creditsOption === 'bigint' || creditsOption === undefined;
81
+ const preCalculatedCredits = isFixedCredits
82
+ ? this.creditsContext.resolve(creditsOption, argsOrVars, null, authResult)
83
+ : undefined;
84
+ // Determine effective planId: explicit option overrides token-derived value
85
+ const effectivePlanId = options?.planId ?? authResult.planId;
86
+ // 3. Build PaywallContext for handler (with extra wrapper for backward compatibility)
87
+ const paywallContext = {
88
+ authResult,
89
+ credits: preCalculatedCredits,
90
+ planId: authResult.planId,
91
+ subscriberAddress: authResult.subscriberAddress,
92
+ agentRequest: authResult.agentRequest,
80
93
  };
81
- return wrapAsyncIterable(result, onFinally, effectivePlanId, authResult.subscriberAddress, credits);
94
+ // 4. Execute original handler with context
95
+ const result = await handler(...allArgs, paywallContext);
96
+ // 5. Resolve final credits to burn (may be different if credits are dynamic)
97
+ const credits = isFixedCredits
98
+ ? (preCalculatedCredits ?? 1n)
99
+ : this.creditsContext.resolve(creditsOption, argsOrVars, result, authResult);
100
+ // Update context with final resolved credits
101
+ paywallContext.credits = credits;
102
+ // 6. If the result is an AsyncIterable (stream), redeem on completion
103
+ if (isAsyncIterable(result)) {
104
+ const onFinally = async () => {
105
+ return await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, authResult.httpUrl, 'POST');
106
+ };
107
+ return wrapAsyncIterable(result, onFinally, effectivePlanId, authResult.subscriberAddress, credits);
108
+ }
109
+ // 7. Non-streaming: redeem immediately
110
+ const creditsResult = await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl,
111
+ // fix: pre-existing arg order — fallbackEndpoint=httpUrl, httpVerb='POST'
112
+ // (matches the streaming site above)
113
+ authResult.httpUrl, 'POST');
114
+ // Settlement failed AFTER the tool executed: per the x402 v2 MCP
115
+ // transport spec, do NOT return the tool's content — surface only the
116
+ // payment error so a paid result is never delivered without payment
117
+ // landing. (onRedeemError "ignore" therefore no longer delivers paid
118
+ // content; "propagate" already threw a Misconfiguration in redeemCredits.)
119
+ if (creditsResult && !creditsResult.success) {
120
+ console.error(`[x402] settlement failed after tool execution; suppressing tool content. reason=${creditsResult.errorReason}`);
121
+ throw new SettlementFailedError(this.buildPaymentRequiredFromAuth(authResult));
122
+ }
123
+ // creditsResult is undefined for free / no-credit calls (no settlement
124
+ // performed) — in that case the spec receipt is omitted. On success the
125
+ // full receipt goes under the spec key; Nevermined observability is kept
126
+ // under a namespaced key so it never collides with the spec shape.
127
+ result._meta = {
128
+ ...result._meta,
129
+ ...(creditsResult && { [X402_PAYMENT_RESPONSE_META_KEY]: creditsResult }),
130
+ [NEVERMINED_CREDITS_META_KEY]: {
131
+ ...(creditsResult?.transaction && { txHash: creditsResult.transaction }),
132
+ creditsRedeemed: creditsResult?.success
133
+ ? (creditsResult.creditsRedeemed ?? credits.toString())
134
+ : '0',
135
+ remainingBalance: creditsResult?.remainingBalance,
136
+ planId: authResult.planId,
137
+ subscriberAddress: authResult.subscriberAddress,
138
+ success: creditsResult ? creditsResult.success : true,
139
+ },
140
+ };
141
+ return result;
142
+ }
143
+ catch (error) {
144
+ // Payment-required (pre-execution, from auth) and settlement-failure
145
+ // (post-execution) are surfaced in band as an error tool result for
146
+ // tools. Resources/prompts have no tool-result error channel, so the
147
+ // error propagates as a JSON-RPC error instead.
148
+ if (error instanceof PaymentRequiredError && kind === 'tool') {
149
+ return paymentRequiredResult(error.paymentRequired);
150
+ }
151
+ throw error;
82
152
  }
83
- // 7. Non-streaming: redeem immediately
84
- const creditsResult = await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, 'POST', authResult.httpUrl);
85
- result._meta = {
86
- ...result._meta,
87
- ...(creditsResult.transaction && { txHash: creditsResult.transaction }),
88
- creditsRedeemed: creditsResult.success ? (creditsResult.creditsRedeemed ?? credits.toString()) : '0',
89
- remainingBalance: creditsResult.remainingBalance,
90
- planId: authResult.planId,
91
- subscriberAddress: authResult.subscriberAddress,
92
- success: creditsResult.success,
93
- ...(creditsResult.errorReason && { errorReason: creditsResult.errorReason }),
94
- };
95
- return result;
96
153
  };
97
154
  }
155
+ /**
156
+ * Build a spec-shaped `PaymentRequired` dict for a settlement failure, from
157
+ * the authenticated request context. Surfaced (with tool content suppressed)
158
+ * when settlement fails after the tool has executed.
159
+ */
160
+ buildPaymentRequiredFromAuth(authResult) {
161
+ const planId = authResult.planId || '';
162
+ const paymentRequired = buildPaymentRequiredForPlans(planId ? [planId] : [''], {
163
+ endpoint: authResult.logicalUrl || authResult.httpUrl,
164
+ agentId: authResult.agentId,
165
+ httpVerb: 'POST',
166
+ environment: this.payments.getEnvironmentName(),
167
+ });
168
+ paymentRequired.error = 'settlement failed';
169
+ return paymentRequired;
170
+ }
98
171
  /**
99
172
  * Redeem credits after successful request
100
173
  */
101
174
  async redeemCredits(planId, token, subscriberAddress, credits, options, agentId, endpoint, fallbackEndpoint, httpVerb) {
102
- let ret = {
103
- success: true,
104
- transaction: '',
105
- network: '',
106
- };
175
+ // No settlement for free / no-credit calls — signalled to the caller as
176
+ // `undefined` so the spec receipt (_meta["x402/payment-response"]) is omitted.
177
+ if (!(credits && credits > 0n && subscriberAddress && planId)) {
178
+ return undefined;
179
+ }
107
180
  const decoded = decodeAccessToken(token);
108
- const scheme = isValidScheme(decoded?.accepted?.scheme) ? decoded.accepted.scheme : 'nvm:erc4337';
181
+ const scheme = isValidScheme(decoded?.accepted?.scheme)
182
+ ? decoded.accepted.scheme
183
+ : 'nvm:erc4337';
109
184
  try {
110
- if (credits && credits > 0n && subscriberAddress && planId) {
111
- const paymentRequired = buildPaymentRequired(planId, {
112
- endpoint: endpoint || '',
113
- agentId,
114
- httpVerb,
115
- scheme,
116
- environment: this.payments.getEnvironmentName(),
117
- });
118
- ret = await this.payments.facilitator.settlePermissions({
119
- paymentRequired,
120
- x402AccessToken: token,
121
- maxAmount: credits,
122
- });
123
- }
185
+ const paymentRequired = buildPaymentRequired(planId, {
186
+ endpoint: endpoint || '',
187
+ agentId,
188
+ httpVerb,
189
+ scheme,
190
+ environment: this.payments.getEnvironmentName(),
191
+ });
192
+ return await this.payments.facilitator.settlePermissions({
193
+ paymentRequired,
194
+ x402AccessToken: token,
195
+ maxAmount: credits,
196
+ });
124
197
  }
125
198
  catch (primaryError) {
126
199
  // If logical URL fails and we have an HTTP URL fallback, retry with it
@@ -134,26 +207,27 @@ export class PaywallDecorator {
134
207
  scheme,
135
208
  environment: this.payments.getEnvironmentName(),
136
209
  });
137
- ret = await this.payments.facilitator.settlePermissions({
210
+ return await this.payments.facilitator.settlePermissions({
138
211
  paymentRequired,
139
212
  x402AccessToken: token,
140
213
  maxAmount: credits,
141
214
  });
142
- return ret;
143
215
  }
144
216
  catch (fallbackError) {
145
217
  // Fallback also failed, use fallback error as the reported error
146
218
  lastError = fallbackError;
147
219
  }
148
220
  }
149
- ret.success = false;
150
- ret.errorReason = lastError instanceof Error ? lastError.message : String(lastError);
221
+ const errorReason = lastError instanceof Error ? lastError.message : String(lastError);
222
+ console.error(`[x402] settle failed: ${errorReason}`);
151
223
  if (options.onRedeemError === 'propagate') {
152
- throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${ret.errorReason}`);
224
+ throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${errorReason}`);
153
225
  }
154
- // Default: attach error to result but don't throw
226
+ // Default ("ignore"): return a failed result so the caller suppresses the
227
+ // tool content and surfaces the in-band payment error (always-suppress
228
+ // under the x402 v2 MCP transport).
229
+ return { success: false, transaction: '', network: '', errorReason };
155
230
  }
156
- return ret;
157
231
  }
158
232
  }
159
233
  /**
@@ -176,17 +250,29 @@ function wrapAsyncIterable(iterable, onFinally, planId, subscriberAddress, credi
176
250
  finally {
177
251
  creditsResult = await onFinally();
178
252
  }
179
- // Yield a _meta chunk at the end with the redemption result
253
+ // Yield a _meta chunk at the end with the redemption result.
254
+ // NOTE: a stream cannot retroactively suppress already-yielded chunks, so a
255
+ // post-execution settlement failure on a stream is only reported here in the
256
+ // final _meta chunk (under nevermined/credits) — it cannot withhold content
257
+ // the way a non-streaming tool result does. `creditsResult` is undefined for
258
+ // free / no-credit calls.
259
+ const settlement = creditsResult || undefined;
180
260
  const metadataChunk = {
181
261
  _meta: {
182
- // Only include txHash if it has a value
183
- ...(creditsResult?.transaction && { txHash: creditsResult.transaction }),
184
- creditsRedeemed: creditsResult?.success ? (creditsResult.creditsRedeemed ?? credits.toString()) : '0',
185
- remainingBalance: creditsResult?.remainingBalance,
186
- planId,
187
- subscriberAddress,
188
- success: creditsResult?.success || false,
189
- ...(creditsResult?.errorReason && { errorReason: creditsResult.errorReason }),
262
+ // Spec receipt only on a successful settlement.
263
+ ...(settlement?.success && { [X402_PAYMENT_RESPONSE_META_KEY]: settlement }),
264
+ // Nevermined-namespaced observability (NOT part of the x402 spec).
265
+ [NEVERMINED_CREDITS_META_KEY]: {
266
+ ...(settlement?.transaction && { txHash: settlement.transaction }),
267
+ creditsRedeemed: settlement?.success
268
+ ? (settlement.creditsRedeemed ?? credits.toString())
269
+ : '0',
270
+ remainingBalance: settlement?.remainingBalance,
271
+ planId,
272
+ subscriberAddress,
273
+ success: settlement ? settlement.success : true,
274
+ ...(settlement?.errorReason && { errorReason: settlement.errorReason }),
275
+ },
190
276
  },
191
277
  };
192
278
  yield metadataChunk;