import { Checkout, SwapData, NodeSettlementActionHook, CreateCheckoutAmount, OpenReceive } from '@openreceive/node'; import { ErrorBody, ErrorCode, TransactionSettlementStatus, PaymentDetails, PaymentScanWindow, PaymentsDialect } from '@openreceive/core'; type AuthorizeAction = "checkout.prepare" | "checkout.create" | "payment.check" | "swap.quote" | "swap.create" | "swap.read" | "swap.refund"; /** * Copied from the payer's JSON body before any host lookup. `reference` is on * every order-scoped route; `paymentHash` is also set on `payment.check`, * `swap.read`, and `swap.refund`. Either value identifies a row — it does not * prove this caller owns it. * * Both are optional because the type spans every action, not because a * policy has to nil-check them: the handler has already rejected a missing, * empty, or over-long `reference` (400, MAX_REFERENCE_LENGTH) by the time * `authorize` runs, so `reference` is always a non-empty trimmed string at * runtime, and `paymentHash` is undefined on the other four actions. */ interface AuthorizeResource { reference?: string; paymentHash?: string; } interface AuthorizeContext { readonly action: AuthorizeAction; readonly request: Request; readonly resource: AuthorizeResource; /** * The untouched framework-native request (Express req, Fastify request), when * an adapter provides one. Use it for middleware-attached state like * req.session. Same `authorize` callback as `request`; this is not a second * signature. */ readonly native?: unknown; } /** * One callback, one argument. Return `true` to allow, `false` for 403. * May be sync or async. Destructure whichever fields of `context` you need — * they are always present except `native`, which adapters fill in. */ type Authorize = (context: AuthorizeContext) => boolean | Promise; type RateLimit = Authorize; /** * A control-flow error the handler maps to a JSON error response. Carries the HTTP status, a * shared error code, and an optional `retryable` hint / `details` object. */ declare class HttpError extends Error { readonly status: number; readonly code: ErrorCode; readonly retryable?: boolean; readonly details?: Record; /** Emitted as a `Retry-After` header so clients can back off with a hint. */ readonly retryAfterSeconds?: number; constructor(status: number, code: ErrorCode, message: string, options?: { readonly retryable?: boolean; readonly details?: Record; readonly retryAfterSeconds?: number; }); } /** Shape a service error carries: a numeric status and an OpenReceive error body. */ interface ServiceErrorShape { readonly status: number; readonly body: ErrorBody; } /** * Duck-type an ServiceError (from @openreceive/node) without importing the class, so the * handler stays runtime-agnostic and never breaks on cross-module `instanceof` identity mismatches * (source vs. built dist, or two copies of @openreceive/node). */ declare function isServiceErrorShape(error: unknown): error is ServiceErrorShape; /** * Host-route control-flow error with the same `{ status, body }` shape as * {@link ServiceError}. Use for cart/validation failures on app routes * (for example the host's `/orders` route) so framework helpers can map them. */ declare class HostError extends Error { readonly status: number; readonly body: ErrorBody; constructor(status: number, body: ErrorBody); } /** * Convenience factory for a host validation error (default 400 INVALID_REQUEST). * * `retryable` rides along only when it CONTRADICTS the code's own default — * spelling out `retryable: false` on a CONFLICT restates what the shared error * contract already says, and it was the one field that made the JS engine's * conflict bodies differ from the Ruby engine's. */ declare function hostError(message: string, status?: number, code?: ErrorCode): HostError; /** * Map a thrown host/service error to `{ status, body }` for app routes outside the * mounted OpenReceive handler. Returns `null` when the value is not a known shape * (caller should rethrow / pass to `next(error)`). */ declare function mapHostRouteError(error: unknown): { readonly status: number; readonly body: ErrorBody; } | null; /** Generate a per-response request id used in both the body and the `X-Request-Id` header. */ declare function createRequestId(): string; /** * Serialize a value as a snake_case JSON response with the shared content-type and request-id header. * `extraHeaders` are appended (not set) — used for `Retry-After` on 429s. Note for adapters: * multi-value headers other than the ones used here would need per-framework handling * (Express folds repeated `Set-Cookie` appends); the handler deliberately emits none. */ declare function jsonResponse(status: number, body: unknown, requestId: string, extraHeaders?: Iterable): Response; /** * Map any thrown value to a JSON error response with a code from the shared enum. Service errors keep * their status/body (with `request_id` ensured); handler errors map to their status; anything else is * a generic 500 INTERNAL so internal messages never leak. */ declare function errorResponse(error: unknown, requestId: string): Response; /** * Lifecycle of one payment attempt: {@link TransactionSettlementStatus} plus * `attention`. `pending` attempts participate in reconciliation; every other * status is terminal for reconciliation purposes. `attention` marks rows an * operator must review before the attempt moves on: the wallet still * explicitly reports an in-flight transaction state long after invoice expiry. */ type AttemptStatus = TransactionSettlementStatus | "attention"; /** * The minimal row OpenReceive needs for one invoice or swap attempt. * An order may have many of these records. `swapData` must remain server-only. */ interface PaymentRecord { readonly reference: string; readonly paymentHash: string; readonly status: AttemptStatus; /** Optional operator-facing detail for the current status (e.g. "superseded"). */ readonly statusReason?: string | null; readonly paidAt: number | null; /** Unix timestamp after which these payer instructions must not be reused. */ readonly expiresAt: number; /** Unix timestamp used only to choose deterministically between historical attempts. */ readonly createdAt: number; /** Safe, replayable payer response. Contains no wallet or provider credentials. */ readonly checkout: Checkout; readonly swapData?: SwapData | null; } interface PaymentInsert { readonly reference: string; readonly paymentHash: string; readonly expiresAt: number; readonly createdAt: number; readonly checkout: Checkout; readonly swapData?: SwapData; /** Client IP captured at invoice creation, when the adapter could attribute one. */ readonly clientIp?: string; } /** One pending attempt the reconciler should include in its next wallet scan. */ interface ReconcilableAttempt { readonly paymentHash: string; /** Exact NIP-47 invoice creation time returned by make_invoice. */ readonly createdAt: number; readonly createdAtSource?: "wallet" | "host"; /** Actual wallet invoice deadline from the saved checkout, never the deposit deadline. * Closure also requires a covering scan after this deadline plus the expiry grace. */ readonly expiresAt: number; } /** A terminal (non-settled) state transition observed by reconciliation. */ interface ReconciliationTransition { readonly paymentHash: string; readonly status: Exclude; /** Unix timestamp of the wallet scan that justified this transition. */ readonly observedAt: number; /** Operator-facing reason, e.g. "wallet_reported_expired" or "not_found_after_expiry". */ readonly reason: string; } /** One observed wallet settlement, as handed to the repository to record. */ interface SettlementRecord { readonly paymentHash: string; /** Unix timestamp the wallet reports (or the scan observed) the payment at. */ readonly paidAt: number; readonly details?: PaymentDetails; } /** * Persistence boundary for payment attempts. Most applications use the * library-provided SQL repository (`createSqlPayments`); implementing * this interface directly is the advanced escape hatch. * * `commitAttempt` must serialize concurrent creates for one order, reject a * settled order or a reusable live attempt on the same rail/asset, supersede a * near-expiry same-rail attempt, and commit before it returns. Other rails may * remain live so the payer can switch methods. The HTTP handler withholds payer * instructions when this method throws. * * `recordReconciliation` must apply the transition only while the row is still * `pending` — it must never overwrite a settled attempt. * * `recordSettlementWithFulfillment` locks the reference, records settlement, * and awaits host fulfillment in one transaction. Failure rolls back both writes. */ interface SettlementContext extends SettlementRecord { readonly reference: string; /** Handle owned by the settlement transaction; never a root connection. */ readonly transaction: Transaction; } interface ReconcileCursor { readonly created_at: number; readonly payment_hash: string; } interface ReconcileScheduler { cursor: ReconcileCursor | null; windows: PaymentScanWindow[]; } interface ReconcileGateClaim { readonly token: string; readonly scheduler: ReconcileScheduler; } interface PaymentRepository { listForReference(reference: string): Promise; /** Durable acknowledgment for settlement outcomes, including concurrent writes. */ findByPaymentHash(paymentHash: string): Promise; /** * A bounded keyset page of `pending` attempts ordered by createdAt and hash, * strictly after the supplied cursor; terminal rows are excluded. Return at * OPENRECEIVE_RECONCILE_BATCH_SIZE (200) when that many remain, otherwise * all remaining rows. A short page signals exhaustion; the durable scheduler * wraps immediately so new arrivals cannot postpone old fulfillment retries. */ listReconcilableAttempts(after?: ReconcileCursor | null): Promise; /** * The `pending` attempt for one payment hash, or undefined when the hash is * unknown or already terminal. Direct settlement from an NWC notification * needs a by-hash answer: `listReconcilableAttempts` is an oldest-first * batch, so a notified attempt behind a backlog would look unknown. A * repository that omits this falls back to batch membership, which delays * notified settlements past a batch-sized backlog. */ findPendingAttempt?(paymentHash: string): Promise; commitAttempt(input: CheckoutCreatedInput): void | Promise; recordReconciliation(transition: ReconciliationTransition): void | Promise; /** * Record this pending attempt's settlement under the reference lock, await * fulfillment for the first settled sibling, then commit both writes. * Returns true only for the call that won the claim — the attempt was still * unsettled AND no sibling attempt on the order had settled. Later calls for * the same or a sibling attempt must record the settlement (a genuine second * payment is not discarded) and return false. A settled attempt is never * overwritten, and an unknown payment hash is a no-op returning false. */ recordSettlementWithFulfillment(settlement: SettlementRecord, fulfill: (context: SettlementContext) => void | Promise): boolean | Promise; /** * Count attempt rows recorded for this client IP at or after `sinceUnixSeconds`. * Backs the handler's opt-in `rateLimiting` option; when a custom repository * omits it, enabling `rateLimiting` fails at construction (there is no * in-memory fallback — use a custom `rateLimitHook` instead). */ countAttemptsFromIp?(clientIp: string, sinceUnixSeconds: number): number | Promise; /** * Claim the durable global reconcile gate: return a token and scheduler when * this caller owns the lease, or null while another lease/interval is active * (`gate_busy`). Persist progress only through the matching unexpired token. The claim MUST be a durable compare-and-set * shared by every process on the host database (the built-in SQL repository * uses the `openreceive_meta` key/value/rev table) — process-local memory * cannot coordinate multiple workers and must never back this. Backs the * handler's default opportunistic reconcile; when a custom repository omits * it, construction throws unless `opportunisticReconcile: false` — the * default settlement path never degrades silently. */ claimReconcileGate?(input: { readonly now: number; readonly intervalSeconds: number; readonly leaseSeconds?: number; }): ReconcileGateClaim | null | Promise; checkpointReconcileGate?(input: { readonly claim: ReconcileGateClaim; readonly scheduler: ReconcileScheduler; readonly now: number; readonly release?: boolean; readonly intervalSeconds?: number; }): boolean | Promise; } /** * Runs one SQL statement and returns SELECT rows (`[]` otherwise). The SQL * arrives written for the adapter's own dialect — `?` placeholders on sqlite * and mysql, `$1`-style on postgres — so pass it to the driver verbatim. * Nothing rewrites it: a `?` inside a string literal, a comment, or a postgres * JSON operator (`data ? 'field'`) must survive untouched. */ type SqlQuery = (sql: string, params?: readonly unknown[]) => Promise[]>; interface SqlClient { readonly query: SqlQuery; } /** * The escape-hatch database boundary: a dialect, a query function, and a * transaction wrapper. The built-in bindings cover `pg` pools/clients, mysql2 * pools/connections and SQLite (`node:sqlite` or better-sqlite3) without * adding dependencies. `mysql` covers MariaDB too. */ interface SqlAdapter extends SqlClient { readonly dialect: "postgres" | "sqlite" | "mysql"; transaction(run: (tx: SqlClient) => Promise): Promise; } /** * Structural view of a `pg` Pool or Client. A Pool's `connect()` checks out a * per-transaction client; a Client's `connect()` opens its one socket and * resolves nothing, so it must be called at most once. A handle with only * `query` is treated as a single connection and transactions serialize on it. */ interface PgLike { query(sql: string, params?: unknown[]): Promise<{ rows: Record[]; }>; connect?(): Promise; } /** Structural view of `node:sqlite` DatabaseSync or a better-sqlite3 Database. */ interface SqliteLike { prepare(sql: string): { all(...params: unknown[]): unknown[]; run(...params: unknown[]): unknown; }; exec(sql: string): void; } /** * Structural view of a mysql2/promise Pool or Connection. A Pool checks out a * connection per transaction with `getConnection()`; a Connection is one * session whose transactions serialize in-process, like a pg Client. The * callback API's Pool and Connection reach this shape through `promise()`. */ interface MysqlLike extends MysqlSessionLike { execute(sql: string, values?: unknown[]): Promise; getConnection?(): Promise; } interface MysqlSessionLike { query(sql: string, values?: unknown[]): Promise; beginTransaction?(): Promise; commit?(): Promise; rollback?(): Promise; release?(): void; } /** A mysql2 callback-API Pool or Connection. */ interface MysqlCallbackLike { promise(): MysqlLike; } /** Any database handle `createSqlPayments` accepts. */ type SqlDatabase = SqlAdapter | PgLike | MysqlLike | MysqlCallbackLike | SqliteLike; interface SqlPaymentsOptions { /** Payment attempts table name. Default `openreceive_payments`. */ readonly tableName?: string; /** Durable reconcile-gate key/value table name. Default `openreceive_meta`. */ readonly metaTableName?: string; readonly clock?: () => number; } /** Settlement context passed to the host's `onPaid` in library-persistence mode. */ interface PaymentSettlement { readonly reference: string; readonly paymentHash: string; readonly paidAt: number; readonly details?: PaymentDetails; /** * Runs statements inside the settlement transaction. Use it to update the * host order or insert a transactional outbox row. Write the placeholders * your database uses (`?` on sqlite and mysql, `$1`-style on postgres) — * this SQL is yours and reaches the driver exactly as written. * * OpenReceive already guarantees this hook fires at most once per reference * across its own settlement paths. It cannot see fulfillment triggered * anywhere else, though — * an admin action, a second processor, a replayed job — so if any of those * exist, guard the transition here rather than assuming exclusivity: * * ```sql * UPDATE orders SET state = 'paid' * WHERE id = $1 AND state = 'awaiting_payment' RETURNING id * ``` * * An empty result means someone else already fulfilled it; return without * shipping. `fulfillmentNote` in `@openreceive/core` is the full * version, and is what the scaffold writes into every generated file. */ readonly query: SqlQuery; } type PaymentSettlementHook = (settlement: PaymentSettlement) => void | Promise; /** Nonsecret dry-run report for explicit host/operator review. */ interface PaymentRepairCandidate { readonly paymentHash: string; readonly reference: string; readonly status: "expired" | "attention"; readonly statusReason: string | null; readonly updatedAt: number; readonly instructionExpiresAt: number; readonly settlementExpiresAt: number; readonly category: "early_swap_closure" | "attention"; } interface SqlPaymentRepository extends PaymentRepository { /** Read-only, hash-keyset candidate report; normal routes never call this. */ listRepairCandidates(input?: { after?: string; limit?: number; }): Promise<{ candidates: readonly PaymentRepairCandidate[]; nextCursor: string | null; }>; /** Requeue one reviewed row only if its reported status and version still match. */ requeueAttempt(input: { paymentHash: string; expectedStatus: "expired" | "attention"; expectedUpdatedAt: number; reason: string; }): Promise; /** * Replay-safe settlement transaction: set the attempt's `paid_at`/`settled` * status once, and run `fulfill` inside the same transaction only for the * order's first settled attempt. A later sibling settlement is recorded with * reason `duplicate_settlement` and never fulfills again. Returns whether * this call won the order's first-settlement claim (and therefore ran * `fulfill`). */ markPaidOnce(input: { paymentHash: string; paidAt: number; details?: PaymentDetails; }, fulfill: PaymentSettlementHook): Promise; } /** * The canonical payment-attempts DDL, rendered as one executable script. The * statements themselves live in `@openreceive/core` * (`paymentsDdlStatements`) so this helper and the scaffold CLI's * ORM migrations can never drift from each other. A mysql2 connection runs a * script of several statements only when opened with `multipleStatements`; * otherwise run `paymentsSchemaStatements` one at a time. */ declare function paymentsSchemaSql(dialect: PaymentsDialect, tableName?: string, metaTableName?: string): string; /** The same DDL as `paymentsSchemaSql`, one statement per entry, without semicolons. */ declare function paymentsSchemaStatements(dialect: PaymentsDialect, tableName?: string, metaTableName?: string): readonly string[]; /** * Library-owned payment-attempt repository over the host application's existing * database. Owns the commit locking, settlement write-once, and reconciliation * state transitions so host applications never implement them. */ declare function createSqlPayments(db: SqlDatabase, options?: SqlPaymentsOptions): SqlPaymentRepository; /** How a server reaches its Supabase project. */ interface SupabaseStorageOptions { /** The project URL, `https://.supabase.co` (`SUPABASE_URL`). */ readonly url: string; /** * The project's secret key (`sb_secret_…`) or its legacy `service_role` * key. Server-only: never in a `VITE_` or `NEXT_PUBLIC_` variable, never in * browser code, never in logs. */ readonly key: string; /** A `fetch` to use instead of the global one. */ readonly fetch?: (input: string, init: RequestInit) => Promise; } interface SupabasePaymentsOptions extends SupabaseStorageOptions { readonly clock?: () => number; } interface SupabasePaymentRepository extends PaymentRepository { /** * Record this pending attempt's settlement. For the reference's first * settled attempt, `public.openreceive_on_paid` runs in the same * transaction, and the call returns true. A failure there records nothing. */ recordSettlement(settlement: SettlementRecord): Promise; } /** * The payment repository for Supabase over its HTTPS API. Apply the migration * from `npx openreceive scaffold payments --supabase` first; the repository * refuses to serve until the functions are in place, `openreceive_on_paid` is * yours, and Supabase's `anon` and `authenticated` roles can reach none of it. */ declare function createSupabasePayments(options: SupabasePaymentsOptions): SupabasePaymentRepository; interface CreateOpenReceiveHostBaseOptions { /** * The trusted price for a reference, or `null` when there is nothing to * pay for (the request is answered 404). Called only where a price is * minted or quoted; the payer never sends an amount. */ readonly amountFor: (reference: string, context: ResolveCheckoutContext) => HostCheckoutPrice | null | Promise; readonly clock?: () => number; } /** * Default mode: OpenReceive owns the payment-attempt rows in the host * application's existing database. `onPaid` runs inside the settlement * transaction for the first settled attempt for a reference only. */ interface CreateHostDbOptions extends CreateOpenReceiveHostBaseOptions { readonly db: SqlDatabase; /** Payment attempts table name. Default `openreceive_payments`. */ readonly tableName?: string; readonly onPaid: PaymentSettlementHook; readonly payments?: never; readonly supabase?: never; } /** * Supabase over its HTTPS API, for runtimes that cannot open a Postgres * connection to Supabase (Cloudflare Workers, Lovable). The tables and the * write functions come from `openreceive scaffold payments --supabase`, and * fulfillment is the host's SQL function `public.openreceive_on_paid`, which * runs inside the settlement transaction for the first settled attempt for a * reference only. There is no JS `onPaid` in this mode. */ interface CreateHostSupabaseOptions extends CreateOpenReceiveHostBaseOptions { readonly supabase: SupabaseStorageOptions; readonly onPaid?: never; readonly db?: never; readonly tableName?: never; readonly payments?: never; } /** Custom fulfillment receives the resolved reference and its transaction handle. */ type SettlementEvent = SettlementContext; type SettlementEventHook = (settlement: SettlementEvent) => void | Promise; /** The custom repository awaits onPaid before committing; any failure rolls back both writes. */ interface CreateHostRepositoryOptions extends CreateOpenReceiveHostBaseOptions { readonly payments: PaymentRepository; /** Awaited before the winning first-reference settlement transaction commits. */ readonly onPaid: SettlementEventHook; readonly db?: never; readonly tableName?: never; readonly supabase?: never; } type CreateHostOptions = CreateHostDbOptions | CreateHostSupabaseOptions | CreateHostRepositoryOptions; interface Host { readonly resolveCheckout: ResolveCheckoutHook; readonly onCheckoutCreated: CheckoutCreatedHook; readonly onPaid: NodeSettlementActionHook; readonly payments: PaymentRepository; } /** * Build the mounted-route host integration around the host's price hook and * the host database handle (`db`, default), a Supabase project reached over * HTTPS (`supabase`), or a custom payment repository (`payments`, advanced). * Attempt selection, commit locking, settlement write-once, and * reconciliation transitions are library-owned in `db` and `supabase` mode. */ declare function createHost(options: CreateHostOptions): Host; /** * What the payer is buying, in the host's own words — one optional display * string returned beside the price by `amountFor`. It is peeled off here so the * amount handed to the minting service stays exactly the price. */ type HostCheckoutPrice = CreateCheckoutAmount & { readonly description?: string; }; interface IpRateLimitConfig { /** Maximum invoice creations per IP per rolling hour. Default 60. */ readonly limitPerHour?: number; /** Optional additional cap per rolling 24 hours. Unset = hourly cap only. */ readonly limitPerDay?: number; /** Payer-facing message on the 429 response. */ readonly message?: string; /** * Actions to throttle. Default: both invoice-minting actions. Only * `checkout.create` and `swap.create` are accepted — attempt-row counting counts * mints, so a throttle on any other action could never trigger. Police other * actions with a custom `rateLimitHook` instead. */ readonly actions?: readonly AuthorizeAction[]; /** * Client IP extractor. The default reads `native.ip` (Express/Fastify request). * Returning undefined allows the request — the limiter fails open rather than * blocking payers the host cannot attribute, and warns once per limiter so an * adapter that never supplies an IP is visible. */ readonly ip?: (context: AuthorizeContext) => string | undefined; /** * Count invoice attempts for this IP at or after `sinceUnixSeconds`. The handler * wires this to the payment repository's `countAttemptsFromIp` automatically. * Required: without a counter, construction throws — there is no ephemeral * in-process fallback. */ readonly countAttemptsFromIp?: (clientIp: string, sinceUnixSeconds: number) => number | Promise; /** Clock in unix seconds; override in tests. */ readonly now?: () => number; } /** * Build the per-IP invoice rate limit behind the handler's `rateLimiting` option. * Hosts composing their own policy can call this directly and pass the result as * `rateLimitHook`. Over-limit requests fail with `429 RATE_LIMITED` and a retryable, * payer-facing message; requests with no attributable IP are always allowed. * Fails closed at construction when no `countAttemptsFromIp` counter is supplied. */ declare function createIpRateLimit(config?: IpRateLimitConfig): RateLimit; /** * The `rateLimiting` slice of the handler options an adapter passes on when the * host opted into a trusted forwarded IP header. Shared by @openreceive/express, * /fastify and /next so the three adapters cannot drift on a security control. * * `trustProxyIpHeader` is `true` for `x-forwarded-for`, or the name of another * header YOUR reverse proxy sets; the extracted first hop becomes the limiter's * `ip` unless the host supplied its own extractor. * * `requireIpSource` names the adapter for adapters whose native request carries * NO socket IP (Next: a web Request has none). Those must fail loud at * construction rather than build a limiter that can never attribute a request; * adapters that do have a socket-IP fallback (Express/Fastify `req.ip`) leave it * unset and simply keep the default limiter when no header is trusted. */ declare function createProxyRateLimitingConfig(rateLimiting: boolean | IpRateLimitConfig | undefined, trustProxyIpHeader: boolean | string | undefined, options?: { readonly requireIpSource?: string; }): { readonly rateLimiting?: boolean | IpRateLimitConfig; }; /** * Client IP the adapter attributed to this request: `native.ip` (Express/Fastify). * Undefined when no adapter IP is available — callers must treat that as * "unattributable", never as an error. */ declare function resolveClientIp(context: Pick): string | undefined; interface CheckoutCreatedInput { readonly reference: string; readonly paymentHash: string; readonly checkout: Checkout; /** Sensitive server-only provider state. Persist it on the payment attempt; never send it to a browser. */ readonly swapData?: SwapData; /** * Client IP the adapter attributed to this request, stored on the attempt row. * Backs the opt-in `rateLimiting` option; absent when no IP was attributable. */ readonly clientIp?: string; } type CheckoutCreatedHook = (input: CheckoutCreatedInput) => void | Promise; interface ResolveCheckoutContext { readonly action: AuthorizeAction; readonly request: Request; readonly reference: string; readonly payInAsset?: string; /** Untrusted payer input. Use it only to locate/recompute host-owned data. */ readonly input: Readonly>; } interface ResolvedHostCheckout { /** * Host-owned price. Payer input is never an amount authority. Required for * prepare/create/quote actions; status and refund actions on a committed * attempt never need (or wait for) host pricing. */ readonly amount?: CreateCheckoutAmount; /** * What the payer is buying, in the host's own words — one display string, * echoed on the prepare and create responses so the shipped checkout can show * something other than a QR and a number. OpenReceive owns no line items and * never will; this is the whole of what it carries. Never read from a request * body: the payer does not write the copy next to the amount. On create it is * also the default invoice memo, so the payer's wallet shows the same words * the checkout did. */ readonly description?: string; /** Return the selected host payment attempt's hash to reuse or inspect its checkout. */ readonly paymentHash?: string; /** Host-persisted safe checkout snapshot used for retry without a wallet read. */ readonly checkout?: Checkout; /** Server-only structured provider state loaded from the host database. */ readonly swapData?: SwapData; /** * The selected attempt's stored status and settlement time, when the * resolver read them. `payments/check` serves this on the row path * (`gate_busy`, a hash outside the pending set, reconcile disabled) instead * of re-reading the rows the resolver listed moments earlier in the same * request — that route is the highest-frequency one there is. A custom * `resolveCheckout` may omit it; the row is then re-read. */ readonly attemptStatus?: { readonly status: AttemptStatus; readonly paidAt: number | null; }; } type ResolveCheckoutHook = (context: ResolveCheckoutContext) => ResolvedHostCheckout | Promise; interface CreateHttpHandlerOptions { readonly service: OpenReceive; /** Host authentication and authorization policy. OpenReceive never inspects host sessions. */ readonly authorize: Authorize; /** Host authentication-independent payment integration returned by createHost. */ readonly host: Host; readonly rateLimitHook?: RateLimit; /** * Built-in per-IP invoice rate limiting. OFF by default: shared-IP deployments * (point-of-sale terminals, kiosks, NAT'd venues) mint many invoices from one * address and must never be blocked by an accidental default. Recommended for * public web shops: `rateLimiting: true` caps invoice creation at 60 per IP per * rolling hour, counted from `openreceive_payments` rows by `client_ip` — limits * survive restarts and span instances sharing the database. Requires a repository * that can count (the built-in SQL repository does); construction throws otherwise * rather than degrading to per-process memory. Applies only when a new attempt * would be minted — reuse of a committed attempt is never throttled — and fails * open per-request when no client IP is attributable. Pass a config object to * tune limits or the payer-facing message. Mutually exclusive with a custom * `rateLimitHook`. */ readonly rateLimiting?: boolean | IpRateLimitConfig; /** * Opportunistic settlement discovery, ON by default: every mounted payment * route first runs one durably gated reconcile pass when payment attempts * are pending, so abandoned checkouts settle on any later OpenReceive call * with no long-running process. Unauthenticated `GET /rates` never triggers * it — crawlers and health checks must not consume the wallet-scan budget. * The durable `openreceive_meta` gate (min 2s, stretched by invoice age) is * shared by every worker on the host database, so rapid calls collapse to * one real wallet scan per interval; `payments/check` serves the requested * hash from that same pass — one gate claim per request, never a second * per-invoice wallet walk. Requires `payments.claimReconcileGate` (the * built-in SQL repository has it); construction throws otherwise rather * than degrading silently. A dedicated notifications worker claims the same * gate, so running both never double-scans. Pass `false` to disable or * `{ minIntervalSeconds }` to tune. */ readonly opportunisticReconcile?: boolean | { readonly minIntervalSeconds?: number; }; /** * Clock override (unix seconds) for every time-dependent decision the * handler makes: the opportunistic reconcile gate AND the `payments/check` * payment-methods cache TTL. A test that overrides it to control the gate * also controls that cache — which is the point (one clock, one handler), * but worth knowing before a frozen clock keeps a warmed catalog forever. */ readonly clock?: () => number; readonly prefix?: string; /** * Cross-origin access, OFF by default: the routes serve the host's own pages * and refuse a cross-site request. A checkout whose page is on another * origin than these routes (a Supabase Edge Function on .supabase.co, * with the shop elsewhere) lists that page's origins here, exactly * (`https://shop.example`). A listed origin's preflight is answered, its * cross-site requests pass, and each response names it in * `Access-Control-Allow-Origin`. Credentials are never allowed, so the * browser sends no cookies with them: `authorize` checks a token the page * sends instead, such as `Authorization: Bearer `, and a * cookie-based `authorize` refuses them. Every other origin stays refused. */ readonly cors?: { readonly origins: readonly string[]; }; } interface HttpHandler { (request: Request, extras?: { native?: unknown; }): Promise; readonly prefix: string; handle(request: Request, extras?: { native?: unknown; }): Promise; } declare function createHttpHandler(options: CreateHttpHandlerOptions): HttpHandler; interface NotificationListener { /** Unsubscribe from wallet notifications and wait for any in-flight pass. */ stop(): Promise | void; } /** * Opt-in NWC-02 notification listener. Notifications are authenticated wallet * data: a `payment_received` payload that satisfies the settlement rule * (`settled_at` or a settled transaction state — never a preimage alone) and * matches a pending attempt settles that attempt directly through * `host.onPaid`, with no redundant wallet scan for that invoice. Anything less * — no payload, no finality signal, or an unknown/not-pending hash — wakes one * reconcile pass instead, claimed through the durable scan gate so listeners, * workers, and the request path share one wallet-scan budget. Bursts coalesce: * while a pass runs, at most one follow-up pass is queued. Errors go to * `onError` (default: a sanitized console.warn); a direct-settlement failure * also falls back to a scan so the safety net covers it. The polling * reconciler remains the safety net for * notifications missed while offline. Direct settlement assumes the NWC client * binds notification decryption to the connection's wallet pubkey (the bundled * SDK does). */ declare function startNotificationListener(input: { readonly service: OpenReceive; readonly host: Host; readonly overlapSeconds?: number; readonly onError?: (error: unknown) => void; }): Promise; interface NotificationWorker { /** Unsubscribe, stop the periodic pass, and wait for in-flight work. */ stop(): Promise; /** Resolves after `stop()` once the periodic loop has drained. */ readonly done: Promise; } /** * The OPTIONAL case-2 worker: one separate long-lived process that both * listens for NWC-02 `payment_received` notifications AND runs the same * one-pass reconcile on an interval — the safety net for notifications missed * while this worker was down. The web process never does this; its default is * request-path opportunistic reconcile. Every pass — worker or web — claims * the same durable scan gate, so running both never double-scans the wallet. * A wallet without notification support degrades to the periodic pass alone * (reported via `onError`). * * There is no host-aware CLI for this: wire it from a small host script that * owns `service` and `host` (see the scaffold wiring guide). */ declare function startNotificationWorker(input: { readonly service: OpenReceive; readonly host: Host; /** Periodic safety-net pass interval. Default 15 seconds. */ readonly pollIntervalMs?: number; readonly overlapSeconds?: number; readonly onError?: (error: unknown) => void; }): Promise; /** * The one-factory happy path (T1): three callbacks plus a db handle. The stack * builds the service, host, and handler in one call — the composed * `{ service, host, ... }` form and every individual piece stay exported for * tests and custom repositories. * * The stack starts NO background process: settlement of abandoned checkouts * happens opportunistically when any later OpenReceive call wins the durable * reconcile gate (the handler's default `opportunisticReconcile`). Hosts that * want push notifications or a poll loop run the optional worker — * `startNotificationWorker` — in a separate process. */ /** * The wallet the stack talks to: a receive-only NWC connection string — the * stack builds and owns the client, and `close()` closes it — or a prebuilt * (or promised) service for custom options, whose lifecycle stays yours. */ type StackWallet = { readonly nwc: string; readonly service?: never; } | { readonly service: OpenReceive | Promise; readonly nwc?: never; }; /** * Where attempts live, which decides what `onPaid` receives. With the host * database handle (`db`, the default mode) it is the per-reference * `PaymentSettlement`, with `reference` and the transactional `query`; * with a custom repository (`payments`, advanced) it includes the resolved * reference and the repository transaction handle. With a Supabase project * reached over HTTPS (`supabase`) there is no `onPaid`: the host's SQL * function `public.openreceive_on_paid` fulfills inside the settlement * transaction. The branch carries the hook's type, so the wrong signature is * a type error rather than a runtime surprise. */ type StackStorage = Pick | Pick | Pick, "payments" | "onPaid" | "db" | "tableName" | "supabase">; interface CreateStackOptions extends Omit, Omit { readonly wallet: StackWallet; readonly storage: StackStorage; /** * Where the one boot-failure line goes. Boot happens before any service * exists, so this is the only sink @openreceive/http can offer; it defaults * to `console.error`. The message only ever carries `error.message` — a * boot-time wallet error object carries a raw cause that has passed through * none of the redaction the service wires for every other line. */ readonly onBootFailure?: (message: string) => void; } interface Stack { /** * Handler that boots lazily: the first request (and `ready`) awaits service * construction. Boot failures surface on `ready` and on every request. */ readonly handler: HttpHandler; /** Resolves when the service and handler are up. */ readonly ready: Promise; /** Closes the service if the stack created it. */ close(): Promise; } declare function createStack(options: CreateStackOptions): Stack; /** * True when adapter options are the flat all-in-one form (no prebuilt `host`). * The composed form always carries `host`; the flat form carries the host * hooks directly. Composed options that merely forgot `host` (e.g. * `{ service, authorize }`) throw the missing-host error instead of entering * the all-in-one path and blaming the caller for omitting nwc/db/onPaid. */ declare function isStackOptions(options: CreateHttpHandlerOptions | CreateStackOptions): options is CreateStackOptions; export { createHttpHandler as $, type AttemptStatus as A, type ResolveCheckoutContext as B, type CheckoutCreatedHook as C, type ResolveCheckoutHook as D, type ResolvedHostCheckout as E, type ServiceErrorShape as F, type SettlementContext as G, type Host as H, type IpRateLimitConfig as I, type SettlementEvent as J, type SettlementEventHook as K, type SettlementRecord as L, type SqlClient as M, type NotificationListener as N, type SqlDatabase as O, type PaymentInsert as P, type SqlPaymentRepository as Q, type ReconcilableAttempt as R, type SqlAdapter as S, type SqlPaymentsOptions as T, type SqlQuery as U, type Stack as V, type StackStorage as W, type StackWallet as X, type SupabasePaymentRepository as Y, type SupabaseStorageOptions as Z, createHost as _, type Authorize as a, createIpRateLimit as a0, createProxyRateLimitingConfig as a1, createRequestId as a2, createSqlPayments as a3, createStack as a4, createSupabasePayments as a5, errorResponse as a6, hostError as a7, isServiceErrorShape as a8, isStackOptions as a9, jsonResponse as aa, mapHostRouteError as ab, paymentsSchemaSql as ac, paymentsSchemaStatements as ad, resolveClientIp as ae, startNotificationListener as af, startNotificationWorker as ag, type AuthorizeAction as b, type AuthorizeContext as c, type AuthorizeResource as d, type CheckoutCreatedInput as e, type CreateHostDbOptions as f, type CreateHostOptions as g, type CreateHostRepositoryOptions as h, type CreateHostSupabaseOptions as i, type CreateHttpHandlerOptions as j, type CreateStackOptions as k, type HostCheckoutPrice as l, HostError as m, HttpError as n, type HttpHandler as o, type NotificationWorker as p, type PaymentRecord as q, type PaymentRepairCandidate as r, type PaymentRepository as s, type PaymentSettlement as t, type PaymentSettlementHook as u, type RateLimit as v, type ReconcileCursor as w, type ReconcileGateClaim as x, type ReconcileScheduler as y, type ReconciliationTransition as z };