@openreceive/http 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,8 +38,10 @@ write-once settlement with first-attempt-only fulfillment, and the
38
38
  machine (`attention` reads as `pending` on the wire; operators see it only in
39
39
  `openreceive_payments.status`). `onPaid` is the settlement hook in both modes: with `db` it receives
40
40
  `PaymentSettlement` (`reference` plus a `query` that runs inside the
41
- settlement transaction); with a custom repository it receives the raw
42
- `SettlementEvent` (`paymentHash`, `paidAt`, `details`). Settlement
41
+ settlement transaction); with a custom repository it receives
42
+ `SettlementEvent<Transaction>` (`reference`, `paymentHash`, `paidAt`, `details`,
43
+ and `transaction`). The repository awaits fulfillment before commit; failure
44
+ rolls back both payment and host writes. Settlement
43
45
  piggybacks on mounted routes by default through the durable `openreceive_meta`
44
46
  gate (`opportunisticReconcile: false` disables, `{ minIntervalSeconds }`
45
47
  tunes); `startNotificationWorker` is the optional
@@ -1,5 +1,5 @@
1
- import { Checkout, SwapData, NodeSettlementActionHook, CreateCheckoutAmount, NodeSettlementActionInput, OpenReceive } from '@openreceive/node';
2
- import { ErrorBody, ErrorCode, TransactionSettlementStatus, PaymentDetails } from '@openreceive/core';
1
+ import { Checkout, SwapData, NodeSettlementActionHook, CreateCheckoutAmount, OpenReceive } from '@openreceive/node';
2
+ import { ErrorBody, ErrorCode, TransactionSettlementStatus, PaymentDetails, PaymentScanWindow } from '@openreceive/core';
3
3
 
4
4
  type AuthorizeAction = "checkout.prepare" | "checkout.create" | "payment.check" | "swap.quote" | "swap.create" | "swap.read" | "swap.refund";
5
5
  /**
@@ -152,7 +152,9 @@ interface ReconcilableAttempt {
152
152
  readonly paymentHash: string;
153
153
  /** Exact NIP-47 invoice creation time returned by make_invoice. */
154
154
  readonly createdAt: number;
155
- /** Unix timestamp after which the attempt can be closed once a scan confirms no settlement. */
155
+ readonly createdAtSource?: "wallet" | "host";
156
+ /** Actual wallet invoice deadline from the saved checkout, never the deposit deadline.
157
+ * Closure also requires a covering scan after this deadline plus the expiry grace. */
156
158
  readonly expiresAt: number;
157
159
  }
158
160
  /** A terminal (non-settled) state transition observed by reconciliation. */
@@ -185,19 +187,38 @@ interface SettlementRecord {
185
187
  * `recordReconciliation` must apply the transition only while the row is still
186
188
  * `pending` — it must never overwrite a settled attempt.
187
189
  *
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.
190
+ * `recordSettlementWithFulfillment` locks the reference, records settlement,
191
+ * and awaits host fulfillment in one transaction. Failure rolls back both writes.
191
192
  */
192
- interface PaymentRepository {
193
+ interface SettlementContext<Transaction = unknown> extends SettlementRecord {
194
+ readonly reference: string;
195
+ /** Handle owned by the settlement transaction; never a root connection. */
196
+ readonly transaction: Transaction;
197
+ }
198
+ interface ReconcileCursor {
199
+ readonly created_at: number;
200
+ readonly payment_hash: string;
201
+ }
202
+ interface ReconcileScheduler {
203
+ cursor: ReconcileCursor | null;
204
+ windows: PaymentScanWindow[];
205
+ }
206
+ interface ReconcileGateClaim {
207
+ readonly token: string;
208
+ readonly scheduler: ReconcileScheduler;
209
+ }
210
+ interface PaymentRepository<Transaction = unknown> {
193
211
  listForReference(reference: string): Promise<readonly PaymentRecord[]>;
212
+ /** Durable acknowledgment for settlement outcomes, including concurrent writes. */
213
+ findByPaymentHash(paymentHash: string): Promise<PaymentRecord | undefined>;
194
214
  /**
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.
215
+ * A bounded keyset page of `pending` attempts ordered by createdAt and hash,
216
+ * strictly after the supplied cursor; terminal rows are excluded. Return at
217
+ * OPENRECEIVE_RECONCILE_BATCH_SIZE (200) when that many remain, otherwise
218
+ * all remaining rows. A short page signals exhaustion; the durable scheduler
219
+ * wraps immediately so new arrivals cannot postpone old fulfillment retries.
199
220
  */
200
- listReconcilableAttempts(): Promise<readonly ReconcilableAttempt[]>;
221
+ listReconcilableAttempts(after?: ReconcileCursor | null): Promise<readonly ReconcilableAttempt[]>;
201
222
  /**
202
223
  * The `pending` attempt for one payment hash, or undefined when the hash is
203
224
  * unknown or already terminal. Direct settlement from an NWC notification
@@ -210,14 +231,15 @@ interface PaymentRepository {
210
231
  commitAttempt(input: CheckoutCreatedInput): void | Promise<void>;
211
232
  recordReconciliation(transition: ReconciliationTransition): void | Promise<void>;
212
233
  /**
213
- * Claim the order's first settlement for this attempt and persist it.
234
+ * Record this pending attempt's settlement under the reference lock, await
235
+ * fulfillment for the first settled sibling, then commit both writes.
214
236
  * Returns true only for the call that won the claim — the attempt was still
215
237
  * unsettled AND no sibling attempt on the order had settled. Later calls for
216
238
  * the same or a sibling attempt must record the settlement (a genuine second
217
239
  * payment is not discarded) and return false. A settled attempt is never
218
240
  * overwritten, and an unknown payment hash is a no-op returning false.
219
241
  */
220
- recordSettlement(settlement: SettlementRecord): boolean | Promise<boolean>;
242
+ recordSettlementWithFulfillment(settlement: SettlementRecord, fulfill: (context: SettlementContext<Transaction>) => void | Promise<void>): boolean | Promise<boolean>;
221
243
  /**
222
244
  * Count attempt rows recorded for this client IP at or after `sinceUnixSeconds`.
223
245
  * Backs the handler's opt-in `rateLimiting` option; when a custom repository
@@ -226,9 +248,9 @@ interface PaymentRepository {
226
248
  */
227
249
  countAttemptsFromIp?(clientIp: string, sinceUnixSeconds: number): number | Promise<number>;
228
250
  /**
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
251
+ * Claim the durable global reconcile gate: return a token and scheduler when
252
+ * this caller owns the lease, or null while another lease/interval is active
253
+ * (`gate_busy`). Persist progress only through the matching unexpired token. The claim MUST be a durable compare-and-set
232
254
  * shared by every process on the host database (the built-in SQL repository
233
255
  * uses the `openreceive_meta` key/value/rev table) — process-local memory
234
256
  * cannot coordinate multiple workers and must never back this. Backs the
@@ -239,6 +261,14 @@ interface PaymentRepository {
239
261
  claimReconcileGate?(input: {
240
262
  readonly now: number;
241
263
  readonly intervalSeconds: number;
264
+ readonly leaseSeconds?: number;
265
+ }): ReconcileGateClaim | null | Promise<ReconcileGateClaim | null>;
266
+ checkpointReconcileGate?(input: {
267
+ readonly claim: ReconcileGateClaim;
268
+ readonly scheduler: ReconcileScheduler;
269
+ readonly now: number;
270
+ readonly release?: boolean;
271
+ readonly intervalSeconds?: number;
242
272
  }): boolean | Promise<boolean>;
243
273
  }
244
274
 
@@ -328,7 +358,33 @@ interface PaymentSettlement {
328
358
  readonly query: SqlQuery;
329
359
  }
330
360
  type PaymentSettlementHook = (settlement: PaymentSettlement) => void | Promise<void>;
331
- interface SqlPaymentRepository extends PaymentRepository {
361
+ /** Nonsecret dry-run report for explicit host/operator review. */
362
+ interface PaymentRepairCandidate {
363
+ readonly paymentHash: string;
364
+ readonly reference: string;
365
+ readonly status: "expired" | "attention";
366
+ readonly statusReason: string | null;
367
+ readonly updatedAt: number;
368
+ readonly instructionExpiresAt: number;
369
+ readonly settlementExpiresAt: number;
370
+ readonly category: "early_swap_closure" | "attention";
371
+ }
372
+ interface SqlPaymentRepository extends PaymentRepository<SqlClient> {
373
+ /** Read-only, hash-keyset candidate report; normal routes never call this. */
374
+ listRepairCandidates(input?: {
375
+ after?: string;
376
+ limit?: number;
377
+ }): Promise<{
378
+ candidates: readonly PaymentRepairCandidate[];
379
+ nextCursor: string | null;
380
+ }>;
381
+ /** Requeue one reviewed row only if its reported status and version still match. */
382
+ requeueAttempt(input: {
383
+ paymentHash: string;
384
+ expectedStatus: "expired" | "attention";
385
+ expectedUpdatedAt: number;
386
+ reason: string;
387
+ }): Promise<boolean>;
332
388
  /**
333
389
  * Replay-safe settlement transaction: set the attempt's `paid_at`/`settled`
334
390
  * status once, and run `fulfill` inside the same transaction only for the
@@ -378,35 +434,18 @@ interface CreateHostDbOptions extends CreateOpenReceiveHostBaseOptions {
378
434
  readonly onPaid: PaymentSettlementHook;
379
435
  readonly payments?: never;
380
436
  }
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;
437
+ /** Custom fulfillment receives the resolved reference and its transaction handle. */
438
+ type SettlementEvent<Transaction = unknown> = SettlementContext<Transaction>;
439
+ type SettlementEventHook<Transaction = unknown> = (settlement: SettlementEvent<Transaction>) => void | Promise<void>;
440
+ /** The custom repository awaits onPaid before committing; any failure rolls back both writes. */
441
+ interface CreateHostRepositoryOptions<Transaction = unknown> extends CreateOpenReceiveHostBaseOptions {
442
+ readonly payments: PaymentRepository<Transaction>;
443
+ /** Awaited before the winning first-reference settlement transaction commits. */
444
+ readonly onPaid: SettlementEventHook<Transaction>;
406
445
  readonly db?: never;
407
446
  readonly tableName?: never;
408
447
  }
409
- type CreateHostOptions = CreateHostDbOptions | CreateHostRepositoryOptions;
448
+ type CreateHostOptions<Transaction = unknown> = CreateHostDbOptions | CreateHostRepositoryOptions<Transaction>;
410
449
  interface Host {
411
450
  readonly resolveCheckout: ResolveCheckoutHook;
412
451
  readonly onCheckoutCreated: CheckoutCreatedHook;
@@ -420,7 +459,7 @@ interface Host {
420
459
  * settlement write-once, and reconciliation transitions are library-owned in
421
460
  * `db` mode.
422
461
  */
423
- declare function createHost(options: CreateHostOptions): Host;
462
+ declare function createHost<Transaction = unknown>(options: CreateHostOptions<Transaction>): Host;
424
463
  /**
425
464
  * What the payer is buying, in the host's own words — one optional display
426
465
  * string returned beside the price by `amountFor`. It is peeled off here so the
@@ -528,8 +567,10 @@ interface ResolvedHostCheckout {
528
567
  * What the payer is buying, in the host's own words — one display string,
529
568
  * echoed on the prepare and create responses so the shipped checkout can show
530
569
  * something other than a QR and a number. OpenReceive owns no line items and
531
- * never will; this is the whole of what it carries. Response only: it is
532
- * never read from a request body.
570
+ * never will; this is the whole of what it carries. Never read from a request
571
+ * body: the payer does not write the copy next to the amount. On create it is
572
+ * also the default invoice memo, so the payer's wallet shows the same words
573
+ * the checkout did.
533
574
  */
534
575
  readonly description?: string;
535
576
  /** Return the selected host payment attempt's hash to reuse or inspect its checkout. */
@@ -697,14 +738,14 @@ type StackWallet = {
697
738
  * Where attempts live, which decides what `onPaid` receives. With the host
698
739
  * database handle (`db`, the default mode) it is the per-reference
699
740
  * `PaymentSettlement`, with `reference` and the transactional `query`;
700
- * with a custom repository (`payments`, advanced) it is the raw
701
- * `SettlementEvent`. The branch carries the hook's type, so the
741
+ * with a custom repository (`payments`, advanced) it includes the resolved
742
+ * reference and the repository transaction handle. The branch carries the hook's type, so the
702
743
  * wrong signature is a type error rather than a runtime surprise.
703
744
  */
704
- type StackStorage = Pick<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> | Pick<CreateHostRepositoryOptions, "payments" | "onPaid" | "db" | "tableName">;
705
- interface CreateStackOptions extends Omit<CreateHttpHandlerOptions, "service" | "host">, Omit<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> {
745
+ type StackStorage<Transaction = unknown> = Pick<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> | Pick<CreateHostRepositoryOptions<Transaction>, "payments" | "onPaid" | "db" | "tableName">;
746
+ interface CreateStackOptions<Transaction = unknown> extends Omit<CreateHttpHandlerOptions, "service" | "host">, Omit<CreateHostDbOptions, "db" | "tableName" | "onPaid" | "payments"> {
706
747
  readonly wallet: StackWallet;
707
- readonly storage: StackStorage;
748
+ readonly storage: StackStorage<Transaction>;
708
749
  /**
709
750
  * Where the one boot-failure line goes. Boot happens before any service
710
751
  * exists, so this is the only sink @openreceive/http can offer; it defaults
@@ -725,7 +766,7 @@ interface Stack {
725
766
  /** Closes the service if the stack created it. */
726
767
  close(): Promise<void>;
727
768
  }
728
- declare function createStack(options: CreateStackOptions): Stack;
769
+ declare function createStack<Transaction = unknown>(options: CreateStackOptions<Transaction>): Stack;
729
770
  /**
730
771
  * True when adapter options are the flat all-in-one form (no prebuilt `host`).
731
772
  * The composed form always carries `host`; the flat form carries the host
@@ -735,4 +776,4 @@ declare function createStack(options: CreateStackOptions): Stack;
735
776
  */
736
777
  declare function isStackOptions(options: CreateHttpHandlerOptions | CreateStackOptions): options is CreateStackOptions;
737
778
 
738
- 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_RECONCILE_BATCH_SIZE 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, type HostCheckoutPrice as k, HostError as l, HttpError as m, type HttpHandler as n, type NotificationWorker 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 };
779
+ export { createProxyRateLimitingConfig as $, type AttemptStatus as A, type ResolveCheckoutHook as B, type CheckoutCreatedHook as C, type ResolvedHostCheckout as D, type ServiceErrorShape as E, type SettlementContext as F, type SettlementEvent as G, type Host as H, type IpRateLimitConfig as I, type SettlementEventHook as J, type SettlementRecord as K, type SqlClient as L, type SqlDatabase as M, type NotificationListener as N, OPENRECEIVE_RECONCILE_BATCH_SIZE 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, createHost as Y, createHttpHandler as Z, createIpRateLimit as _, type Authorize as a, createRequestId as a0, createSqlPayments as a1, createStack as a2, errorResponse as a3, hostError as a4, isServiceErrorShape as a5, isStackOptions as a6, jsonResponse as a7, mapHostRouteError as a8, paymentsSchemaSql as a9, resolveClientIp as aa, startNotificationListener as ab, startNotificationWorker as ac, 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, type HostCheckoutPrice as k, HostError as l, HttpError as m, type HttpHandler as n, type NotificationWorker as o, type PaymentRecord as p, type PaymentRepairCandidate as q, type PaymentRepository as r, type PaymentSettlement as s, type PaymentSettlementHook as t, type RateLimit as u, type ReconcileCursor as v, type ReconcileGateClaim as w, type ReconcileScheduler as x, type ReconciliationTransition as y, type ResolveCheckoutContext as z };
@@ -1,3 +1,3 @@
1
1
  export { Checkout, CreateCheckoutAmount, OpenReceive, PaymentCheck, SwapCheckout } from '@openreceive/node';
2
- export { a as Authorize, b as AuthorizeAction, c as AuthorizeContext, d as AuthorizeResource, C as CheckoutCreatedHook, e as CheckoutCreatedInput, i as CreateHttpHandlerOptions, j as CreateStackOptions, H as Host, l as HostError, m as HttpError, n as HttpHandler, I as IpRateLimitConfig, o as NotificationWorker, q as PaymentRepository, r as PaymentSettlement, s as PaymentSettlementHook, t as RateLimit, v as ResolveCheckoutContext, w as ResolveCheckoutHook, x as ResolvedHostCheckout, y as ServiceErrorShape, z as SettlementEvent, B as SettlementEventHook, L as Stack, M as StackStorage, Q as StackWallet, U as createHttpHandler, Z as createStack, $ as hostError, a0 as isServiceErrorShape, a3 as mapHostRouteError, a7 as startNotificationWorker } from './adapter-surface-DHngkTKQ.js';
2
+ export { a as Authorize, b as AuthorizeAction, c as AuthorizeContext, d as AuthorizeResource, C as CheckoutCreatedHook, e as CheckoutCreatedInput, i as CreateHttpHandlerOptions, j as CreateStackOptions, H as Host, l as HostError, m as HttpError, n as HttpHandler, I as IpRateLimitConfig, o as NotificationWorker, r as PaymentRepository, s as PaymentSettlement, t as PaymentSettlementHook, u as RateLimit, z as ResolveCheckoutContext, B as ResolveCheckoutHook, D as ResolvedHostCheckout, E as ServiceErrorShape, G as SettlementEvent, J as SettlementEventHook, V as Stack, W as StackStorage, X as StackWallet, Z as createHttpHandler, a2 as createStack, a4 as hostError, a5 as isServiceErrorShape, a8 as mapHostRouteError, ac as startNotificationWorker } from './adapter-surface-Cs4g29Ez.js';
3
3
  import '@openreceive/core';
@@ -7,7 +7,7 @@ import {
7
7
  isServiceErrorShape,
8
8
  mapHostRouteError,
9
9
  startNotificationWorker
10
- } from "./chunk-TJY2HBVU.js";
10
+ } from "./chunk-PGZAFYN6.js";
11
11
  export {
12
12
  HostError,
13
13
  HttpError,