@openreceive/http 0.2.1

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.
@@ -0,0 +1,722 @@
1
+ import { Checkout, SwapData, NodeSettlementActionHook, CreateCheckoutAmount, NodeSettlementActionInput, OpenReceive } from '@openreceive/node';
2
+ import { ErrorBody, ErrorCode, TransactionSettlementStatus, PaymentDetails } from '@openreceive/core';
3
+
4
+ type AuthorizeAction = "checkout.prepare" | "checkout.create" | "payment.check" | "swap.quote" | "swap.create" | "swap.read" | "swap.refund";
5
+ /**
6
+ * Copied from the payer's JSON body before any host lookup. `reference` is on
7
+ * every order-scoped route; `paymentHash` is also set on `payment.check`,
8
+ * `swap.read`, and `swap.refund`. Either value identifies a row — it does not
9
+ * prove this caller owns it.
10
+ */
11
+ interface AuthorizeResource {
12
+ reference?: string;
13
+ paymentHash?: string;
14
+ }
15
+ interface AuthorizeContext {
16
+ readonly action: AuthorizeAction;
17
+ readonly request: Request;
18
+ readonly resource: AuthorizeResource;
19
+ /**
20
+ * The untouched framework-native request (Express req, Fastify request), when
21
+ * an adapter provides one. Use it for middleware-attached state like
22
+ * req.session. Same `authorize` callback as `request`; this is not a second
23
+ * signature.
24
+ */
25
+ readonly native?: unknown;
26
+ }
27
+ /**
28
+ * One callback, one argument. Return `true` to allow, `false` for 403.
29
+ * May be sync or async. Destructure whichever fields of `context` you need —
30
+ * they are always present except `native`, which adapters fill in.
31
+ */
32
+ type Authorize = (context: AuthorizeContext) => boolean | Promise<boolean>;
33
+ type RateLimit = Authorize;
34
+
35
+ /**
36
+ * A control-flow error the handler maps to a JSON error response. Carries the HTTP status, a
37
+ * shared error code, and an optional `retryable` hint / `details` object.
38
+ */
39
+ declare class HttpError extends Error {
40
+ readonly status: number;
41
+ readonly code: ErrorCode;
42
+ readonly retryable?: boolean;
43
+ readonly details?: Record<string, unknown>;
44
+ /** Emitted as a `Retry-After` header so clients can back off with a hint. */
45
+ readonly retryAfterSeconds?: number;
46
+ constructor(status: number, code: ErrorCode, message: string, options?: {
47
+ readonly retryable?: boolean;
48
+ readonly details?: Record<string, unknown>;
49
+ readonly retryAfterSeconds?: number;
50
+ });
51
+ }
52
+ /** Shape a service error carries: a numeric status and an OpenReceive error body. */
53
+ interface ServiceErrorShape {
54
+ readonly status: number;
55
+ readonly body: ErrorBody;
56
+ }
57
+ /**
58
+ * Duck-type an ServiceError (from @openreceive/node) without importing the class, so the
59
+ * handler stays runtime-agnostic and never breaks on cross-module `instanceof` identity mismatches
60
+ * (source vs. built dist, or two copies of @openreceive/node).
61
+ */
62
+ declare function isServiceErrorShape(error: unknown): error is ServiceErrorShape;
63
+ /**
64
+ * Host-route control-flow error with the same `{ status, body }` shape as
65
+ * {@link ServiceError}. Use for cart/validation failures on app routes
66
+ * (for example the host's `/orders` route) so framework helpers can map them.
67
+ */
68
+ declare class HostError extends Error {
69
+ readonly status: number;
70
+ readonly body: ErrorBody;
71
+ constructor(status: number, body: ErrorBody);
72
+ }
73
+ /**
74
+ * Convenience factory for a host validation error (default 400 INVALID_REQUEST).
75
+ *
76
+ * `retryable` rides along only when it CONTRADICTS the code's own default —
77
+ * spelling out `retryable: false` on a CONFLICT restates what the shared error
78
+ * contract already says, and it was the one field that made the JS engine's
79
+ * conflict bodies differ from the Ruby engine's.
80
+ */
81
+ declare function hostError(message: string, status?: number, code?: ErrorCode): HostError;
82
+ /**
83
+ * Map a thrown host/service error to `{ status, body }` for app routes outside the
84
+ * mounted OpenReceive handler. Returns `null` when the value is not a known shape
85
+ * (caller should rethrow / pass to `next(error)`).
86
+ */
87
+ declare function mapHostRouteError(error: unknown): {
88
+ readonly status: number;
89
+ readonly body: ErrorBody;
90
+ } | null;
91
+ /** Generate a per-response request id used in both the body and the `X-Request-Id` header. */
92
+ declare function createRequestId(): string;
93
+ /**
94
+ * Serialize a value as a snake_case JSON response with the shared content-type and request-id header.
95
+ * `extraHeaders` are appended (not set) — used for `Retry-After` on 429s. Note for adapters:
96
+ * multi-value headers other than the ones used here would need per-framework handling
97
+ * (Express folds repeated `Set-Cookie` appends); the handler deliberately emits none.
98
+ */
99
+ declare function jsonResponse(status: number, body: unknown, requestId: string, extraHeaders?: Iterable<readonly [string, string]>): Response;
100
+ /**
101
+ * Map any thrown value to a JSON error response with a code from the shared enum. Service errors keep
102
+ * their status/body (with `request_id` ensured); handler errors map to their status; anything else is
103
+ * a generic 500 INTERNAL so internal messages never leak.
104
+ */
105
+ declare function errorResponse(error: unknown, requestId: string): Response;
106
+
107
+ /**
108
+ * Seconds past an attempt's expiry during which reconciliation still scans for a
109
+ * settlement before closing the attempt. Covers clock skew and wallets that
110
+ * accept a payment moments after nominal invoice expiry.
111
+ */
112
+ declare const OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS: 900;
113
+ /**
114
+ * Lifecycle of one payment attempt: {@link TransactionSettlementStatus} plus
115
+ * `attention`. `pending` attempts participate in reconciliation; every other
116
+ * status is terminal for reconciliation purposes. `attention` marks rows an
117
+ * operator must review before the attempt moves on: the wallet still
118
+ * explicitly reports an in-flight transaction state long after invoice expiry.
119
+ */
120
+ type AttemptStatus = TransactionSettlementStatus | "attention";
121
+ /**
122
+ * The minimal row OpenReceive needs for one invoice or swap attempt.
123
+ * An order may have many of these records. `swapData` must remain server-only.
124
+ */
125
+ interface PaymentRecord {
126
+ readonly reference: string;
127
+ readonly paymentHash: string;
128
+ readonly status: AttemptStatus;
129
+ /** Optional operator-facing detail for the current status (e.g. "superseded"). */
130
+ readonly statusReason?: string | null;
131
+ readonly paidAt: number | null;
132
+ /** Unix timestamp after which these payer instructions must not be reused. */
133
+ readonly expiresAt: number;
134
+ /** Unix timestamp used only to choose deterministically between historical attempts. */
135
+ readonly createdAt: number;
136
+ /** Safe, replayable payer response. Contains no wallet or provider credentials. */
137
+ readonly checkout: Checkout;
138
+ readonly swapData?: SwapData | null;
139
+ }
140
+ interface PaymentInsert {
141
+ readonly reference: string;
142
+ readonly paymentHash: string;
143
+ readonly expiresAt: number;
144
+ readonly createdAt: number;
145
+ readonly checkout: Checkout;
146
+ readonly swapData?: SwapData;
147
+ /** Client IP captured at invoice creation, when the adapter could attribute one. */
148
+ readonly clientIp?: string;
149
+ }
150
+ /** One pending attempt the reconciler should include in its next wallet scan. */
151
+ interface ReconcilableAttempt {
152
+ readonly paymentHash: string;
153
+ /** Exact NIP-47 invoice creation time returned by make_invoice. */
154
+ readonly createdAt: number;
155
+ /** Unix timestamp after which the attempt can be closed once a scan confirms no settlement. */
156
+ readonly expiresAt: number;
157
+ }
158
+ /** A terminal (non-settled) state transition observed by reconciliation. */
159
+ interface ReconciliationTransition {
160
+ readonly paymentHash: string;
161
+ readonly status: Exclude<AttemptStatus, "pending" | "settled">;
162
+ /** Unix timestamp of the wallet scan that justified this transition. */
163
+ readonly observedAt: number;
164
+ /** Operator-facing reason, e.g. "wallet_reported_expired" or "not_found_after_expiry". */
165
+ readonly reason: string;
166
+ }
167
+ /** One observed wallet settlement, as handed to the repository to record. */
168
+ interface SettlementRecord {
169
+ readonly paymentHash: string;
170
+ /** Unix timestamp the wallet reports (or the scan observed) the payment at. */
171
+ readonly paidAt: number;
172
+ readonly details?: PaymentDetails;
173
+ }
174
+ /**
175
+ * Persistence boundary for payment attempts. Most applications use the
176
+ * library-provided SQL repository (`createSqlPayments`); implementing
177
+ * this interface directly is the advanced escape hatch.
178
+ *
179
+ * `commitAttempt` must serialize concurrent creates for one order, reject a
180
+ * settled order or a reusable live attempt on the same rail/asset, supersede a
181
+ * near-expiry same-rail attempt, and commit before it returns. Other rails may
182
+ * remain live so the payer can switch methods. The HTTP handler withholds payer
183
+ * instructions when this method throws.
184
+ *
185
+ * `recordReconciliation` must apply the transition only while the row is still
186
+ * `pending` — it must never overwrite a settled attempt.
187
+ *
188
+ * `recordSettlement` is the write-once settlement claim: the library calls it
189
+ * for every observed settlement and runs the host's own handler only when the
190
+ * call reports the claim won, so a redelivered settlement fulfills once.
191
+ */
192
+ interface PaymentRepository {
193
+ listForReference(reference: string): Promise<readonly PaymentRecord[]>;
194
+ /**
195
+ * The oldest `pending` attempts, terminal rows excluded. A repository with a
196
+ * large backlog should return an oldest-first batch (the built-in SQL one
197
+ * caps each pass at OPENRECEIVE_RECONCILE_BATCH_SIZE); the remainder is
198
+ * covered by later passes.
199
+ */
200
+ listReconcilableAttempts(): Promise<readonly ReconcilableAttempt[]>;
201
+ /**
202
+ * The `pending` attempt for one payment hash, or undefined when the hash is
203
+ * unknown or already terminal. Direct settlement from an NWC notification
204
+ * needs a by-hash answer: `listReconcilableAttempts` is an oldest-first
205
+ * batch, so a notified attempt behind a backlog would look unknown. A
206
+ * repository that omits this falls back to batch membership, which delays
207
+ * notified settlements past a batch-sized backlog.
208
+ */
209
+ findPendingAttempt?(paymentHash: string): Promise<ReconcilableAttempt | undefined>;
210
+ commitAttempt(input: CheckoutCreatedInput): void | Promise<void>;
211
+ recordReconciliation(transition: ReconciliationTransition): void | Promise<void>;
212
+ /**
213
+ * Claim the order's first settlement for this attempt and persist it.
214
+ * Returns true only for the call that won the claim — the attempt was still
215
+ * unsettled AND no sibling attempt on the order had settled. Later calls for
216
+ * the same or a sibling attempt must record the settlement (a genuine second
217
+ * payment is not discarded) and return false. A settled attempt is never
218
+ * overwritten, and an unknown payment hash is a no-op returning false.
219
+ */
220
+ recordSettlement(settlement: SettlementRecord): boolean | Promise<boolean>;
221
+ /**
222
+ * Count attempt rows recorded for this client IP at or after `sinceUnixSeconds`.
223
+ * Backs the handler's opt-in `rateLimiting` option; when a custom repository
224
+ * omits it, enabling `rateLimiting` fails at construction (there is no
225
+ * in-memory fallback — use a custom `rateLimitHook` instead).
226
+ */
227
+ countAttemptsFromIp?(clientIp: string, sinceUnixSeconds: number): number | Promise<number>;
228
+ /**
229
+ * Claim the durable global reconcile gate: return true when this caller may
230
+ * run a wallet scan now, false when another worker scanned within
231
+ * `intervalSeconds` (`gate_busy`). The claim MUST be a durable compare-and-set
232
+ * shared by every process on the host database (the built-in SQL repository
233
+ * uses the `openreceive_meta` key/value/rev table) — process-local memory
234
+ * cannot coordinate multiple workers and must never back this. Backs the
235
+ * handler's default opportunistic reconcile; when a custom repository omits
236
+ * it, construction throws unless `opportunisticReconcile: false` — the
237
+ * default settlement path never degrades silently.
238
+ */
239
+ claimReconcileGate?(input: {
240
+ readonly now: number;
241
+ readonly intervalSeconds: number;
242
+ }): boolean | Promise<boolean>;
243
+ }
244
+
245
+ /**
246
+ * Runs one SQL statement and returns SELECT rows (`[]` otherwise). The SQL
247
+ * arrives written for the adapter's own dialect — `?` placeholders on sqlite,
248
+ * `$1`-style on postgres — so pass it to the driver verbatim. Nothing rewrites
249
+ * it: a `?` inside a string literal, a comment, or a postgres JSON operator
250
+ * (`data ? 'field'`) must survive untouched.
251
+ */
252
+ type SqlQuery = (sql: string, params?: readonly unknown[]) => Promise<readonly Record<string, unknown>[]>;
253
+ interface SqlClient {
254
+ readonly query: SqlQuery;
255
+ }
256
+ /**
257
+ * The escape-hatch database boundary: a dialect, a query function, and a
258
+ * transaction wrapper. The built-in bindings cover `pg` pools/clients and
259
+ * SQLite (`node:sqlite` or better-sqlite3) without adding dependencies.
260
+ */
261
+ interface SqlAdapter extends SqlClient {
262
+ readonly dialect: "postgres" | "sqlite";
263
+ transaction<T>(run: (tx: SqlClient) => Promise<T>): Promise<T>;
264
+ }
265
+ /**
266
+ * Structural view of a `pg` Pool or Client. A Pool's `connect()` checks out a
267
+ * per-transaction client; a Client's `connect()` opens its one socket and
268
+ * resolves nothing, so it must be called at most once. A handle with only
269
+ * `query` is treated as a single connection and transactions serialize on it.
270
+ */
271
+ interface PgLike {
272
+ query(sql: string, params?: unknown[]): Promise<{
273
+ rows: Record<string, unknown>[];
274
+ }>;
275
+ connect?(): Promise<unknown>;
276
+ }
277
+ /** Structural view of `node:sqlite` DatabaseSync or a better-sqlite3 Database. */
278
+ interface SqliteLike {
279
+ prepare(sql: string): {
280
+ all(...params: unknown[]): unknown[];
281
+ run(...params: unknown[]): unknown;
282
+ };
283
+ exec(sql: string): void;
284
+ }
285
+ /** Any database handle `createSqlPayments` accepts. */
286
+ type SqlDatabase = SqlAdapter | PgLike | SqliteLike;
287
+
288
+ /**
289
+ * Oldest-first page size for one reconciliation pass. A backlog of pending
290
+ * attempts is drained over several passes instead of loading every row (and
291
+ * scanning every invoice's window) in one unbounded query.
292
+ */
293
+ declare const OPENRECEIVE_RECONCILE_BATCH_SIZE: 200;
294
+ interface SqlPaymentsOptions {
295
+ /** Payment attempts table name. Default `openreceive_payments`. */
296
+ readonly tableName?: string;
297
+ /** Durable reconcile-gate key/value table name. Default `openreceive_meta`. */
298
+ readonly metaTableName?: string;
299
+ readonly clock?: () => number;
300
+ }
301
+ /** Settlement context passed to the host's `onPaid` in library-persistence mode. */
302
+ interface PaymentSettlement {
303
+ readonly reference: string;
304
+ readonly paymentHash: string;
305
+ readonly paidAt: number;
306
+ readonly details?: PaymentDetails;
307
+ /**
308
+ * Runs statements inside the settlement transaction. Use it to update the
309
+ * host order or insert a transactional outbox row. Write the placeholders
310
+ * your database uses (`?` on sqlite, `$1`-style on postgres) — this SQL is
311
+ * yours and reaches the driver exactly as written.
312
+ *
313
+ * OpenReceive already guarantees this hook fires at most once per reference
314
+ * across its own settlement paths. It cannot see fulfillment triggered
315
+ * anywhere else, though —
316
+ * an admin action, a second processor, a replayed job — so if any of those
317
+ * exist, guard the transition here rather than assuming exclusivity:
318
+ *
319
+ * ```sql
320
+ * UPDATE orders SET state = 'paid'
321
+ * WHERE id = $1 AND state = 'awaiting_payment' RETURNING id
322
+ * ```
323
+ *
324
+ * An empty result means someone else already fulfilled it; return without
325
+ * shipping. `fulfillmentNote` in `@openreceive/core` is the full
326
+ * version, and is what the scaffold writes into every generated file.
327
+ */
328
+ readonly query: SqlQuery;
329
+ }
330
+ type PaymentSettlementHook = (settlement: PaymentSettlement) => void | Promise<void>;
331
+ interface SqlPaymentRepository extends PaymentRepository {
332
+ /**
333
+ * Replay-safe settlement transaction: set the attempt's `paid_at`/`settled`
334
+ * status once, and run `fulfill` inside the same transaction only for the
335
+ * order's first settled attempt. A later sibling settlement is recorded with
336
+ * reason `duplicate_settlement` and never fulfills again. Returns whether
337
+ * this call won the order's first-settlement claim (and therefore ran
338
+ * `fulfill`).
339
+ */
340
+ markPaidOnce(input: {
341
+ paymentHash: string;
342
+ paidAt: number;
343
+ details?: PaymentDetails;
344
+ }, fulfill: PaymentSettlementHook): Promise<boolean>;
345
+ }
346
+ /**
347
+ * The canonical payment-attempts DDL, rendered as one executable script. The
348
+ * statements themselves live in `@openreceive/core`
349
+ * (`paymentsDdlStatements`) so this helper and the scaffold CLI's
350
+ * ORM migrations can never drift from each other.
351
+ */
352
+ declare function paymentsSchemaSql(dialect: "postgres" | "sqlite", tableName?: string, metaTableName?: string): string;
353
+ /**
354
+ * Library-owned payment-attempt repository over the host application's existing
355
+ * database. Owns the commit locking, settlement write-once, and reconciliation
356
+ * state transitions so host applications never implement them.
357
+ */
358
+ declare function createSqlPayments(db: SqlDatabase, options?: SqlPaymentsOptions): SqlPaymentRepository;
359
+
360
+ interface CreateOpenReceiveHostBaseOptions {
361
+ /**
362
+ * The trusted price for a reference, or `null` when there is nothing to
363
+ * pay for (the request is answered 404). Called only where a price is
364
+ * minted or quoted; the payer never sends an amount.
365
+ */
366
+ readonly amountFor: (reference: string, context: ResolveCheckoutContext) => CreateCheckoutAmount | null | Promise<CreateCheckoutAmount | null>;
367
+ readonly clock?: () => number;
368
+ }
369
+ /**
370
+ * Default mode: OpenReceive owns the payment-attempt rows in the host
371
+ * application's existing database. `onPaid` runs inside the settlement
372
+ * transaction for the first settled attempt for a reference only.
373
+ */
374
+ interface CreateHostDbOptions extends CreateOpenReceiveHostBaseOptions {
375
+ readonly db: SqlDatabase;
376
+ /** Payment attempts table name. Default `openreceive_payments`. */
377
+ readonly tableName?: string;
378
+ readonly onPaid: PaymentSettlementHook;
379
+ readonly payments?: never;
380
+ }
381
+ /**
382
+ * Settlement context passed to repository-mode `onPaid`: the raw core
383
+ * settlement event (`paymentHash`, `paidAt`, `details`). Unlike db-mode's
384
+ * {@link PaymentSettlement} it carries no `reference` and no
385
+ * transactional `query` — the custom repository owns that mapping.
386
+ */
387
+ type SettlementEvent = NodeSettlementActionInput;
388
+ type SettlementEventHook = (settlement: SettlementEvent) => void | Promise<void>;
389
+ /**
390
+ * Advanced escape hatch: the host implements the full
391
+ * `PaymentRepository` contract, including commit locking, write-once
392
+ * settlement, and reconciliation transitions.
393
+ *
394
+ * The settlement hook is `onPaid` in this mode too, but its context type
395
+ * differs from db mode: it receives the raw {@link SettlementEvent}
396
+ * (`paymentHash`, `paidAt`, `details`), with no `reference` and no transactional
397
+ * `query` — unlike db-mode `onPaid`, which runs inside the library's settlement
398
+ * transaction. Write-once is still the library's: repository-mode `onPaid`
399
+ * runs only for the settlement whose `payments.recordSettlement` claim was
400
+ * won, so a redelivered settlement never fulfills twice.
401
+ */
402
+ interface CreateHostRepositoryOptions extends CreateOpenReceiveHostBaseOptions {
403
+ readonly payments: PaymentRepository;
404
+ /** Host settlement handler; runs once, for the winning first-settlement claim. */
405
+ readonly onPaid: SettlementEventHook;
406
+ readonly db?: never;
407
+ readonly tableName?: never;
408
+ }
409
+ type CreateHostOptions = CreateHostDbOptions | CreateHostRepositoryOptions;
410
+ interface Host {
411
+ readonly resolveCheckout: ResolveCheckoutHook;
412
+ readonly onCheckoutCreated: CheckoutCreatedHook;
413
+ readonly onPaid: NodeSettlementActionHook;
414
+ readonly payments: PaymentRepository;
415
+ }
416
+ /**
417
+ * Build the mounted-route host integration around the host's price hook and
418
+ * either the host database handle (`db`, default) or a custom payment
419
+ * repository (`payments`, advanced). Attempt selection, commit locking,
420
+ * settlement write-once, and reconciliation transitions are library-owned in
421
+ * `db` mode.
422
+ */
423
+ declare function createHost(options: CreateHostOptions): Host;
424
+
425
+ interface IpRateLimitConfig {
426
+ /** Maximum invoice creations per IP per rolling hour. Default 60. */
427
+ readonly limitPerHour?: number;
428
+ /** Optional additional cap per rolling 24 hours. Unset = hourly cap only. */
429
+ readonly limitPerDay?: number;
430
+ /** Payer-facing message on the 429 response. */
431
+ readonly message?: string;
432
+ /**
433
+ * Actions to throttle. Default: both invoice-minting actions. Only
434
+ * `checkout.create` and `swap.create` are accepted — attempt-row counting counts
435
+ * mints, so a throttle on any other action could never trigger. Police other
436
+ * actions with a custom `rateLimitHook` instead.
437
+ */
438
+ readonly actions?: readonly AuthorizeAction[];
439
+ /**
440
+ * Client IP extractor. The default reads `native.ip` (Express/Fastify request).
441
+ * Returning undefined allows the request — the limiter fails open rather than
442
+ * blocking payers the host cannot attribute, and warns once per limiter so an
443
+ * adapter that never supplies an IP is visible.
444
+ */
445
+ readonly ip?: (context: AuthorizeContext) => string | undefined;
446
+ /**
447
+ * Count invoice attempts for this IP at or after `sinceUnixSeconds`. The handler
448
+ * wires this to the payment repository's `countAttemptsFromIp` automatically.
449
+ * Required: without a counter, construction throws — there is no ephemeral
450
+ * in-process fallback.
451
+ */
452
+ readonly countAttemptsFromIp?: (clientIp: string, sinceUnixSeconds: number) => number | Promise<number>;
453
+ /** Clock in unix seconds; override in tests. */
454
+ readonly now?: () => number;
455
+ }
456
+ /**
457
+ * Build the per-IP invoice rate limit behind the handler's `rateLimiting` option.
458
+ * Hosts composing their own policy can call this directly and pass the result as
459
+ * `rateLimitHook`. Over-limit requests fail with `429 RATE_LIMITED` and a retryable,
460
+ * payer-facing message; requests with no attributable IP are always allowed.
461
+ * Fails closed at construction when no `countAttemptsFromIp` counter is supplied.
462
+ */
463
+ declare function createIpRateLimit(config?: IpRateLimitConfig): RateLimit;
464
+ /**
465
+ * The `rateLimiting` slice of the handler options an adapter passes on when the
466
+ * host opted into a trusted forwarded IP header. Shared by @openreceive/express,
467
+ * /fastify and /next so the three adapters cannot drift on a security control.
468
+ *
469
+ * `trustProxyIpHeader` is `true` for `x-forwarded-for`, or the name of another
470
+ * header YOUR reverse proxy sets; the extracted first hop becomes the limiter's
471
+ * `ip` unless the host supplied its own extractor.
472
+ *
473
+ * `requireIpSource` names the adapter for adapters whose native request carries
474
+ * NO socket IP (Next: a web Request has none). Those must fail loud at
475
+ * construction rather than build a limiter that can never attribute a request;
476
+ * adapters that do have a socket-IP fallback (Express/Fastify `req.ip`) leave it
477
+ * unset and simply keep the default limiter when no header is trusted.
478
+ */
479
+ declare function createProxyRateLimitingConfig(rateLimiting: boolean | IpRateLimitConfig | undefined, trustProxyIpHeader: boolean | string | undefined, options?: {
480
+ readonly requireIpSource?: string;
481
+ }): {
482
+ readonly rateLimiting?: boolean | IpRateLimitConfig;
483
+ };
484
+ /**
485
+ * Client IP the adapter attributed to this request: `native.ip` (Express/Fastify).
486
+ * Undefined when no adapter IP is available — callers must treat that as
487
+ * "unattributable", never as an error.
488
+ */
489
+ declare function resolveClientIp(context: Pick<AuthorizeContext, "native">): string | undefined;
490
+
491
+ interface CheckoutCreatedInput {
492
+ readonly reference: string;
493
+ readonly paymentHash: string;
494
+ readonly checkout: Checkout;
495
+ /** Sensitive server-only provider state. Persist it on the payment attempt; never send it to a browser. */
496
+ readonly swapData?: SwapData;
497
+ /**
498
+ * Client IP the adapter attributed to this request, stored on the attempt row.
499
+ * Backs the opt-in `rateLimiting` option; absent when no IP was attributable.
500
+ */
501
+ readonly clientIp?: string;
502
+ }
503
+ type CheckoutCreatedHook = (input: CheckoutCreatedInput) => void | Promise<void>;
504
+ interface ResolveCheckoutContext {
505
+ readonly action: AuthorizeAction;
506
+ readonly request: Request;
507
+ readonly reference: string;
508
+ readonly payInAsset?: string;
509
+ /** Untrusted payer input. Use it only to locate/recompute host-owned data. */
510
+ readonly input: Readonly<Record<string, unknown>>;
511
+ }
512
+ interface ResolvedHostCheckout {
513
+ /**
514
+ * Host-owned price. Payer input is never an amount authority. Required for
515
+ * prepare/create/quote actions; status and refund actions on a committed
516
+ * attempt never need (or wait for) host pricing.
517
+ */
518
+ readonly amount?: CreateCheckoutAmount;
519
+ /** Return the selected host payment attempt's hash to reuse or inspect its checkout. */
520
+ readonly paymentHash?: string;
521
+ /** Host-persisted safe checkout snapshot used for retry without a wallet read. */
522
+ readonly checkout?: Checkout;
523
+ /** Server-only structured provider state loaded from the host database. */
524
+ readonly swapData?: SwapData;
525
+ /**
526
+ * The selected attempt's stored status and settlement time, when the
527
+ * resolver read them. `payments/check` serves this on the row path
528
+ * (`gate_busy`, a hash outside the pending set, reconcile disabled) instead
529
+ * of re-reading the rows the resolver listed moments earlier in the same
530
+ * request — that route is the highest-frequency one there is. A custom
531
+ * `resolveCheckout` may omit it; the row is then re-read.
532
+ */
533
+ readonly attemptStatus?: {
534
+ readonly status: AttemptStatus;
535
+ readonly paidAt: number | null;
536
+ };
537
+ }
538
+ type ResolveCheckoutHook = (context: ResolveCheckoutContext) => ResolvedHostCheckout | Promise<ResolvedHostCheckout>;
539
+ interface CreateHttpHandlerOptions {
540
+ readonly service: OpenReceive;
541
+ /** Host authentication and authorization policy. OpenReceive never inspects host sessions. */
542
+ readonly authorize: Authorize;
543
+ /** Host authentication-independent payment integration returned by createHost. */
544
+ readonly host: Host;
545
+ readonly rateLimitHook?: RateLimit;
546
+ /**
547
+ * Built-in per-IP invoice rate limiting. OFF by default: shared-IP deployments
548
+ * (point-of-sale terminals, kiosks, NAT'd venues) mint many invoices from one
549
+ * address and must never be blocked by an accidental default. Recommended for
550
+ * public web shops: `rateLimiting: true` caps invoice creation at 60 per IP per
551
+ * rolling hour, counted from `openreceive_payments` rows by `client_ip` — limits
552
+ * survive restarts and span instances sharing the database. Requires a repository
553
+ * that can count (the built-in SQL repository does); construction throws otherwise
554
+ * rather than degrading to per-process memory. Applies only when a new attempt
555
+ * would be minted — reuse of a committed attempt is never throttled — and fails
556
+ * open per-request when no client IP is attributable. Pass a config object to
557
+ * tune limits or the payer-facing message. Mutually exclusive with a custom
558
+ * `rateLimitHook`.
559
+ */
560
+ readonly rateLimiting?: boolean | IpRateLimitConfig;
561
+ /**
562
+ * Opportunistic settlement discovery, ON by default: every mounted payment
563
+ * route first runs one durably gated reconcile pass when payment attempts
564
+ * are pending, so abandoned checkouts settle on any later OpenReceive call
565
+ * with no long-running process. Unauthenticated `GET /rates` never triggers
566
+ * it — crawlers and health checks must not consume the wallet-scan budget.
567
+ * The durable `openreceive_meta` gate (min 2s, stretched by invoice age) is
568
+ * shared by every worker on the host database, so rapid calls collapse to
569
+ * one real wallet scan per interval; `payments/check` serves the requested
570
+ * hash from that same pass — one gate claim per request, never a second
571
+ * per-invoice wallet walk. Requires `payments.claimReconcileGate` (the
572
+ * built-in SQL repository has it); construction throws otherwise rather
573
+ * than degrading silently. A dedicated notifications worker claims the same
574
+ * gate, so running both never double-scans. Pass `false` to disable or
575
+ * `{ minIntervalSeconds }` to tune.
576
+ */
577
+ readonly opportunisticReconcile?: boolean | {
578
+ readonly minIntervalSeconds?: number;
579
+ };
580
+ /**
581
+ * Clock override (unix seconds) for every time-dependent decision the
582
+ * handler makes: the opportunistic reconcile gate AND the `payments/check`
583
+ * payment-methods cache TTL. A test that overrides it to control the gate
584
+ * also controls that cache — which is the point (one clock, one handler),
585
+ * but worth knowing before a frozen clock keeps a warmed catalog forever.
586
+ */
587
+ readonly clock?: () => number;
588
+ readonly prefix?: string;
589
+ }
590
+ interface HttpHandler {
591
+ (request: Request, extras?: {
592
+ native?: unknown;
593
+ }): Promise<Response>;
594
+ readonly prefix: string;
595
+ handle(request: Request, extras?: {
596
+ native?: unknown;
597
+ }): Promise<Response>;
598
+ }
599
+ declare function createHttpHandler(options: CreateHttpHandlerOptions): HttpHandler;
600
+
601
+ interface NotificationListener {
602
+ /** Unsubscribe from wallet notifications and wait for any in-flight pass. */
603
+ stop(): Promise<void> | void;
604
+ }
605
+ /**
606
+ * Opt-in NWC-02 notification listener. Notifications are authenticated wallet
607
+ * data: a `payment_received` payload that satisfies the settlement rule
608
+ * (`settled_at` or a settled transaction state — never a preimage alone) and
609
+ * matches a pending attempt settles that attempt directly through
610
+ * `host.onPaid`, with no redundant wallet scan for that invoice. Anything less
611
+ * — no payload, no finality signal, or an unknown/not-pending hash — wakes one
612
+ * reconcile pass instead, claimed through the durable scan gate so listeners,
613
+ * workers, and the request path share one wallet-scan budget. Bursts coalesce:
614
+ * while a pass runs, at most one follow-up pass is queued. Errors go to
615
+ * `onError` (default: a sanitized console.warn); a direct-settlement failure
616
+ * also falls back to a scan so the safety net covers it. The polling
617
+ * reconciler remains the safety net for
618
+ * notifications missed while offline. Direct settlement assumes the NWC client
619
+ * binds notification decryption to the connection's wallet pubkey (the bundled
620
+ * SDK does).
621
+ */
622
+ declare function startNotificationListener(input: {
623
+ readonly service: OpenReceive;
624
+ readonly host: Host;
625
+ readonly overlapSeconds?: number;
626
+ readonly onError?: (error: unknown) => void;
627
+ }): Promise<NotificationListener>;
628
+ interface NotificationWorker {
629
+ /** Unsubscribe, stop the periodic pass, and wait for in-flight work. */
630
+ stop(): Promise<void>;
631
+ /** Resolves after `stop()` once the periodic loop has drained. */
632
+ readonly done: Promise<void>;
633
+ }
634
+ /**
635
+ * The OPTIONAL case-2 worker: one separate long-lived process that both
636
+ * listens for NWC-02 `payment_received` notifications AND runs the same
637
+ * one-pass reconcile on an interval — the safety net for notifications missed
638
+ * while this worker was down. The web process never does this; its default is
639
+ * request-path opportunistic reconcile. Every pass — worker or web — claims
640
+ * the same durable scan gate, so running both never double-scans the wallet.
641
+ * A wallet without notification support degrades to the periodic pass alone
642
+ * (reported via `onError`).
643
+ *
644
+ * There is no host-aware CLI for this: wire it from a small host script that
645
+ * owns `service` and `host` (see the scaffold wiring guide).
646
+ */
647
+ declare function startNotificationWorker(input: {
648
+ readonly service: OpenReceive;
649
+ readonly host: Host;
650
+ /** Periodic safety-net pass interval. Default 15 seconds. */
651
+ readonly pollIntervalMs?: number;
652
+ readonly overlapSeconds?: number;
653
+ readonly onError?: (error: unknown) => void;
654
+ }): Promise<NotificationWorker>;
655
+
656
+ /**
657
+ * The one-factory happy path (T1): three callbacks plus a db handle. The stack
658
+ * builds the service, host, and handler in one call — the composed
659
+ * `{ service, host, ... }` form and every individual piece stay exported for
660
+ * tests and custom repositories.
661
+ *
662
+ * The stack starts NO background process: settlement of abandoned checkouts
663
+ * happens opportunistically when any later OpenReceive call wins the durable
664
+ * reconcile gate (the handler's default `opportunisticReconcile`). Hosts that
665
+ * want push notifications or a poll loop run the optional worker —
666
+ * `startNotificationWorker` — in a separate process.
667
+ */
668
+ /**
669
+ * The wallet the stack talks to: a receive-only NWC connection string — the
670
+ * stack builds and owns the client, and `close()` closes it — or a prebuilt
671
+ * (or promised) service for custom options, whose lifecycle stays yours.
672
+ */
673
+ type StackWallet = {
674
+ readonly nwc: string;
675
+ readonly service?: never;
676
+ } | {
677
+ readonly service: OpenReceive | Promise<OpenReceive>;
678
+ readonly nwc?: never;
679
+ };
680
+ /**
681
+ * Where attempts live, which decides what `onPaid` receives. With the host
682
+ * database handle (`db`, the default mode) it is the per-reference
683
+ * `PaymentSettlement`, with `reference` and the transactional `query`;
684
+ * with a custom repository (`payments`, advanced) it is the raw
685
+ * `SettlementEvent`. The branch carries the hook's type, so the
686
+ * wrong signature is a type error rather than a runtime surprise.
687
+ */
688
+ type StackStorage = Pick<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> | Pick<CreateHostRepositoryOptions, "payments" | "onPaid" | "db" | "tableName">;
689
+ interface CreateStackOptions extends Omit<CreateHttpHandlerOptions, "service" | "host">, Omit<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> {
690
+ readonly wallet: StackWallet;
691
+ readonly storage: StackStorage;
692
+ /**
693
+ * Where the one boot-failure line goes. Boot happens before any service
694
+ * exists, so this is the only sink @openreceive/http can offer; it defaults
695
+ * to `console.error`. The message only ever carries `error.message` — a
696
+ * boot-time wallet error object carries a raw cause that has passed through
697
+ * none of the redaction the service wires for every other line.
698
+ */
699
+ readonly onBootFailure?: (message: string) => void;
700
+ }
701
+ interface Stack {
702
+ /**
703
+ * Handler that boots lazily: the first request (and `ready`) awaits service
704
+ * construction. Boot failures surface on `ready` and on every request.
705
+ */
706
+ readonly handler: HttpHandler;
707
+ /** Resolves when the service and handler are up. */
708
+ readonly ready: Promise<void>;
709
+ /** Closes the service if the stack created it. */
710
+ close(): Promise<void>;
711
+ }
712
+ declare function createStack(options: CreateStackOptions): Stack;
713
+ /**
714
+ * True when adapter options are the flat all-in-one form (no prebuilt `host`).
715
+ * The composed form always carries `host`; the flat form carries the host
716
+ * hooks directly. Composed options that merely forgot `host` (e.g.
717
+ * `{ service, authorize }`) throw the missing-host error instead of entering
718
+ * the all-in-one path and blaming the caller for omitting nwc/db/onPaid.
719
+ */
720
+ declare function isStackOptions(options: CreateHttpHandlerOptions | CreateStackOptions): options is CreateStackOptions;
721
+
722
+ export { hostError as $, type AttemptStatus as A, type SettlementEventHook as B, type CheckoutCreatedHook as C, type SettlementRecord as D, type SqlClient as E, type SqlDatabase as F, type SqlPaymentRepository as G, type Host as H, type IpRateLimitConfig as I, type SqlPaymentsOptions as J, type SqlQuery as K, type Stack as L, type StackStorage as M, type NotificationListener as N, OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS as O, type PaymentInsert as P, type StackWallet as Q, type ReconcilableAttempt as R, type SqlAdapter as S, createHost as T, createHttpHandler as U, createIpRateLimit as V, createProxyRateLimitingConfig as W, createRequestId as X, createSqlPayments as Y, createStack as Z, errorResponse as _, type Authorize as a, isServiceErrorShape as a0, isStackOptions as a1, jsonResponse as a2, mapHostRouteError as a3, paymentsSchemaSql as a4, resolveClientIp as a5, startNotificationListener as a6, startNotificationWorker as a7, 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 CreateHttpHandlerOptions as i, type CreateStackOptions as j, HostError as k, HttpError as l, type HttpHandler as m, type NotificationWorker as n, OPENRECEIVE_RECONCILE_BATCH_SIZE as o, type PaymentRecord as p, type PaymentRepository as q, type PaymentSettlement as r, type PaymentSettlementHook as s, type RateLimit as t, type ReconciliationTransition as u, type ResolveCheckoutContext as v, type ResolveCheckoutHook as w, type ResolvedHostCheckout as x, type ServiceErrorShape as y, type SettlementEvent as z };