@nevermined-io/payments 1.7.0 → 1.9.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.
- package/README.md +2 -1
- package/dist/a2a/agent-card.d.ts +26 -0
- package/dist/a2a/agent-card.d.ts.map +1 -1
- package/dist/a2a/agent-card.js +36 -1
- package/dist/a2a/agent-card.js.map +1 -1
- package/dist/a2a/paymentsClient.d.ts +41 -1
- package/dist/a2a/paymentsClient.d.ts.map +1 -1
- package/dist/a2a/paymentsClient.js +120 -8
- package/dist/a2a/paymentsClient.js.map +1 -1
- package/dist/a2a/paymentsRequestHandler.d.ts +25 -2
- package/dist/a2a/paymentsRequestHandler.d.ts.map +1 -1
- package/dist/a2a/paymentsRequestHandler.js +240 -20
- package/dist/a2a/paymentsRequestHandler.js.map +1 -1
- package/dist/a2a/server.d.ts +2 -2
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +70 -20
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/types.d.ts +31 -1
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/a2a/x402-a2a.d.ts +142 -0
- package/dist/a2a/x402-a2a.d.ts.map +1 -0
- package/dist/a2a/x402-a2a.js +254 -0
- package/dist/a2a/x402-a2a.js.map +1 -0
- package/dist/api/agents-api.d.ts +19 -0
- package/dist/api/agents-api.d.ts.map +1 -1
- package/dist/api/agents-api.js +28 -1
- package/dist/api/agents-api.js.map +1 -1
- package/dist/api/nvm-api.d.ts +1 -0
- package/dist/api/nvm-api.d.ts.map +1 -1
- package/dist/api/nvm-api.js +4 -0
- package/dist/api/nvm-api.js.map +1 -1
- package/dist/api/plans-api.d.ts +12 -2
- package/dist/api/plans-api.d.ts.map +1 -1
- package/dist/api/plans-api.js +9 -2
- package/dist/api/plans-api.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/core/auth.d.ts +16 -2
- package/dist/mcp/core/auth.d.ts.map +1 -1
- package/dist/mcp/core/auth.js +70 -26
- package/dist/mcp/core/auth.js.map +1 -1
- package/dist/mcp/core/paywall.d.ts +6 -0
- package/dist/mcp/core/paywall.d.ts.map +1 -1
- package/dist/mcp/core/paywall.js +179 -89
- package/dist/mcp/core/paywall.js.map +1 -1
- package/dist/mcp/core/server-manager.d.ts.map +1 -1
- package/dist/mcp/core/server-manager.js +6 -4
- package/dist/mcp/core/server-manager.js.map +1 -1
- package/dist/mcp/http/client-registration.d.ts.map +1 -1
- package/dist/mcp/http/client-registration.js +4 -2
- package/dist/mcp/http/client-registration.js.map +1 -1
- package/dist/mcp/http/oauth-metadata.d.ts.map +1 -1
- package/dist/mcp/http/oauth-metadata.js +2 -1
- package/dist/mcp/http/oauth-metadata.js.map +1 -1
- package/dist/mcp/index.d.ts +12 -5
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +46 -23
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/types/http.types.d.ts +2 -2
- package/dist/mcp/types/http.types.d.ts.map +1 -1
- package/dist/mcp/types/http.types.js.map +1 -1
- package/dist/mcp/types/paywall.types.d.ts +6 -2
- package/dist/mcp/types/paywall.types.d.ts.map +1 -1
- package/dist/mcp/types/paywall.types.js.map +1 -1
- package/dist/mcp/types/server.types.d.ts +4 -2
- package/dist/mcp/types/server.types.d.ts.map +1 -1
- package/dist/mcp/types/server.types.js.map +1 -1
- package/dist/mcp/utils/errors.d.ts +26 -0
- package/dist/mcp/utils/errors.d.ts.map +1 -1
- package/dist/mcp/utils/errors.js +32 -0
- package/dist/mcp/utils/errors.js.map +1 -1
- package/dist/mcp/utils/meta.d.ts +54 -0
- package/dist/mcp/utils/meta.d.ts.map +1 -0
- package/dist/mcp/utils/meta.js +72 -0
- package/dist/mcp/utils/meta.js.map +1 -0
- package/dist/payments.d.ts.map +1 -1
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -0
- package/dist/utils.js.map +1 -1
- package/dist/x402/facilitator-api.d.ts +21 -0
- package/dist/x402/facilitator-api.d.ts.map +1 -1
- package/dist/x402/facilitator-api.js +39 -0
- package/dist/x402/facilitator-api.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp/core/auth.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { decodeAccessToken } from '../../utils.js';
|
|
2
2
|
import { getCurrentRequestContext } from '../http/mcp-handler.js';
|
|
3
|
-
import {
|
|
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,67 @@ export class PaywallAuthenticator {
|
|
|
95
95
|
// HTTP fallback also failed
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
// Both attempts failed —
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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, 'Payment required.', planIdOverride);
|
|
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.', fallbackPlanId) {
|
|
117
|
+
const planIds = [];
|
|
118
|
+
const names = [];
|
|
119
|
+
let plansLookupFailed = false;
|
|
120
|
+
// Only look up the agent's plans when an agentId is configured. Under the
|
|
121
|
+
// plan-centric model agentId is optional, so we advertise the configured
|
|
122
|
+
// plan directly (below) instead of requiring an agent lookup.
|
|
123
|
+
if (agentId) {
|
|
124
|
+
try {
|
|
125
|
+
const plans = await this.payments.agents.getAgentPlans(agentId);
|
|
126
|
+
if (plans && Array.isArray(plans.plans)) {
|
|
127
|
+
for (const p of plans.plans) {
|
|
128
|
+
const pid = p.planId || p.id;
|
|
129
|
+
if (pid)
|
|
130
|
+
planIds.push(pid);
|
|
131
|
+
if (pid)
|
|
132
|
+
names.push(`${pid}${p.name ? ` (${p.name})` : ''}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Best-effort: a backend failure must not look like a clean "unpaid".
|
|
138
|
+
plansLookupFailed = true;
|
|
139
|
+
console.error(`[x402] Failed to fetch agent plans while building payment-required (agentId=${agentId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
108
140
|
}
|
|
109
141
|
}
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
// Plan-centric fallback: advertise the configured plan when no plans were
|
|
143
|
+
// resolved via the agent (or no agentId was provided).
|
|
144
|
+
if (planIds.length === 0 && fallbackPlanId) {
|
|
145
|
+
planIds.push(fallbackPlanId);
|
|
112
146
|
}
|
|
113
|
-
|
|
114
|
-
|
|
147
|
+
const plansMsg = names.length > 0 ? ` Available plans: ${names.slice(0, 3).join(', ')}...` : '';
|
|
148
|
+
const paymentRequired = buildPaymentRequiredForPlans(planIds, {
|
|
149
|
+
endpoint,
|
|
150
|
+
agentId,
|
|
151
|
+
httpVerb: 'POST',
|
|
152
|
+
environment: this.payments.getEnvironmentName(),
|
|
115
153
|
});
|
|
154
|
+
// When the plans lookup itself failed (backend outage) the `accepts` array
|
|
155
|
+
// falls back to an empty plan id; flag it so a client can't mistake the
|
|
156
|
+
// resulting payment-required for a clean "free / no plan needed" response.
|
|
157
|
+
paymentRequired.error = plansLookupFailed ? 'plans unavailable' : 'payment required';
|
|
158
|
+
return new PaymentRequiredError(paymentRequired, `${message}${plansMsg}`);
|
|
116
159
|
}
|
|
117
160
|
/**
|
|
118
161
|
* Verify permissions against a single endpoint URL.
|
|
@@ -126,7 +169,8 @@ export class PaywallAuthenticator {
|
|
|
126
169
|
let planId = planIdOverride ?? decodedAccessToken.accepted?.planId;
|
|
127
170
|
const subscriberAddress = decodedAccessToken.payload?.authorization?.from;
|
|
128
171
|
// If planId is not available, try to get it from the agent's plans
|
|
129
|
-
|
|
172
|
+
// (only possible when an agentId is configured).
|
|
173
|
+
if (!planId && agentId) {
|
|
130
174
|
try {
|
|
131
175
|
const agentPlans = await this.payments.agents.getAgentPlans(agentId);
|
|
132
176
|
if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {
|
|
@@ -140,7 +184,9 @@ export class PaywallAuthenticator {
|
|
|
140
184
|
if (!planId || !subscriberAddress) {
|
|
141
185
|
throw new Error('Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)');
|
|
142
186
|
}
|
|
143
|
-
const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)
|
|
187
|
+
const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)
|
|
188
|
+
? decodedAccessToken.accepted.scheme
|
|
189
|
+
: 'nvm:erc4337';
|
|
144
190
|
const paymentRequired = buildPaymentRequired(planId, {
|
|
145
191
|
endpoint,
|
|
146
192
|
agentId,
|
|
@@ -162,15 +208,14 @@ export class PaywallAuthenticator {
|
|
|
162
208
|
* Authenticate an MCP request
|
|
163
209
|
*/
|
|
164
210
|
async authenticate(extra, options = {}, agentId, serverName, name, kind, argsOrVars) {
|
|
211
|
+
const logicalUrl = buildLogicalUrl({ kind, serverName, name, argsOrVars });
|
|
165
212
|
const authHeader = this.extractAuthHeaderFromContext(extra);
|
|
166
213
|
if (!authHeader) {
|
|
167
|
-
throw
|
|
168
|
-
reason: 'missing',
|
|
169
|
-
});
|
|
214
|
+
throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.', options.planId);
|
|
170
215
|
}
|
|
171
216
|
return this.verifyWithFallback({
|
|
172
217
|
accessToken: stripBearer(authHeader),
|
|
173
|
-
logicalUrl
|
|
218
|
+
logicalUrl,
|
|
174
219
|
httpUrl: this.buildHttpUrlFromContext(),
|
|
175
220
|
maxAmount: options.maxAmount ?? 1n,
|
|
176
221
|
agentId,
|
|
@@ -182,15 +227,14 @@ export class PaywallAuthenticator {
|
|
|
182
227
|
* Returns an AuthResult compatible with paywall flows (without redeem step).
|
|
183
228
|
*/
|
|
184
229
|
async authenticateMeta(extra, options = {}, agentId, serverName, method) {
|
|
230
|
+
const logicalUrl = buildLogicalMetaUrl(serverName, method);
|
|
185
231
|
const authHeader = this.extractAuthHeaderFromContext(extra);
|
|
186
232
|
if (!authHeader) {
|
|
187
|
-
throw
|
|
188
|
-
reason: 'missing',
|
|
189
|
-
});
|
|
233
|
+
throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.', options.planId);
|
|
190
234
|
}
|
|
191
235
|
return this.verifyWithFallback({
|
|
192
236
|
accessToken: stripBearer(authHeader),
|
|
193
|
-
logicalUrl
|
|
237
|
+
logicalUrl,
|
|
194
238
|
httpUrl: this.buildHttpUrlFromContext(),
|
|
195
239
|
maxAmount: options.maxAmount ?? 1n,
|
|
196
240
|
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, 'Payment required.', planIdOverride)\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 | undefined,\n endpoint: string,\n message = 'Payment required.',\n fallbackPlanId?: string,\n ): Promise<PaymentRequiredError> {\n const planIds: string[] = []\n const names: string[] = []\n let plansLookupFailed = false\n // Only look up the agent's plans when an agentId is configured. Under the\n // plan-centric model agentId is optional, so we advertise the configured\n // plan directly (below) instead of requiring an agent lookup.\n if (agentId) {\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\n // Plan-centric fallback: advertise the configured plan when no plans were\n // resolved via the agent (or no agentId was provided).\n if (planIds.length === 0 && fallbackPlanId) {\n planIds.push(fallbackPlanId)\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 | undefined,\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 // (only possible when an agentId is configured).\n if (!planId && agentId) {\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 | undefined,\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(\n agentId,\n logicalUrl,\n 'Authorization required.',\n options.planId,\n )\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 | undefined,\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(\n agentId,\n logicalUrl,\n 'Authorization required.',\n options.planId,\n )\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;
|
|
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;IAQnC;;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;IA6K5B;;;;OAIG;IACH,OAAO,CAAC,4BAA4B;IAYpC;;OAEG;YACW,aAAa;CAyE5B"}
|
package/dist/mcp/core/paywall.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -15,7 +19,7 @@ export class PaywallDecorator {
|
|
|
15
19
|
this.creditsContext = creditsContext;
|
|
16
20
|
// Internal config ensures serverName is always a concrete string
|
|
17
21
|
this.config = {
|
|
18
|
-
|
|
22
|
+
planId: '',
|
|
19
23
|
serverName: 'mcp-server',
|
|
20
24
|
};
|
|
21
25
|
}
|
|
@@ -24,7 +28,8 @@ export class PaywallDecorator {
|
|
|
24
28
|
*/
|
|
25
29
|
configure(options) {
|
|
26
30
|
this.config = {
|
|
27
|
-
|
|
31
|
+
planId: options.planId || this.config.planId,
|
|
32
|
+
agentId: options.agentId ?? this.config.agentId,
|
|
28
33
|
serverName: options.serverName ?? this.config.serverName,
|
|
29
34
|
};
|
|
30
35
|
}
|
|
@@ -36,9 +41,12 @@ export class PaywallDecorator {
|
|
|
36
41
|
*/
|
|
37
42
|
createWrappedHandler(handler, options) {
|
|
38
43
|
return async (...allArgs) => {
|
|
39
|
-
// Validate configuration
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// Validate configuration: a planId must be resolvable (per-tool option or
|
|
45
|
+
// server-level config). agentId is optional under the plan-centric model
|
|
46
|
+
// (the facilitator resolves everything from planId + token).
|
|
47
|
+
const configuredPlanId = options?.planId ?? this.config.planId;
|
|
48
|
+
if (!configuredPlanId) {
|
|
49
|
+
throw createRpcError(ERROR_CODES.Misconfiguration, 'Server misconfiguration: missing planId');
|
|
42
50
|
}
|
|
43
51
|
const kind = options?.kind ?? 'tool';
|
|
44
52
|
const name = options?.name ?? 'unnamed';
|
|
@@ -46,81 +54,150 @@ export class PaywallDecorator {
|
|
|
46
54
|
const isResource = allArgs.length >= 2 && allArgs[0] instanceof URL;
|
|
47
55
|
const extra = isResource ? allArgs[2] : allArgs[1];
|
|
48
56
|
const argsOrVars = isResource ? allArgs[1] : allArgs[0];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
try {
|
|
58
|
+
// x402 v2 MCP transport: prefer the in-band payment payload from
|
|
59
|
+
// params._meta["x402/payment"]. Re-encode it into the access token
|
|
60
|
+
// string the verify/settle path expects and present it via the same
|
|
61
|
+
// extra/headers shape the auth flow reads, so the in-band payload takes
|
|
62
|
+
// precedence over the Authorization header (kept as a deprecated
|
|
63
|
+
// fallback when the in-band payload is absent). The RAW extra is still
|
|
64
|
+
// forwarded to the user handler below.
|
|
65
|
+
const paymentPayload = readPaymentPayload(extra);
|
|
66
|
+
let authExtra = extra;
|
|
67
|
+
if (paymentPayload) {
|
|
68
|
+
const token = encodeAccessToken(paymentPayload);
|
|
69
|
+
// Synthesize an auth-only extra carrying the in-band token. This
|
|
70
|
+
// intentionally drops the rest of `extra` for the AUTH call only; the
|
|
71
|
+
// RAW `extra` (with `_meta`) is still forwarded to the user handler below.
|
|
72
|
+
authExtra = { requestInfo: { headers: { authorization: `Bearer ${token}` } } };
|
|
73
|
+
}
|
|
74
|
+
else if (!authHeaderDeprecationWarned) {
|
|
75
|
+
authHeaderDeprecationWarned = true;
|
|
76
|
+
console.warn('[x402] No _meta["x402/payment"] on the MCP request; falling back to the ' +
|
|
77
|
+
'Authorization header (deprecated under the x402 v2 MCP transport).');
|
|
78
|
+
}
|
|
79
|
+
// 1. Authenticate request
|
|
80
|
+
const authResult = await this.authenticator.authenticate(authExtra, { planId: configuredPlanId, maxAmount: options?.maxAmount }, this.config.agentId, this.config.serverName, name, kind, argsOrVars);
|
|
81
|
+
// 2. Pre-calculate credits if they are fixed (not a function)
|
|
82
|
+
// This allows handlers to access credits during execution
|
|
83
|
+
const creditsOption = options?.credits;
|
|
84
|
+
const isFixedCredits = typeof creditsOption === 'bigint' || creditsOption === undefined;
|
|
85
|
+
const preCalculatedCredits = isFixedCredits
|
|
86
|
+
? this.creditsContext.resolve(creditsOption, argsOrVars, null, authResult)
|
|
87
|
+
: undefined;
|
|
88
|
+
// Determine effective planId: explicit option overrides token-derived value
|
|
89
|
+
const effectivePlanId = options?.planId ?? authResult.planId;
|
|
90
|
+
// 3. Build PaywallContext for handler (with extra wrapper for backward compatibility)
|
|
91
|
+
const paywallContext = {
|
|
92
|
+
authResult,
|
|
93
|
+
credits: preCalculatedCredits,
|
|
94
|
+
planId: authResult.planId,
|
|
95
|
+
subscriberAddress: authResult.subscriberAddress,
|
|
96
|
+
agentRequest: authResult.agentRequest,
|
|
80
97
|
};
|
|
81
|
-
|
|
98
|
+
// 4. Execute original handler with context
|
|
99
|
+
const result = await handler(...allArgs, paywallContext);
|
|
100
|
+
// 5. Resolve final credits to burn (may be different if credits are dynamic)
|
|
101
|
+
const credits = isFixedCredits
|
|
102
|
+
? (preCalculatedCredits ?? 1n)
|
|
103
|
+
: this.creditsContext.resolve(creditsOption, argsOrVars, result, authResult);
|
|
104
|
+
// Update context with final resolved credits
|
|
105
|
+
paywallContext.credits = credits;
|
|
106
|
+
// 6. If the result is an AsyncIterable (stream), redeem on completion
|
|
107
|
+
if (isAsyncIterable(result)) {
|
|
108
|
+
const onFinally = async () => {
|
|
109
|
+
return await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, authResult.httpUrl, 'POST');
|
|
110
|
+
};
|
|
111
|
+
return wrapAsyncIterable(result, onFinally, effectivePlanId, authResult.subscriberAddress, credits);
|
|
112
|
+
}
|
|
113
|
+
// 7. Non-streaming: redeem immediately
|
|
114
|
+
const creditsResult = await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl,
|
|
115
|
+
// fix: pre-existing arg order — fallbackEndpoint=httpUrl, httpVerb='POST'
|
|
116
|
+
// (matches the streaming site above)
|
|
117
|
+
authResult.httpUrl, 'POST');
|
|
118
|
+
// Settlement failed AFTER the tool executed: per the x402 v2 MCP
|
|
119
|
+
// transport spec, do NOT return the tool's content — surface only the
|
|
120
|
+
// payment error so a paid result is never delivered without payment
|
|
121
|
+
// landing. (onRedeemError "ignore" therefore no longer delivers paid
|
|
122
|
+
// content; "propagate" already threw a Misconfiguration in redeemCredits.)
|
|
123
|
+
if (creditsResult && !creditsResult.success) {
|
|
124
|
+
console.error(`[x402] settlement failed after tool execution; suppressing tool content. reason=${creditsResult.errorReason}`);
|
|
125
|
+
throw new SettlementFailedError(this.buildPaymentRequiredFromAuth(authResult));
|
|
126
|
+
}
|
|
127
|
+
// creditsResult is undefined for free / no-credit calls (no settlement
|
|
128
|
+
// performed) — in that case the spec receipt is omitted. On success the
|
|
129
|
+
// full receipt goes under the spec key; Nevermined observability is kept
|
|
130
|
+
// under a namespaced key so it never collides with the spec shape.
|
|
131
|
+
result._meta = {
|
|
132
|
+
...result._meta,
|
|
133
|
+
...(creditsResult && { [X402_PAYMENT_RESPONSE_META_KEY]: creditsResult }),
|
|
134
|
+
[NEVERMINED_CREDITS_META_KEY]: {
|
|
135
|
+
...(creditsResult?.transaction && { txHash: creditsResult.transaction }),
|
|
136
|
+
creditsRedeemed: creditsResult?.success
|
|
137
|
+
? (creditsResult.creditsRedeemed ?? credits.toString())
|
|
138
|
+
: '0',
|
|
139
|
+
remainingBalance: creditsResult?.remainingBalance,
|
|
140
|
+
planId: authResult.planId,
|
|
141
|
+
subscriberAddress: authResult.subscriberAddress,
|
|
142
|
+
success: creditsResult ? creditsResult.success : true,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Payment-required (pre-execution, from auth) and settlement-failure
|
|
149
|
+
// (post-execution) are surfaced in band as an error tool result for
|
|
150
|
+
// tools. Resources/prompts have no tool-result error channel, so the
|
|
151
|
+
// error propagates as a JSON-RPC error instead.
|
|
152
|
+
if (error instanceof PaymentRequiredError && kind === 'tool') {
|
|
153
|
+
return paymentRequiredResult(error.paymentRequired);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
82
156
|
}
|
|
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
157
|
};
|
|
97
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Build a spec-shaped `PaymentRequired` dict for a settlement failure, from
|
|
161
|
+
* the authenticated request context. Surfaced (with tool content suppressed)
|
|
162
|
+
* when settlement fails after the tool has executed.
|
|
163
|
+
*/
|
|
164
|
+
buildPaymentRequiredFromAuth(authResult) {
|
|
165
|
+
const planId = authResult.planId || '';
|
|
166
|
+
const paymentRequired = buildPaymentRequiredForPlans(planId ? [planId] : [''], {
|
|
167
|
+
endpoint: authResult.logicalUrl || authResult.httpUrl,
|
|
168
|
+
agentId: authResult.agentId,
|
|
169
|
+
httpVerb: 'POST',
|
|
170
|
+
environment: this.payments.getEnvironmentName(),
|
|
171
|
+
});
|
|
172
|
+
paymentRequired.error = 'settlement failed';
|
|
173
|
+
return paymentRequired;
|
|
174
|
+
}
|
|
98
175
|
/**
|
|
99
176
|
* Redeem credits after successful request
|
|
100
177
|
*/
|
|
101
178
|
async redeemCredits(planId, token, subscriberAddress, credits, options, agentId, endpoint, fallbackEndpoint, httpVerb) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
179
|
+
// No settlement for free / no-credit calls — signalled to the caller as
|
|
180
|
+
// `undefined` so the spec receipt (_meta["x402/payment-response"]) is omitted.
|
|
181
|
+
if (!(credits && credits > 0n && subscriberAddress && planId)) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
107
184
|
const decoded = decodeAccessToken(token);
|
|
108
|
-
const scheme = isValidScheme(decoded?.accepted?.scheme)
|
|
185
|
+
const scheme = isValidScheme(decoded?.accepted?.scheme)
|
|
186
|
+
? decoded.accepted.scheme
|
|
187
|
+
: 'nvm:erc4337';
|
|
109
188
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
}
|
|
189
|
+
const paymentRequired = buildPaymentRequired(planId, {
|
|
190
|
+
endpoint: endpoint || '',
|
|
191
|
+
agentId,
|
|
192
|
+
httpVerb,
|
|
193
|
+
scheme,
|
|
194
|
+
environment: this.payments.getEnvironmentName(),
|
|
195
|
+
});
|
|
196
|
+
return await this.payments.facilitator.settlePermissions({
|
|
197
|
+
paymentRequired,
|
|
198
|
+
x402AccessToken: token,
|
|
199
|
+
maxAmount: credits,
|
|
200
|
+
});
|
|
124
201
|
}
|
|
125
202
|
catch (primaryError) {
|
|
126
203
|
// If logical URL fails and we have an HTTP URL fallback, retry with it
|
|
@@ -134,26 +211,27 @@ export class PaywallDecorator {
|
|
|
134
211
|
scheme,
|
|
135
212
|
environment: this.payments.getEnvironmentName(),
|
|
136
213
|
});
|
|
137
|
-
|
|
214
|
+
return await this.payments.facilitator.settlePermissions({
|
|
138
215
|
paymentRequired,
|
|
139
216
|
x402AccessToken: token,
|
|
140
217
|
maxAmount: credits,
|
|
141
218
|
});
|
|
142
|
-
return ret;
|
|
143
219
|
}
|
|
144
220
|
catch (fallbackError) {
|
|
145
221
|
// Fallback also failed, use fallback error as the reported error
|
|
146
222
|
lastError = fallbackError;
|
|
147
223
|
}
|
|
148
224
|
}
|
|
149
|
-
|
|
150
|
-
|
|
225
|
+
const errorReason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
226
|
+
console.error(`[x402] settle failed: ${errorReason}`);
|
|
151
227
|
if (options.onRedeemError === 'propagate') {
|
|
152
|
-
throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${
|
|
228
|
+
throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${errorReason}`);
|
|
153
229
|
}
|
|
154
|
-
// Default:
|
|
230
|
+
// Default ("ignore"): return a failed result so the caller suppresses the
|
|
231
|
+
// tool content and surfaces the in-band payment error (always-suppress
|
|
232
|
+
// under the x402 v2 MCP transport).
|
|
233
|
+
return { success: false, transaction: '', network: '', errorReason };
|
|
155
234
|
}
|
|
156
|
-
return ret;
|
|
157
235
|
}
|
|
158
236
|
}
|
|
159
237
|
/**
|
|
@@ -176,17 +254,29 @@ function wrapAsyncIterable(iterable, onFinally, planId, subscriberAddress, credi
|
|
|
176
254
|
finally {
|
|
177
255
|
creditsResult = await onFinally();
|
|
178
256
|
}
|
|
179
|
-
// Yield a _meta chunk at the end with the redemption result
|
|
257
|
+
// Yield a _meta chunk at the end with the redemption result.
|
|
258
|
+
// NOTE: a stream cannot retroactively suppress already-yielded chunks, so a
|
|
259
|
+
// post-execution settlement failure on a stream is only reported here in the
|
|
260
|
+
// final _meta chunk (under nevermined/credits) — it cannot withhold content
|
|
261
|
+
// the way a non-streaming tool result does. `creditsResult` is undefined for
|
|
262
|
+
// free / no-credit calls.
|
|
263
|
+
const settlement = creditsResult || undefined;
|
|
180
264
|
const metadataChunk = {
|
|
181
265
|
_meta: {
|
|
182
|
-
//
|
|
183
|
-
...(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
266
|
+
// Spec receipt only on a successful settlement.
|
|
267
|
+
...(settlement?.success && { [X402_PAYMENT_RESPONSE_META_KEY]: settlement }),
|
|
268
|
+
// Nevermined-namespaced observability (NOT part of the x402 spec).
|
|
269
|
+
[NEVERMINED_CREDITS_META_KEY]: {
|
|
270
|
+
...(settlement?.transaction && { txHash: settlement.transaction }),
|
|
271
|
+
creditsRedeemed: settlement?.success
|
|
272
|
+
? (settlement.creditsRedeemed ?? credits.toString())
|
|
273
|
+
: '0',
|
|
274
|
+
remainingBalance: settlement?.remainingBalance,
|
|
275
|
+
planId,
|
|
276
|
+
subscriberAddress,
|
|
277
|
+
success: settlement ? settlement.success : true,
|
|
278
|
+
...(settlement?.errorReason && { errorReason: settlement.errorReason }),
|
|
279
|
+
},
|
|
190
280
|
},
|
|
191
281
|
};
|
|
192
282
|
yield metadataChunk;
|