@nevermined-io/payments 1.12.0 → 1.13.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 +33 -0
- package/dist/a2a/clientRegistry.d.ts +10 -1
- package/dist/a2a/clientRegistry.d.ts.map +1 -1
- package/dist/a2a/clientRegistry.js +13 -4
- package/dist/a2a/clientRegistry.js.map +1 -1
- package/dist/a2a/paymentsClient.d.ts +60 -3
- package/dist/a2a/paymentsClient.d.ts.map +1 -1
- package/dist/a2a/paymentsClient.js +129 -8
- package/dist/a2a/paymentsClient.js.map +1 -1
- package/dist/a2a/types.d.ts +5 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/common/types.d.ts +87 -0
- package/dist/common/types.d.ts.map +1 -1
- package/dist/common/types.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/core/paywall.d.ts.map +1 -1
- package/dist/mcp/core/paywall.js +10 -0
- package/dist/mcp/core/paywall.js.map +1 -1
- package/dist/mpp/fetch.d.ts +8 -3
- package/dist/mpp/fetch.d.ts.map +1 -1
- package/dist/mpp/fetch.js +6 -0
- package/dist/mpp/fetch.js.map +1 -1
- package/dist/mpp/mpp-api.d.ts +11 -5
- package/dist/mpp/mpp-api.d.ts.map +1 -1
- package/dist/mpp/mpp-api.js +10 -3
- package/dist/mpp/mpp-api.js.map +1 -1
- package/dist/x402/express/middleware.d.ts.map +1 -1
- package/dist/x402/express/middleware.js +98 -1
- package/dist/x402/express/middleware.js.map +1 -1
- package/dist/x402/facilitator-api.d.ts +90 -7
- package/dist/x402/facilitator-api.d.ts.map +1 -1
- package/dist/x402/facilitator-api.js +64 -3
- package/dist/x402/facilitator-api.js.map +1 -1
- package/dist/x402/index.d.ts +3 -2
- package/dist/x402/index.d.ts.map +1 -1
- package/dist/x402/index.js +1 -0
- package/dist/x402/index.js.map +1 -1
- package/dist/x402/token-request.d.ts +8 -0
- package/dist/x402/token-request.d.ts.map +1 -1
- package/dist/x402/token-request.js +101 -1
- package/dist/x402/token-request.js.map +1 -1
- package/dist/x402/token-version.d.ts +97 -0
- package/dist/x402/token-version.d.ts.map +1 -0
- package/dist/x402/token-version.js +127 -0
- package/dist/x402/token-version.js.map +1 -0
- package/dist/x402/token.d.ts +33 -2
- package/dist/x402/token.d.ts.map +1 -1
- package/dist/x402/token.js +47 -2
- package/dist/x402/token.js.map +1 -1
- package/package.json +1 -1
package/dist/mpp/mpp-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mpp-api.js","sourceRoot":"","sources":["../../src/mpp/mpp-api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Machine Payments Protocol (MPP) API.\n *\n * MPP is a second payment framing over the unchanged Nevermined core: the same\n * plan, the same delegation and the same credit burn as x402, negotiated with\n * MPP headers. Sellers issue a challenge and redeem a credential; buyers mint\n * an MPP-domain access token and present it as a credential.\n */\n\nimport { BasePaymentsAPI } from '../api/base-payments.js'\nimport {\n API_URL_MPP_CHALLENGE,\n API_URL_MPP_CREATE_PERMISSION,\n API_URL_MPP_SETTLE,\n API_URL_MPP_VERIFY,\n} from '../api/nvm-api.js'\nimport { PaymentsError } from '../common/payments.error.js'\nimport type { PaymentOptions, X402TokenOptions } from '../common/types.js'\nimport type { SettlePermissionsResult, VerifyPermissionsResult } from '../x402/facilitator-api.js'\nimport { buildX402TokenRequestBody } from '../x402/token-request.js'\nimport { MppError, MppSettlementOutcomeUnknownError, toMppError } from './errors.js'\nimport { mppFetch, type MppFetchOptions, type MppFetchResult } from './fetch.js'\n\n/** Only whole, non-negative decimal digits — no sign, no leading/trailing\n * whitespace, no hex/octal/binary prefix, no fractional part. `BigInt(x)`\n * silently accepts all of those for a string input (`BigInt('0x10')` is 16,\n * `BigInt('')` is 0n, `BigInt(' 5 ')` is 5n), which a decimal-string\n * contract must reject rather than accept quietly. */\nconst DECIMAL_INTEGER_STRING = /^\\d+$/\n\n/**\n * Normalizes `credits` to the exact decimal-string amount that gets sealed\n * into the challenge and burned. A bare `.toString()` on a JS `number` can\n * emit scientific notation (`1e+21`), `\"NaN\"` or `\"Infinity\"` — all\n * forwarded unvalidated into an amount this PR's own docs describe as\n * having \"no post-hoc re-pricing as there is with x402\": a corrupted amount\n * here is not correctable downstream, it IS the amount.\n *\n * `BigInt(x)` renders an integer-valued number exactly (no scientific\n * notation) and throws `RangeError` on a non-integer, `NaN` or `Infinity`\n * instead of silently stringifying them — which is why non-string inputs go\n * through it. The `string` arm gets its own check instead of relying on\n * `BigInt`'s string parsing, which is far more permissive than a decimal\n * string contract should be.\n */\nexport function normalizeCredits(credits: string | number | bigint): string {\n if (typeof credits === 'string') {\n if (!DECIMAL_INTEGER_STRING.test(credits)) {\n throw new MppError(\n `credits must be a non-negative integer decimal string, got ${JSON.stringify(credits)}`,\n )\n }\n return credits\n }\n\n let normalized: string\n try {\n normalized = BigInt(credits).toString()\n } catch (error) {\n throw new MppError(\n `credits must be a non-negative integer, got ${credits}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n if (normalized.startsWith('-')) {\n throw new MppError(`credits must be a non-negative integer, got ${credits}`)\n }\n return normalized\n}\n\n/** Deadline on an outbound MPP request so a hung backend cannot hold the\n * caller's connection open indefinitely — `middleware.ts` defers the\n * buyer's own response until settlement resolves. */\nconst REQUEST_TIMEOUT_MS = 30_000\n\n/**\n * Socket-level failures that can only happen once the request was already on\n * the wire, so on a burning call the backend may have completed the burn and\n * only the answer was lost.\n *\n * `ECONNRESET` / `EPIPE` mean the peer tore down (or we wrote into) a\n * connection that had been established; `UND_ERR_SOCKET` is undici's wrapper\n * for the same thing, and `UND_ERR_ABORTED` covers a mid-flight abort that\n * did not come through the `TimeoutError` path.\n */\nconst OUTCOME_UNKNOWN_SOCKET_CODES = new Set([\n 'ECONNRESET',\n 'EPIPE',\n 'UND_ERR_SOCKET',\n 'UND_ERR_ABORTED',\n])\n\n/**\n * Whether a `fetch` rejection is a connection that died AFTER the request was\n * written — i.e. an unknown settlement outcome rather than a definite failure.\n *\n * Deliberately an ALLOW-LIST, not a blanket promotion of every network error.\n * `ECONNREFUSED`, `ENOTFOUND` and `EAI_AGAIN` all mean the request never\n * reached anything that could burn, so reporting them as \"may have burned\"\n * would inflate a seller's records with burns that never happened — the same\n * corruption as the 4xx case one screen down, in the other direction. Any code\n * not named here keeps the definite `network_error` classification.\n *\n * Node's `fetch` surfaces the underlying socket error as `error.cause`, so the\n * code is read from there first and off the error itself as a fallback.\n */\nfunction isPostRequestSocketFailure(error: unknown): boolean {\n if (!(error instanceof Error)) return false\n // `cause` is read off a widened shape rather than `Error.cause`: the\n // package's lib target predates ES2022, so the property is not on the\n // declared `Error` type even though Node populates it.\n const { cause, code: ownCode } = error as unknown as { cause?: unknown; code?: unknown }\n const causeCode = (cause as { code?: unknown } | undefined)?.code\n return (\n (typeof causeCode === 'string' && OUTCOME_UNKNOWN_SOCKET_CODES.has(causeCode)) ||\n (typeof ownCode === 'string' && OUTCOME_UNKNOWN_SOCKET_CODES.has(ownCode))\n )\n}\n\nexport interface IssueMppChallengeParams {\n /** The Nevermined plan the credits are burned against. */\n planId: string\n /** Credits the buyer is asked to redeem. Sent as a decimal string. */\n credits: string | number | bigint\n agentId?: string\n /** The protected resource. Sealed into the challenge and re-asserted at redeem. */\n resource: string\n /** HTTP verb of that resource. Part of the same binding. */\n httpVerb: string\n /** `sha-256=<base64>` digest binding the challenge to one request body. */\n digest?: string\n description?: string\n}\n\nexport interface RedeemMppParams {\n /** The `Authorization: Payment …` value presented by the buyer. */\n credential: string\n /** Must equal the resource the challenge was issued for. */\n resource: string\n /** Must equal the verb the challenge was issued for. */\n httpVerb: string\n /** Digest of the body actually received, when the challenge bound one. */\n bodyDigest?: string\n}\n\n/**\n * A discriminated union rather than a flat `paymentReceipt: string`: a\n * settlement can fail, and `SettlePermissionsResult.success` already says\n * so, but the flat shape let `{ success: false }` with no receipt typecheck\n * — a state the seller middleware then had no way to reject at compile time\n * even though it could not safely attach a receipt header for it.\n */\nexport type MppSettleResult =\n | (SettlePermissionsResult & { success: true; paymentReceipt: string })\n | (SettlePermissionsResult & { success: false; paymentReceipt?: string })\n\nexport class MppAPI extends BasePaymentsAPI {\n static getInstance(options: PaymentOptions): MppAPI {\n return new MppAPI(options)\n }\n\n /**\n * Mints the challenge a plan-protected endpoint returns with its 402.\n *\n * Each call returns a distinct challenge even for identical inputs — the id\n * doubles as the burn idempotency key, so two requests sharing one would\n * settle as a single burn.\n */\n async issueChallenge(\n params: IssueMppChallengeParams,\n ): Promise<{ challenge: string; id: string }> {\n const { planId, credits, agentId, resource, httpVerb, digest, description } = params\n return this.post<{ challenge: string; id: string }>(API_URL_MPP_CHALLENGE, {\n planId,\n credits: normalizeCredits(credits),\n resource,\n httpVerb,\n ...(agentId && { agentId }),\n ...(digest && { digest }),\n ...(description && { description }),\n })\n }\n\n /** Runs the full credential and plan checks without burning anything. */\n async verifyCredential(params: RedeemMppParams): Promise<VerifyPermissionsResult> {\n return this.post<VerifyPermissionsResult>(API_URL_MPP_VERIFY, this.redeemBody(params))\n }\n\n /**\n * Verifies and burns through the same chokepoint an x402 settlement uses, so\n * the credits charged are identical on both protocols. Settling the same\n * credential twice burns once.\n */\n async settleCredential(params: RedeemMppParams): Promise<MppSettleResult> {\n return this.post<MppSettleResult>(API_URL_MPP_SETTLE, this.redeemBody(params), {\n burns: true,\n })\n }\n\n /**\n * Mints an access token signed under the `Nevermined-MPP` EIP-712 domain.\n *\n * Same inputs and same settlement rail as {@link X402TokenAPI.getX402AccessToken};\n * the token verifies only on the MPP routes, which is what keeps the two\n * protocols isolated even though the tokens are byte-identical on the wire.\n */\n async getMppAccessToken(\n planId: string,\n agentId?: string,\n tokenOptions?: X402TokenOptions,\n ): Promise<{ accessToken: string }> {\n const body = buildX402TokenRequestBody({\n planId,\n agentId,\n tokenOptions,\n environmentName: this.environmentName,\n })\n return this.post<{ accessToken: string }>(API_URL_MPP_CREATE_PERMISSION, body)\n }\n\n /**\n * Performs an HTTP request, paying an MPP challenge if the endpoint returns\n * one.\n *\n * The buyer needs no new plan, delegation or credential: the delegation that\n * works for x402 works here unchanged.\n *\n * `init.body`, if set, must be replayable **if the endpoint may challenge\n * the request**: a `ReadableStream` throws a typed {@link PaymentsError}\n * once a 402 challenge actually requires a retry, since the stream cannot\n * be resent. A request that is never challenged sends a stream body exactly\n * once, exactly like plain `fetch` — the `paid: false` / untouched-response\n * guarantee still holds. A `string`, `Buffer`/`ArrayBuffer`/typed array,\n * `URLSearchParams`, `FormData` or `Blob` body all work unchanged either way.\n *\n * `options.delegationConfig` must carry a `delegationId` — this call\n * refuses the deprecated inline create-on-the-fly shape (no `delegationId`)\n * that {@link X402TokenAPI.getX402AccessToken} otherwise tolerates with a\n * warning: the retry loop here can mint an access token twice per call, so\n * that shape could silently create two delegations as a side effect of\n * paying.\n *\n * @example\n * ```typescript\n * const { response, receipt } = await payments.mpp.fetch(\n * 'https://agent.example/ask',\n * { method: 'POST', body: JSON.stringify({ q: 'hello' }) },\n * { delegationConfig: { delegationId }, planId },\n * )\n * ```\n */\n async fetch(\n input: string | URL,\n init: RequestInit | undefined,\n options: MppFetchOptions,\n ): Promise<MppFetchResult> {\n if (!options?.delegationConfig?.delegationId) {\n throw PaymentsError.validation(\n 'payments.mpp.fetch requires delegationConfig.delegationId. Create a delegation first ' +\n 'with payments.delegation.createDelegation(), then pass { delegationId }. An inline ' +\n 'create-on-the-fly delegationConfig (no delegationId) is not accepted here — the retry ' +\n 'loop can mint against it twice per call, which would create two delegations.',\n )\n }\n return mppFetch(\n (planId, agentId, tokenOptions) => this.getMppAccessToken(planId, agentId, tokenOptions),\n input,\n init,\n options,\n )\n }\n\n private redeemBody(params: RedeemMppParams): Record<string, unknown> {\n const { credential, resource, httpVerb, bodyDigest } = params\n return {\n credential,\n resource,\n httpVerb,\n ...(bodyDigest && { bodyDigest }),\n }\n }\n\n /**\n * One place for the POST + error translation both surfaces share.\n *\n * `burns` marks a call where \"no answer\" is not the same as \"did not\n * happen\" — currently only `settleCredential`. For that call alone, a\n * fetch rejection caused by OUR OWN `AbortSignal.timeout()` firing (not\n * some other network failure) is raised as {@link MppSettlementOutcomeUnknownError}\n * instead of the generic `network_error` `MppError` used everywhere else,\n * so a caller can tell \"definitely nothing happened\" apart from \"the\n * backend may have already burned the credits; we just didn't hear back\".\n * `error.name === 'TimeoutError'` combined with `instanceof DOMException`\n * is the documented, empirically-confirmed shape Node's `fetch` throws\n * when an `AbortSignal.timeout()` deadline fires — as opposed to, say,\n * connection-refused, which surfaces as a plain `TypeError`.\n *\n * The same reasoning covers the other two ways a burning call can end\n * without a definite answer: an upstream 5xx/408, and a 2xx whose body\n * could not be read. All three raise {@link MppSettlementOutcomeUnknownError};\n * only a 4xx — the backend deliberately rejecting — is a definite failure.\n */\n private async post<T>(\n path: string,\n body: Record<string, unknown>,\n options: { burns?: boolean } = {},\n ): Promise<T> {\n const url = new URL(path, this.environment.backend)\n const requestOptions = {\n ...this.getBackendHTTPOptions('POST', body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n }\n\n let response: Response\n try {\n response = await fetch(url, requestOptions)\n } catch (error) {\n if (options.burns && error instanceof DOMException && error.name === 'TimeoutError') {\n throw new MppSettlementOutcomeUnknownError()\n }\n if (options.burns && isPostRequestSocketFailure(error)) {\n throw new MppSettlementOutcomeUnknownError(\n `The connection to the backend failed after the settle request was written: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n throw toMppError(\n 'network_error',\n `Network error during MPP request: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n if (!response.ok) {\n let message = `MPP request to ${path} failed`\n let code: string | undefined = `http_${response.status}`\n try {\n const errorData = await response.json()\n if (errorData.message) message = errorData.message\n if (errorData.code) code = errorData.code\n if (errorData.hint) message = `${message} — ${errorData.hint}`\n } catch {\n // Keep the default message.\n }\n // On a burning call, a 5xx (or a 408) is an UNKNOWN outcome, not a\n // failure. A 504 in particular means an intermediary gave up waiting on\n // the backend — it says nothing about whether the burn committed, which\n // is the exact condition MppSettlementOutcomeUnknownError exists for,\n // and the argument the docstring below makes for an unreadable 2xx\n // applies word for word. Classified as a definite failure, the\n // middleware logs \"settlement failed\" and skips onAfterSettle for\n // credits that may well be gone.\n //\n // Confined to 5xx/408: the backend's own rejections (BCK.MPP.0003 and\n // friends) come back as 4xx and ARE definite — reporting those as\n // unknown would corrupt the accounting in the other direction.\n if (options.burns && (response.status >= 500 || response.status === 408)) {\n throw new MppSettlementOutcomeUnknownError(\n `The backend answered ${response.status} without a definite settlement outcome: ${message}`,\n )\n }\n throw toMppError(code, message)\n }\n\n // The success path is not exempt from a malformed body: a WAF\n // interstitial, a gateway HTML page, or a truncated 2xx response would\n // otherwise raise a raw SyntaxError here — the one call site in this\n // method that was NOT already wrapped and converted to a typed error.\n try {\n return (await response.json()) as T\n } catch (error) {\n // On a burning call this is an UNKNOWN outcome, not a failure. The\n // backend already answered 2xx — the burn committed — and only the\n // body was lost (our own AbortSignal.timeout() also aborts the body\n // stream, and a mid-body reset lands here too). Reporting it as a\n // definite failure is exactly the accounting corruption\n // MppSettlementOutcomeUnknownError exists to prevent: the middleware\n // would log \"settlement failed\" and skip onAfterSettle for credits\n // that were in fact burned. If anything this case is MORE likely to\n // have burned than the pre-response timeout the error was introduced\n // for.\n if (options.burns) {\n throw new MppSettlementOutcomeUnknownError(\n `The backend answered ${response.status} but its body could not be read: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n throw toMppError(\n `http_${response.status}`,\n `MPP response from ${path} was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mpp-api.js","sourceRoot":"","sources":["../../src/mpp/mpp-api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Machine Payments Protocol (MPP) API.\n *\n * MPP is a second payment framing over the unchanged Nevermined core: the same\n * plan, the same delegation and the same credit burn as x402, negotiated with\n * MPP headers. Sellers issue a challenge and redeem a credential; buyers mint\n * an MPP-domain access token and present it as a credential.\n */\n\nimport { BasePaymentsAPI } from '../api/base-payments.js'\nimport {\n API_URL_MPP_CHALLENGE,\n API_URL_MPP_CREATE_PERMISSION,\n API_URL_MPP_SETTLE,\n API_URL_MPP_VERIFY,\n} from '../api/nvm-api.js'\nimport { PaymentsError } from '../common/payments.error.js'\nimport type { MppTokenOptions, PaymentOptions } from '../common/types.js'\nimport type { SettlePermissionsResult, VerifyPermissionsResult } from '../x402/facilitator-api.js'\nimport { buildX402TokenRequestBody } from '../x402/token-request.js'\nimport { MppError, MppSettlementOutcomeUnknownError, toMppError } from './errors.js'\nimport { mppFetch, type MppFetchOptions, type MppFetchResult } from './fetch.js'\n\n/** Only whole, non-negative decimal digits — no sign, no leading/trailing\n * whitespace, no hex/octal/binary prefix, no fractional part. `BigInt(x)`\n * silently accepts all of those for a string input (`BigInt('0x10')` is 16,\n * `BigInt('')` is 0n, `BigInt(' 5 ')` is 5n), which a decimal-string\n * contract must reject rather than accept quietly. */\nconst DECIMAL_INTEGER_STRING = /^\\d+$/\n\n/**\n * Normalizes `credits` to the exact decimal-string amount that gets sealed\n * into the challenge and burned. A bare `.toString()` on a JS `number` can\n * emit scientific notation (`1e+21`), `\"NaN\"` or `\"Infinity\"` — all\n * forwarded unvalidated into an amount this PR's own docs describe as\n * having \"no post-hoc re-pricing as there is with x402\": a corrupted amount\n * here is not correctable downstream, it IS the amount.\n *\n * `BigInt(x)` renders an integer-valued number exactly (no scientific\n * notation) and throws `RangeError` on a non-integer, `NaN` or `Infinity`\n * instead of silently stringifying them — which is why non-string inputs go\n * through it. The `string` arm gets its own check instead of relying on\n * `BigInt`'s string parsing, which is far more permissive than a decimal\n * string contract should be.\n */\nexport function normalizeCredits(credits: string | number | bigint): string {\n if (typeof credits === 'string') {\n if (!DECIMAL_INTEGER_STRING.test(credits)) {\n throw new MppError(\n `credits must be a non-negative integer decimal string, got ${JSON.stringify(credits)}`,\n )\n }\n return credits\n }\n\n let normalized: string\n try {\n normalized = BigInt(credits).toString()\n } catch (error) {\n throw new MppError(\n `credits must be a non-negative integer, got ${credits}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n if (normalized.startsWith('-')) {\n throw new MppError(`credits must be a non-negative integer, got ${credits}`)\n }\n return normalized\n}\n\n/** Deadline on an outbound MPP request so a hung backend cannot hold the\n * caller's connection open indefinitely — `middleware.ts` defers the\n * buyer's own response until settlement resolves. */\nconst REQUEST_TIMEOUT_MS = 30_000\n\n/**\n * Socket-level failures that can only happen once the request was already on\n * the wire, so on a burning call the backend may have completed the burn and\n * only the answer was lost.\n *\n * `ECONNRESET` / `EPIPE` mean the peer tore down (or we wrote into) a\n * connection that had been established; `UND_ERR_SOCKET` is undici's wrapper\n * for the same thing, and `UND_ERR_ABORTED` covers a mid-flight abort that\n * did not come through the `TimeoutError` path.\n */\nconst OUTCOME_UNKNOWN_SOCKET_CODES = new Set([\n 'ECONNRESET',\n 'EPIPE',\n 'UND_ERR_SOCKET',\n 'UND_ERR_ABORTED',\n])\n\n/**\n * Whether a `fetch` rejection is a connection that died AFTER the request was\n * written — i.e. an unknown settlement outcome rather than a definite failure.\n *\n * Deliberately an ALLOW-LIST, not a blanket promotion of every network error.\n * `ECONNREFUSED`, `ENOTFOUND` and `EAI_AGAIN` all mean the request never\n * reached anything that could burn, so reporting them as \"may have burned\"\n * would inflate a seller's records with burns that never happened — the same\n * corruption as the 4xx case one screen down, in the other direction. Any code\n * not named here keeps the definite `network_error` classification.\n *\n * Node's `fetch` surfaces the underlying socket error as `error.cause`, so the\n * code is read from there first and off the error itself as a fallback.\n */\nfunction isPostRequestSocketFailure(error: unknown): boolean {\n if (!(error instanceof Error)) return false\n // `cause` is read off a widened shape rather than `Error.cause`: the\n // package's lib target predates ES2022, so the property is not on the\n // declared `Error` type even though Node populates it.\n const { cause, code: ownCode } = error as unknown as { cause?: unknown; code?: unknown }\n const causeCode = (cause as { code?: unknown } | undefined)?.code\n return (\n (typeof causeCode === 'string' && OUTCOME_UNKNOWN_SOCKET_CODES.has(causeCode)) ||\n (typeof ownCode === 'string' && OUTCOME_UNKNOWN_SOCKET_CODES.has(ownCode))\n )\n}\n\nexport interface IssueMppChallengeParams {\n /** The Nevermined plan the credits are burned against. */\n planId: string\n /** Credits the buyer is asked to redeem. Sent as a decimal string. */\n credits: string | number | bigint\n agentId?: string\n /** The protected resource. Sealed into the challenge and re-asserted at redeem. */\n resource: string\n /** HTTP verb of that resource. Part of the same binding. */\n httpVerb: string\n /** `sha-256=<base64>` digest binding the challenge to one request body. */\n digest?: string\n description?: string\n}\n\nexport interface RedeemMppParams {\n /** The `Authorization: Payment …` value presented by the buyer. */\n credential: string\n /** Must equal the resource the challenge was issued for. */\n resource: string\n /** Must equal the verb the challenge was issued for. */\n httpVerb: string\n /** Digest of the body actually received, when the challenge bound one. */\n bodyDigest?: string\n}\n\n/**\n * A discriminated union rather than a flat `paymentReceipt: string`: a\n * settlement can fail, and `SettlePermissionsResult.success` already says\n * so, but the flat shape let `{ success: false }` with no receipt typecheck\n * — a state the seller middleware then had no way to reject at compile time\n * even though it could not safely attach a receipt header for it.\n */\nexport type MppSettleResult =\n | (SettlePermissionsResult & { success: true; paymentReceipt: string })\n | (SettlePermissionsResult & { success: false; paymentReceipt?: string })\n\nexport class MppAPI extends BasePaymentsAPI {\n static getInstance(options: PaymentOptions): MppAPI {\n return new MppAPI(options)\n }\n\n /**\n * Mints the challenge a plan-protected endpoint returns with its 402.\n *\n * Each call returns a distinct challenge even for identical inputs — the id\n * doubles as the burn idempotency key, so two requests sharing one would\n * settle as a single burn.\n */\n async issueChallenge(\n params: IssueMppChallengeParams,\n ): Promise<{ challenge: string; id: string }> {\n const { planId, credits, agentId, resource, httpVerb, digest, description } = params\n return this.post<{ challenge: string; id: string }>(API_URL_MPP_CHALLENGE, {\n planId,\n credits: normalizeCredits(credits),\n resource,\n httpVerb,\n ...(agentId && { agentId }),\n ...(digest && { digest }),\n ...(description && { description }),\n })\n }\n\n /** Runs the full credential and plan checks without burning anything. */\n async verifyCredential(params: RedeemMppParams): Promise<VerifyPermissionsResult> {\n return this.post<VerifyPermissionsResult>(API_URL_MPP_VERIFY, this.redeemBody(params))\n }\n\n /**\n * Verifies and burns through the same chokepoint an x402 settlement uses, so\n * the credits charged are identical on both protocols. Settling the same\n * credential twice burns once.\n */\n async settleCredential(params: RedeemMppParams): Promise<MppSettleResult> {\n return this.post<MppSettleResult>(API_URL_MPP_SETTLE, this.redeemBody(params), {\n burns: true,\n })\n }\n\n /**\n * Mints an access token signed under the `Nevermined-MPP` EIP-712 domain.\n *\n * Same inputs and same settlement rail as {@link X402TokenAPI.getX402AccessToken}\n * minus `tokenVersion`; the token verifies only on the MPP routes, which is\n * what keeps the two protocols isolated.\n *\n * MPP carries **no token version** (nvm-monorepo#3266): its single-use unit is\n * the challenge, not the token, so one MPP token is presented across many\n * challenges by design. The parameter type says so, and a JavaScript caller\n * that sends one anyway is refused before the request — the backend answers\n * `BCK.MPP.0007` for any value, `2` included.\n */\n async getMppAccessToken(\n planId: string,\n agentId?: string,\n tokenOptions?: MppTokenOptions,\n ): Promise<{ accessToken: string }> {\n const body = buildX402TokenRequestBody({\n planId,\n agentId,\n tokenOptions,\n environmentName: this.environmentName,\n protocol: 'mpp',\n })\n return this.post<{ accessToken: string }>(API_URL_MPP_CREATE_PERMISSION, body)\n }\n\n /**\n * Performs an HTTP request, paying an MPP challenge if the endpoint returns\n * one.\n *\n * The buyer needs no new plan, delegation or credential: the delegation that\n * works for x402 works here unchanged.\n *\n * `init.body`, if set, must be replayable **if the endpoint may challenge\n * the request**: a `ReadableStream` throws a typed {@link PaymentsError}\n * once a 402 challenge actually requires a retry, since the stream cannot\n * be resent. A request that is never challenged sends a stream body exactly\n * once, exactly like plain `fetch` — the `paid: false` / untouched-response\n * guarantee still holds. A `string`, `Buffer`/`ArrayBuffer`/typed array,\n * `URLSearchParams`, `FormData` or `Blob` body all work unchanged either way.\n *\n * `options.delegationConfig` must carry a `delegationId` — this call\n * refuses the deprecated inline create-on-the-fly shape (no `delegationId`)\n * that {@link X402TokenAPI.getX402AccessToken} otherwise tolerates with a\n * warning: the retry loop here can mint an access token twice per call, so\n * that shape could silently create two delegations as a side effect of\n * paying.\n *\n * @example\n * ```typescript\n * const { response, receipt } = await payments.mpp.fetch(\n * 'https://agent.example/ask',\n * { method: 'POST', body: JSON.stringify({ q: 'hello' }) },\n * { delegationConfig: { delegationId }, planId },\n * )\n * ```\n */\n async fetch(\n input: string | URL,\n init: RequestInit | undefined,\n options: MppFetchOptions,\n ): Promise<MppFetchResult> {\n if (!options?.delegationConfig?.delegationId) {\n throw PaymentsError.validation(\n 'payments.mpp.fetch requires delegationConfig.delegationId. Create a delegation first ' +\n 'with payments.delegation.createDelegation(), then pass { delegationId }. An inline ' +\n 'create-on-the-fly delegationConfig (no delegationId) is not accepted here — the retry ' +\n 'loop can mint against it twice per call, which would create two delegations.',\n )\n }\n return mppFetch(\n (planId, agentId, tokenOptions) => this.getMppAccessToken(planId, agentId, tokenOptions),\n input,\n init,\n options,\n )\n }\n\n private redeemBody(params: RedeemMppParams): Record<string, unknown> {\n const { credential, resource, httpVerb, bodyDigest } = params\n return {\n credential,\n resource,\n httpVerb,\n ...(bodyDigest && { bodyDigest }),\n }\n }\n\n /**\n * One place for the POST + error translation both surfaces share.\n *\n * `burns` marks a call where \"no answer\" is not the same as \"did not\n * happen\" — currently only `settleCredential`. For that call alone, a\n * fetch rejection caused by OUR OWN `AbortSignal.timeout()` firing (not\n * some other network failure) is raised as {@link MppSettlementOutcomeUnknownError}\n * instead of the generic `network_error` `MppError` used everywhere else,\n * so a caller can tell \"definitely nothing happened\" apart from \"the\n * backend may have already burned the credits; we just didn't hear back\".\n * `error.name === 'TimeoutError'` combined with `instanceof DOMException`\n * is the documented, empirically-confirmed shape Node's `fetch` throws\n * when an `AbortSignal.timeout()` deadline fires — as opposed to, say,\n * connection-refused, which surfaces as a plain `TypeError`.\n *\n * The same reasoning covers the other two ways a burning call can end\n * without a definite answer: an upstream 5xx/408, and a 2xx whose body\n * could not be read. All three raise {@link MppSettlementOutcomeUnknownError};\n * only a 4xx — the backend deliberately rejecting — is a definite failure.\n */\n private async post<T>(\n path: string,\n body: Record<string, unknown>,\n options: { burns?: boolean } = {},\n ): Promise<T> {\n const url = new URL(path, this.environment.backend)\n const requestOptions = {\n ...this.getBackendHTTPOptions('POST', body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n }\n\n let response: Response\n try {\n response = await fetch(url, requestOptions)\n } catch (error) {\n if (options.burns && error instanceof DOMException && error.name === 'TimeoutError') {\n throw new MppSettlementOutcomeUnknownError()\n }\n if (options.burns && isPostRequestSocketFailure(error)) {\n throw new MppSettlementOutcomeUnknownError(\n `The connection to the backend failed after the settle request was written: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n throw toMppError(\n 'network_error',\n `Network error during MPP request: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n if (!response.ok) {\n let message = `MPP request to ${path} failed`\n let code: string | undefined = `http_${response.status}`\n try {\n const errorData = await response.json()\n if (errorData.message) message = errorData.message\n if (errorData.code) code = errorData.code\n if (errorData.hint) message = `${message} — ${errorData.hint}`\n } catch {\n // Keep the default message.\n }\n // On a burning call, a 5xx (or a 408) is an UNKNOWN outcome, not a\n // failure. A 504 in particular means an intermediary gave up waiting on\n // the backend — it says nothing about whether the burn committed, which\n // is the exact condition MppSettlementOutcomeUnknownError exists for,\n // and the argument the docstring below makes for an unreadable 2xx\n // applies word for word. Classified as a definite failure, the\n // middleware logs \"settlement failed\" and skips onAfterSettle for\n // credits that may well be gone.\n //\n // Confined to 5xx/408: the backend's own rejections (BCK.MPP.0003 and\n // friends) come back as 4xx and ARE definite — reporting those as\n // unknown would corrupt the accounting in the other direction.\n if (options.burns && (response.status >= 500 || response.status === 408)) {\n throw new MppSettlementOutcomeUnknownError(\n `The backend answered ${response.status} without a definite settlement outcome: ${message}`,\n )\n }\n throw toMppError(code, message)\n }\n\n // The success path is not exempt from a malformed body: a WAF\n // interstitial, a gateway HTML page, or a truncated 2xx response would\n // otherwise raise a raw SyntaxError here — the one call site in this\n // method that was NOT already wrapped and converted to a typed error.\n try {\n return (await response.json()) as T\n } catch (error) {\n // On a burning call this is an UNKNOWN outcome, not a failure. The\n // backend already answered 2xx — the burn committed — and only the\n // body was lost (our own AbortSignal.timeout() also aborts the body\n // stream, and a mid-body reset lands here too). Reporting it as a\n // definite failure is exactly the accounting corruption\n // MppSettlementOutcomeUnknownError exists to prevent: the middleware\n // would log \"settlement failed\" and skip onAfterSettle for credits\n // that were in fact burned. If anything this case is MORE likely to\n // have burned than the pre-response timeout the error was introduced\n // for.\n if (options.burns) {\n throw new MppSettlementOutcomeUnknownError(\n `The backend answered ${response.status} but its body could not be read: ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n throw toMppError(\n `http_${response.status}`,\n `MPP response from ${path} was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/x402/express/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE9D;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;AACzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAOL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/x402/express/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
import { buildPaymentRequired, resolveNetwork, resolveScheme, } from '../facilitator-api.js';
|
|
49
49
|
import { MPP_HEADERS, extractCredential, mppCredentialId, mppResource, mppVerb, resolveMppOption, } from './mpp-support.js';
|
|
50
50
|
import { computeBodyDigest, getRawBody } from './raw-body.js';
|
|
51
|
+
import { isAccessTokenAlreadyUsed, X402_TOKEN_ALREADY_USED_CODE } from '../token-version.js';
|
|
51
52
|
import { MppError, MppCredentialRejectedError, MppSettlementOutcomeUnknownError, isRetryableMppCode, } from '../../mpp/errors.js';
|
|
52
53
|
import { normalizeCredits } from '../../mpp/mpp-api.js';
|
|
53
54
|
/**
|
|
@@ -299,6 +300,55 @@ function isCredentialSpent(credentialId) {
|
|
|
299
300
|
}
|
|
300
301
|
return true;
|
|
301
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* How long a spent x402 access token stays refused.
|
|
305
|
+
*
|
|
306
|
+
* A v3 token has no expiry the seller can read, so this is a memory bound
|
|
307
|
+
* rather than a semantic one: past it the backend still refuses the token with
|
|
308
|
+
* `BCK.X402.0059`, and the only thing lost is this edge's ability to refuse it
|
|
309
|
+
* BEFORE running the handler. An hour is far longer than any plausible replay
|
|
310
|
+
* window and keeps the map small.
|
|
311
|
+
*/
|
|
312
|
+
const SPENT_X402_TOKEN_TTL_MS = 3600000;
|
|
313
|
+
/**
|
|
314
|
+
* Access tokens whose settlement was refused as already spent
|
|
315
|
+
* (`BCK.X402.0059`), and the instant each stops being worth remembering.
|
|
316
|
+
*
|
|
317
|
+
* Single-use (v3) tokens turn a previously harmless swallow into free service.
|
|
318
|
+
* `verify()` is a dry run the seller may repeat, so the backend deliberately
|
|
319
|
+
* never consults the nonce there — only `settle` spends it. A buyer replaying a
|
|
320
|
+
* spent token therefore verifies clean, the handler runs, and settlement throws
|
|
321
|
+
* afterwards. Under v2 that cost the seller nothing (settle always burned); a
|
|
322
|
+
* v3 replay would be served for free, repeatably, by anyone who has seen one
|
|
323
|
+
* used token.
|
|
324
|
+
*
|
|
325
|
+
* So single-use is the seller edge's job here, exactly as it is for MPP
|
|
326
|
+
* credentials — see {@link spentMppCredentials}, whose shape this mirrors.
|
|
327
|
+
*
|
|
328
|
+
* Same process-local caveat: this does not span processes or horizontally
|
|
329
|
+
* scaled instances, which need a shared store this package does not provide.
|
|
330
|
+
*/
|
|
331
|
+
const spentX402Tokens = new Map();
|
|
332
|
+
function markX402TokenSpent(token) {
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
for (const [spent, expiresAt] of spentX402Tokens) {
|
|
335
|
+
if (expiresAt > now)
|
|
336
|
+
break;
|
|
337
|
+
spentX402Tokens.delete(spent);
|
|
338
|
+
}
|
|
339
|
+
spentX402Tokens.set(token, now + SPENT_X402_TOKEN_TTL_MS);
|
|
340
|
+
}
|
|
341
|
+
/** Whether this access token has already been settled once at this edge. */
|
|
342
|
+
function isX402TokenSpent(token) {
|
|
343
|
+
const expiresAt = spentX402Tokens.get(token);
|
|
344
|
+
if (expiresAt === undefined)
|
|
345
|
+
return false;
|
|
346
|
+
if (expiresAt <= Date.now()) {
|
|
347
|
+
spentX402Tokens.delete(token);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
302
352
|
/**
|
|
303
353
|
* The MPP request path.
|
|
304
354
|
*
|
|
@@ -1001,6 +1051,20 @@ export function paymentMiddleware(payments, routes, options = {}) {
|
|
|
1001
1051
|
sendPaymentRequired(res, paymentRequired, `Missing x402 payment token. Send token in ${X402_HEADERS.PAYMENT_SIGNATURE} header.`);
|
|
1002
1052
|
return;
|
|
1003
1053
|
}
|
|
1054
|
+
// A token this edge already settled once is refused here, before the
|
|
1055
|
+
// handler runs. `verify` cannot catch it — the backend never consults the
|
|
1056
|
+
// nonce there — so without this the buyer is served and settlement fails
|
|
1057
|
+
// afterwards, which for a single-use token means free service.
|
|
1058
|
+
if (isX402TokenSpent(token)) {
|
|
1059
|
+
const error = new Error('Payment required: this x402 access token was already used');
|
|
1060
|
+
if (onPaymentError) {
|
|
1061
|
+
onPaymentError(error, req, res);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
sendPaymentRequired(res, paymentRequired, 'This x402 access token was already used. Single-use (v3) tokens are consumed by their ' +
|
|
1065
|
+
'first settlement — mint a new token for this request.');
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1004
1068
|
// Calculate credits to verify
|
|
1005
1069
|
const creditsToVerify = typeof credits === 'function' ? await credits(req, res) : credits;
|
|
1006
1070
|
try {
|
|
@@ -1083,9 +1147,21 @@ export function paymentMiddleware(payments, routes, options = {}) {
|
|
|
1083
1147
|
});
|
|
1084
1148
|
})
|
|
1085
1149
|
.catch((settleError) => {
|
|
1150
|
+
// A settle refused as already-spent is not a transient failure:
|
|
1151
|
+
// the token bought a response once and must not buy another.
|
|
1152
|
+
// Remember it so the NEXT request is refused before the handler
|
|
1153
|
+
// runs, and tell the buffered path below to withhold this body.
|
|
1154
|
+
if (isAccessTokenAlreadyUsed(settleError)) {
|
|
1155
|
+
markX402TokenSpent(token);
|
|
1156
|
+
spentTokenRefusal = settleError;
|
|
1157
|
+
}
|
|
1086
1158
|
console.error('Payment settlement failed:', settleError);
|
|
1087
1159
|
});
|
|
1088
1160
|
};
|
|
1161
|
+
// Set when settlement was refused because the token had already been
|
|
1162
|
+
// spent, so the `end` wrapper can withhold a body that was never paid
|
|
1163
|
+
// for. Only reachable on a single-use (v3) token.
|
|
1164
|
+
let spentTokenRefusal = null;
|
|
1089
1165
|
res.end = function (...args) {
|
|
1090
1166
|
// Only bill on 2xx success. Skipping 3xx avoids charging when the
|
|
1091
1167
|
// handler redirects (e.g. `res.redirect(...)`), 304 Not Modified,
|
|
@@ -1111,8 +1187,29 @@ export function paymentMiddleware(payments, routes, options = {}) {
|
|
|
1111
1187
|
return originalEnd(...args);
|
|
1112
1188
|
}
|
|
1113
1189
|
// Buffered response path: defer the real `end` until settlement
|
|
1114
|
-
// finishes so the receipt header makes it into the same response
|
|
1190
|
+
// finishes so the receipt header makes it into the same response —
|
|
1191
|
+
// and so a settlement refused as already-spent can still withhold the
|
|
1192
|
+
// body the handler produced.
|
|
1115
1193
|
runSettlement().finally(() => {
|
|
1194
|
+
if (spentTokenRefusal && !res.headersSent) {
|
|
1195
|
+
// The handler already ran, so the work is done and wasted; what
|
|
1196
|
+
// must not happen is delivering it. Replace the body with the
|
|
1197
|
+
// 402 the buyer would have got had `verify` been able to see the
|
|
1198
|
+
// spent nonce.
|
|
1199
|
+
const payload = JSON.stringify({
|
|
1200
|
+
error: 'This x402 access token was already used. Single-use (v3) tokens are consumed ' +
|
|
1201
|
+
'by their first settlement — mint a new token for this request.',
|
|
1202
|
+
code: X402_TOKEN_ALREADY_USED_CODE,
|
|
1203
|
+
});
|
|
1204
|
+
res.statusCode = 402;
|
|
1205
|
+
res.removeHeader('ETag');
|
|
1206
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1207
|
+
res.setHeader('Content-Length', Buffer.byteLength(payload));
|
|
1208
|
+
// `originalEnd` is Express's overloaded `end`; the 3-arg form
|
|
1209
|
+
// (chunk, encoding, callback) is the one its type accepts here.
|
|
1210
|
+
originalEnd(payload, 'utf8');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1116
1213
|
originalEnd(...args);
|
|
1117
1214
|
});
|
|
1118
1215
|
return res;
|