/** * @pwngh/economy-lab * * Copyright (c) Preston Neal * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import type { ErrorCode } from './errors.js'; import type { RateLimiter, ReplayStore, Ports } from './ports.js'; import type { Economy, Outcome, Principal } from './contract.js'; import type { InstanceEconomies } from './instance.js'; /** * Host handler for inbound provider callbacks (webhooks), e.g. a payment processor reporting a * settlement or dispute. Takes the URL's provider name and the request, returns the HTTP response. * * The server verifies signature and freshness before the handler runs, so the bytes are trusted. * `request` is a fresh `Request` over the same raw body, re-readable without double-consuming. */ export type WebhookHandler = (provider: string, request: Request) => Promise; /** * Authentication for `/submit`: maps the request to the acting principal, or null to refuse * with 401. The server stamps the result onto the operation and rejects a body that carries its * own `actor`. */ export type Authenticate = (request: Request) => Promise; /** What the HTTP layer needs from the bag; a full Ports is assignable. */ export type ServerPorts = Pick; /** What {@link createServer} returns: a Fetch-native handler for any WinterCG runtime. */ export type FetchHandler = (request: Request) => Promise; /** * Admission control for `/submit`: each request counts against a caller key and a denial * answers 429 with retry-after. The default key is the authenticated principal, else the * client address the Node bridge stamps; hosts on other runtimes supply `keyFor`. A throwing * limiter fails open — degraded protection, not degraded availability — and counts * `economy.ratelimit.degraded`. */ export type RateLimitConfig = { limiter: RateLimiter; keyFor?: (request: Request, principal?: Principal) => string; }; /** * Everything {@link createServer} accepts. `economy`, `ports`, and the `authenticate` posture are * required; the rest are opt-in edge behavior — absent, CORS stays off, admission control is * off, and the webhook and instance routes answer 404. */ export interface ServerOptions { economy: Economy; /** * The narrow pick the routes read: config for webhook policy, secrets for the webhook HMAC, * clock for freshness, meter for the duplicate and degraded counters. */ ports: ServerPorts; /** * Required posture, not a default: a function authenticates `/submit`, and an explicit `false` * declares the body's actor is trusted — safe only for in-process hosts, never for a * network-exposed handler. Omitting it entirely refuses to construct. */ authenticate?: Authenticate | false; /** * Handler for verified provider callbacks on `POST /webhooks/:provider`; absent, the route * answers 404. */ webhook?: WebhookHandler; /** * `false` declares admission control off; absent means off too (infra silence is allowed * here, unlike authentication). */ rateLimit?: RateLimitConfig | false; /** * Dedup store for provider `eventId`s: a repeat delivery returns 200 without invoking the * handler. When absent, the host dedups. The claim-last ordering lives at webhookRoute. */ replay?: ReplayStore; /** * Browser origins allowed by CORS, matched exactly. Absent means no CORS headers at all, so * cross-origin browser calls fail closed. */ cors?: { origins: ReadonlyArray; }; /** * The instance-economy lane manager (see src/instance.ts). When present, the server exposes * `POST /instances/:scope/purchase` — the transport an unprivileged game server calls to * request in-world purchases, with the lane living in this tier. Absent, the routes 404. */ instances?: InstanceEconomies; /** * Byte ceiling on request bodies; past it the reply is 413. Defaults to * {@link DEFAULT_MAX_BODY_BYTES}, which every legitimate operation fits well under. */ maxBodyBytes?: number; /** * Deadline on reading a request body; past it the reply is 408, so a trickled body cannot * hold the handler open. Defaults to {@link DEFAULT_READ_TIMEOUT_MS}. */ readTimeoutMs?: number; } /** Default byte ceiling on request bodies. The Node host bridge enforces the same limit. */ export declare const DEFAULT_MAX_BODY_BYTES: number; /** Default deadline on reading a request body. The Node host bridge enforces the same limit. */ export declare const DEFAULT_READ_TIMEOUT_MS = 10000; /** * Where the trusted client address rides. The Node bridge stamps this from the socket, * overwriting anything inbound, so a caller can't spoof it. */ export declare const CLIENT_IP_HEADER = "x-economy-client-ip"; /** Where the correlation id is accepted and echoed on `/submit`. */ export declare const REQUEST_ID_HEADER = "x-request-id"; /** * HTTP entry point for an {@link Economy}: takes a Fetch `Request`, returns a `Response`. Uses only * Fetch globals (no Node APIs), so it runs on Node, Bun, Deno, and Cloudflare Workers. * * Routes these paths, 404s the rest: * - `POST /submit` reads one operation from the JSON body, runs it, returns the result. * - `POST /instances/:scope/purchase` (only with `instances` configured) hands an in-world * purchase to the scope's fast-lane session — the transport an unprivileged game server calls. * - `POST /webhooks/:provider` verifies the callback, then hands it to the injected handler. * - `GET /healthz` reports liveness without touching storage. * - `GET /readyz` reports readiness via one cheap store-touching read through the economy. * * A thrown {@link EconomyError} becomes an RFC 9457 problem+json response: {@link statusForError} maps * the status, `title` carries the caller-safe message, and the stable `code` and `retryable` ride * as extensions. `detail`, `cause`, and stack never leave the server. * * `/submit` authenticates through `authenticate` when configured; every body reads under a byte * ceiling and deadline; CORS stays off unless `cors` lists origins. * * Single-submit on purpose: `Economy.submitBatch` and the submit coalescer are in-process * surface for the host composing the economy, not a route — a shared batch transaction cannot * carry each request's own correlation id. * * @example * const handler = createServer({ * economy, * ports, * authenticate: async (request) => { * const userId = await verifyToken(request.headers.get('authorization')); * return userId === null ? null : { kind: 'user', userId }; // null answers 401 * }, * }); * * @see {@link https://economy-lab-docs.pages.dev/economy/reference/http-service/ HTTP service} for * the routes, codec, and webhook gate. */ export declare function createServer(options: ServerOptions): FetchHandler; /** * The wire shape of an Outcome. A rejected outcome carries both the typed `detail` (branded * Amounts and bigints become decimal strings) and a top-level `reason` derived from * `detail.reason` — HTTP clients live outside the type system, so the wire keeps the harmless * duplicate for stability even though `detail.reason` is the sole discriminant in TypeScript. */ export declare function encodeOutcome(outcome: Outcome): unknown; /** * Builds an RFC 9457 problem+json response. `title` is the caller-safe message; when a fault is * given, its stable `code` and `retryable` flag ride as extensions. `detail`, `cause`, and stack * stay server-side and never reach the wire. See https://www.rfc-editor.org/rfc/rfc9457 for the * format. */ export declare function problemResponse(status: number, title: string, fault?: { code: ErrorCode; retryable: boolean; }): Response; export { decodeWebhookEvent, handlePurchaseWebhook, handleWebhook, toOperation, } from './webhooks.js'; export type { DisputeEvent, PayoutFailedEvent, PayoutSettledEvent, PurchaseEvent, WebhookEvent, WebhookReceipt, } from './webhooks.js';