diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts index 01965382a1..75dfe30319 100644 --- a/typescript/packages/core/src/server/x402ResourceServer.ts +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -688,25 +688,24 @@ export class x402ResourceServer { } // Find the matching supported kind from facilitator - const supportedKind = this.getSupportedKind( - x402Version, - resourceConfig.network, - SchemeNetworkServer.scheme, - ); - - if (!supportedKind) { + // If facilitator not yet initialized, skip validation so 402 can be emitted on cold start. + // Facilitator validation is only required at verify/settle time. + const isInitialized = this.supportedResponsesMap.size > 0; + const supportedKind = isInitialized + ? this.getSupportedKind(x402Version, resourceConfig.network, SchemeNetworkServer.scheme) + : undefined; + + if (isInitialized && !supportedKind) { throw new Error( `Facilitator does not support ${SchemeNetworkServer.scheme} on ${resourceConfig.network}. ` + `Make sure to call initialize() to fetch supported kinds from facilitators.`, ); } - // Get facilitator extensions for this combination - const facilitatorExtensions = this.getFacilitatorExtensions( - x402Version, - resourceConfig.network, - SchemeNetworkServer.scheme, - ); + // Get facilitator extensions for this combination (empty if not yet initialized) + const facilitatorExtensions = isInitialized + ? this.getFacilitatorExtensions(x402Version, resourceConfig.network, SchemeNetworkServer.scheme) + : undefined; // Parse the price using the scheme's price parser const parsedPrice = await SchemeNetworkServer.parsePrice( @@ -729,11 +728,14 @@ export class x402ResourceServer { }; // Delegate to the implementation for scheme-specific enhancements - const requirement = await SchemeNetworkServer.enhancePaymentRequirements( - baseRequirements, - supportedKind, - facilitatorExtensions, - ); + // If facilitator not yet initialized, skip enhancement and use base requirements directly + const requirement = isInitialized + ? await SchemeNetworkServer.enhancePaymentRequirements( + baseRequirements, + supportedKind!, + facilitatorExtensions, + ) + : baseRequirements; requirements.push(requirement); return requirements; diff --git a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts index 24a1e9c60e..e8d10a72a1 100644 --- a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts +++ b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts @@ -316,6 +316,39 @@ describe("x402ResourceServer", () => { }); describe("buildPaymentRequirements", () => { + it("should build base requirements without facilitator init (cold start)", async () => { + // Test that buildPaymentRequirements works without calling initialize() + // This enables 402 responses on cold starts (serverless/edge environments) + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + }), + ); + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme", { + amount: "1000000", + asset: "USDC", + extra: {}, + }); + server.register("test:network" as Network, mockScheme); + // Do NOT call server.initialize() - simulating cold start + + const requirements = await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient_address", + price: "$1.00", + network: "test:network" as Network, + }); + + // Should return base requirements without facilitator-specific enhancements + expect(requirements).toHaveLength(1); + expect(requirements[0].scheme).toBe("test-scheme"); + expect(requirements[0].payTo).toBe("recipient_address"); + expect(requirements[0].amount).toBe("1000000"); + // enhancePaymentRequirements should NOT have been called (no facilitator init) + expect(mockScheme.enhanceCalls).toHaveLength(0); + }); + it("should build requirements from ResourceConfig", async () => { const mockClient = new MockFacilitatorClient( buildSupportedResponse({ diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts index 2ef5ac0612..af3dfee4f6 100644 --- a/typescript/packages/http/hono/src/index.test.ts +++ b/typescript/packages/http/hono/src/index.test.ts @@ -40,24 +40,6 @@ let mockProcessSettlement: ReturnType; let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; -type PaymentVerifiedResult = Extract; -type MockHTTPProcessResult = - | Exclude - | (Omit & { - cancellationDispatcher?: PaymentVerifiedResult["cancellationDispatcher"]; - }); - -/** - * Creates a mock payment cancellation dispatcher. - * - * @returns Mock payment cancellation dispatcher. - */ -function createMockPaymentCancellationDispatcher(): PaymentVerifiedResult["cancellationDispatcher"] { - return { - cancel: vi.fn().mockResolvedValue(undefined), - } as unknown as PaymentVerifiedResult["cancellationDispatcher"]; -} - vi.mock("@x402/core/server", () => ({ SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", FacilitatorResponseError: class FacilitatorResponseError extends Error { @@ -99,7 +81,6 @@ vi.mock("@x402/core/server", () => ({ registerExtension: vi.fn(), }, })), - checkIfBazaarNeeded: vi.fn().mockReturnValue(false), })); // --- Mock Factories --- @@ -110,7 +91,7 @@ vi.mock("@x402/core/server", () => ({ * @param settlementResult - Result to return from processSettlement. */ function setupMockHttpServer( - processResult: MockHTTPProcessResult, + processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } | { @@ -123,15 +104,7 @@ function setupMockHttpServer( headers: {}, }, ): void { - const normalizedResult = - processResult.type === "payment-verified" - ? { - ...processResult, - cancellationDispatcher: - processResult.cancellationDispatcher ?? createMockPaymentCancellationDispatcher(), - } - : processResult; - mockProcessHTTPRequest.mockResolvedValue(normalizedResult); + mockProcessHTTPRequest.mockResolvedValue(processResult); mockProcessSettlement.mockResolvedValue(settlementResult); } @@ -475,7 +448,7 @@ describe("paymentMiddleware", () => { ); mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); - const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer, undefined, undefined, true); const next = vi.fn().mockResolvedValue(undefined); await middleware(createMockContext(), next); diff --git a/typescript/packages/http/hono/src/index.ts b/typescript/packages/http/hono/src/index.ts index 9f097d0dad..7575b85bc3 100644 --- a/typescript/packages/http/hono/src/index.ts +++ b/typescript/packages/http/hono/src/index.ts @@ -10,7 +10,6 @@ import { getFacilitatorResponseError, SETTLEMENT_OVERRIDES_HEADER, SettlementOverrides, - checkIfBazaarNeeded, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { Context, MiddlewareHandler } from "hono"; @@ -27,6 +26,24 @@ export function setSettlementOverrides(c: Context, overrides: SettlementOverride c.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); } +/** + * Check if any routes in the configuration declare bazaar extensions + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + // Handle single route config + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + // Handle multiple routes + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + /** * Configuration for registering a payment scheme with a specific network */ @@ -61,7 +78,7 @@ function facilitatorErrorResponse(c: Context, error: FacilitatorResponseError): * @param httpServer - Pre-configured x402HTTPResourceServer instance * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -81,7 +98,7 @@ export function paymentMiddlewareFromHTTPServer( httpServer: x402HTTPResourceServer, paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { // Register custom paywall provider if provided if (paywall) { @@ -192,29 +209,16 @@ export function paymentMiddlewareFromHTTPServer( case "payment-verified": // Payment is valid, need to wrap response for settlement - const { cancellationDispatcher, paymentPayload, paymentRequirements, declaredExtensions } = - result; + const { paymentPayload, paymentRequirements, declaredExtensions } = result; // Proceed to the next middleware or route handler - try { - await next(); - } catch (error) { - await cancellationDispatcher.cancel({ - reason: "handler_threw", - error, - }); - throw error; - } + await next(); // Get the current response let res = c.res; // If the response from the protected route is >= 400, do not settle payment if (res.status >= 400) { - await cancellationDispatcher.cancel({ - reason: "handler_failed", - responseStatus: res.status, - }); return; } @@ -282,7 +286,7 @@ export function paymentMiddlewareFromHTTPServer( * @param server - Pre-configured x402ResourceServer instance * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -300,7 +304,7 @@ export function paymentMiddleware( server: x402ResourceServer, paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { // Create the x402 HTTP server instance with the resource server const httpServer = new x402HTTPResourceServer(server, routes); @@ -324,7 +328,7 @@ export function paymentMiddleware( * @param schemes - Optional array of scheme registrations for server-side payment processing * @param paywallConfig - Optional configuration for the built-in paywall UI * @param paywall - Optional custom paywall provider (overrides default) - * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to false). Set to true only if your deployment environment guarantees warm instances (e.g., long-running servers). For serverless/edge environments (Cloudflare Workers, AWS Lambda), keep false to avoid cold-start timeouts. * @returns Hono middleware handler * * @example @@ -345,7 +349,7 @@ export function paymentMiddlewareFromConfig( schemes?: SchemeRegistration[], paywallConfig?: PaywallConfig, paywall?: PaywallProvider, - syncFacilitatorOnStart: boolean = true, + syncFacilitatorOnStart: boolean = false, ): MiddlewareHandler { const ResourceServer = new x402ResourceServer(facilitatorClients);