@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,2228 @@
1
+ // src/errors.ts
2
+ import {
3
+ isErrorCode,
4
+ isRetryableErrorCode
5
+ } from "@openreceive/core";
6
+ var HttpError = class extends Error {
7
+ status;
8
+ code;
9
+ retryable;
10
+ details;
11
+ /** Emitted as a `Retry-After` header so clients can back off with a hint. */
12
+ retryAfterSeconds;
13
+ constructor(status, code, message, options = {}) {
14
+ super(message);
15
+ this.name = "HttpError";
16
+ this.status = status;
17
+ this.code = code;
18
+ if (options.retryable !== void 0) this.retryable = options.retryable;
19
+ if (options.details !== void 0) this.details = options.details;
20
+ if (options.retryAfterSeconds !== void 0) this.retryAfterSeconds = options.retryAfterSeconds;
21
+ }
22
+ };
23
+ function isServiceErrorShape(error) {
24
+ if (typeof error !== "object" || error === null) return false;
25
+ const candidate = error;
26
+ if (typeof candidate.status !== "number") return false;
27
+ if (typeof candidate.body !== "object" || candidate.body === null) return false;
28
+ const body = candidate.body;
29
+ return isErrorCode(body.code) && typeof body.message === "string";
30
+ }
31
+ function isWalletErrorShape(error) {
32
+ if (typeof error !== "object" || error === null) return false;
33
+ const candidate = error;
34
+ if (candidate.status !== void 0) return false;
35
+ return isErrorCode(candidate.code) && typeof candidate.message === "string";
36
+ }
37
+ function walletErrorStatus(code, retryable) {
38
+ return retryable || isRetryableErrorCode(code) ? 503 : 502;
39
+ }
40
+ var HostError = class extends Error {
41
+ status;
42
+ body;
43
+ constructor(status, body) {
44
+ super(body.message);
45
+ this.name = "HostError";
46
+ this.status = status;
47
+ this.body = body;
48
+ }
49
+ };
50
+ function hostError(message, status = 400, code = "INVALID_REQUEST") {
51
+ return new HostError(status, {
52
+ code,
53
+ message,
54
+ ...isRetryableErrorCode(code) ? { retryable: false } : {}
55
+ });
56
+ }
57
+ function mapHostRouteError(error) {
58
+ if (error instanceof HostError || isServiceErrorShape(error)) {
59
+ return { status: error.status, body: error.body };
60
+ }
61
+ if (isWalletErrorShape(error)) {
62
+ const retryable = error.retryable ?? isRetryableErrorCode(error.code);
63
+ return {
64
+ status: walletErrorStatus(error.code, retryable),
65
+ body: { code: error.code, message: error.message, retryable }
66
+ };
67
+ }
68
+ return null;
69
+ }
70
+ function bigintToJsonNumber(value) {
71
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < -BigInt(Number.MAX_SAFE_INTEGER)) {
72
+ throw new HttpError(500, "INTERNAL", "Numeric value exceeds the JSON safe integer range.");
73
+ }
74
+ return Number(value);
75
+ }
76
+ function createRequestId() {
77
+ return `req_${globalThis.crypto.randomUUID()}`;
78
+ }
79
+ function jsonResponse(status, body, requestId, extraHeaders) {
80
+ const headers = new Headers({
81
+ "content-type": "application/json; charset=utf-8",
82
+ "x-request-id": requestId
83
+ });
84
+ if (extraHeaders !== void 0) {
85
+ for (const [name, value] of extraHeaders) {
86
+ headers.append(name, value);
87
+ }
88
+ }
89
+ return new Response(
90
+ JSON.stringify(
91
+ body,
92
+ (_key, value) => typeof value === "bigint" ? bigintToJsonNumber(value) : value
93
+ ),
94
+ { status, headers }
95
+ );
96
+ }
97
+ function errorResponse(error, requestId) {
98
+ if (error instanceof HttpError) {
99
+ return jsonResponse(
100
+ error.status,
101
+ {
102
+ code: error.code,
103
+ message: error.message,
104
+ ...error.retryable === void 0 ? {} : { retryable: error.retryable },
105
+ request_id: requestId,
106
+ ...error.details === void 0 ? {} : { details: error.details }
107
+ },
108
+ requestId,
109
+ error.retryAfterSeconds === void 0 ? void 0 : [["retry-after", String(Math.max(1, Math.ceil(error.retryAfterSeconds)))]]
110
+ );
111
+ }
112
+ if (isServiceErrorShape(error)) {
113
+ return jsonResponse(
114
+ error.status,
115
+ { ...error.body, request_id: error.body.request_id ?? requestId },
116
+ requestId
117
+ );
118
+ }
119
+ if (isWalletErrorShape(error)) {
120
+ const retryable = error.retryable ?? isRetryableErrorCode(error.code);
121
+ return jsonResponse(
122
+ walletErrorStatus(error.code, retryable),
123
+ { code: error.code, message: error.message, retryable, request_id: requestId },
124
+ requestId
125
+ );
126
+ }
127
+ return jsonResponse(
128
+ 500,
129
+ { code: "INTERNAL", message: "Internal server error.", request_id: requestId },
130
+ requestId
131
+ );
132
+ }
133
+
134
+ // src/router.ts
135
+ function normalizePrefix(prefix) {
136
+ const trimmed = prefix.trim();
137
+ const value = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
138
+ const withoutTrailing = value.length > 1 && value.endsWith("/") ? value.slice(0, -1) : value;
139
+ return withoutTrailing === "/" ? "" : withoutTrailing;
140
+ }
141
+ var ROUTES = {
142
+ "POST /checkouts/prepare": { kind: "checkout.prepare" },
143
+ "POST /checkouts": { kind: "checkout.create" },
144
+ "POST /payments/check": { kind: "payment.check" },
145
+ "POST /swaps/quote": { kind: "swap.quote" },
146
+ "POST /swaps": { kind: "swap.create" },
147
+ "POST /swaps/status": { kind: "swap.read" },
148
+ "POST /swaps/refunds": { kind: "swap.refund" },
149
+ "GET /rates": { kind: "rates" }
150
+ };
151
+ var KNOWN_PATHS = new Set(Object.keys(ROUTES).map((key) => key.split(" ")[1]));
152
+ function routePath(prefix, pathname) {
153
+ const path = pathname === prefix ? "/" : pathname.slice(prefix.length);
154
+ return path.replace(/\/$/, "") || "/";
155
+ }
156
+ function claimsPath(prefix, pathname) {
157
+ if (prefix === "") return KNOWN_PATHS.has(routePath(prefix, pathname));
158
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
159
+ }
160
+ function matchRoute(prefix, method, pathname) {
161
+ if (!claimsPath(prefix, pathname)) return null;
162
+ const normalizedPath = routePath(prefix, pathname);
163
+ const route = ROUTES[`${method.toUpperCase()} ${normalizedPath}`];
164
+ if (route === void 0) {
165
+ if (KNOWN_PATHS.has(normalizedPath)) {
166
+ throw new HttpError(
167
+ 405,
168
+ "INVALID_REQUEST",
169
+ "This OpenReceive route does not support that HTTP method."
170
+ );
171
+ }
172
+ throw new HttpError(404, "NOT_FOUND", "No OpenReceive route matched this method and path.");
173
+ }
174
+ return route;
175
+ }
176
+
177
+ // src/adapter-bridge.ts
178
+ var SKIP_REQUEST_HEADERS = /* @__PURE__ */ new Set(["content-length", "transfer-encoding", "connection"]);
179
+ function webRequest(parts) {
180
+ const rawHost = parts.headers.host;
181
+ const host = (Array.isArray(rawHost) ? rawHost[0] : rawHost) ?? "localhost";
182
+ const protocol = parts.protocol !== void 0 && parts.protocol.length > 0 ? parts.protocol : "http";
183
+ const url = `${protocol}://${host}${parts.url.length > 0 ? parts.url : "/"}`;
184
+ const headers = new Headers();
185
+ for (const [key, value] of Object.entries(parts.headers)) {
186
+ if (SKIP_REQUEST_HEADERS.has(key.toLowerCase())) continue;
187
+ if (typeof value === "string") headers.set(key, value);
188
+ else if (Array.isArray(value)) headers.set(key, value.join(", "));
189
+ }
190
+ const method = parts.method.toUpperCase();
191
+ let body;
192
+ if (method !== "GET" && method !== "HEAD") {
193
+ const parsed = parts.parsedBody;
194
+ assertBodyWasParsed(parts, parsed);
195
+ if (typeof parsed === "string") {
196
+ body = parsed;
197
+ } else if (parsed !== void 0 && parsed !== null && Object.keys(parsed).length > 0) {
198
+ body = JSON.stringify(parsed);
199
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
200
+ }
201
+ }
202
+ return new Request(url, { method, headers, body });
203
+ }
204
+ function assertBodyWasParsed(parts, parsed) {
205
+ if (parsed !== void 0) return;
206
+ const header = (name) => {
207
+ const value = parts.headers[name];
208
+ return Array.isArray(value) ? value[0] : value;
209
+ };
210
+ const declaresBody = Number(header("content-length") ?? "0") > 0 || header("transfer-encoding") !== void 0;
211
+ if (!declaresBody || !(header("content-type") ?? "").includes("json")) return;
212
+ throw new HttpError(
213
+ 500,
214
+ "INTERNAL",
215
+ "OpenReceive received a JSON request body that no body parser had read. Mount your framework's JSON body parser (Express: app.use(express.json())) before the OpenReceive middleware."
216
+ );
217
+ }
218
+ function isUnderPrefix(pathname, prefix) {
219
+ return claimsPath(prefix, pathname);
220
+ }
221
+
222
+ // src/handler.ts
223
+ import { compact as compact2, unixSeconds as unixSeconds7 } from "@openreceive/core";
224
+
225
+ // src/reconcile-gate.ts
226
+ import { compact, unixSeconds as unixSeconds5 } from "@openreceive/core";
227
+
228
+ // src/host-payments.ts
229
+ import { unixSeconds as unixSeconds3 } from "@openreceive/core";
230
+ import { sanitizeEvent } from "@openreceive/node";
231
+
232
+ // src/payment-repository.ts
233
+ import {
234
+ unixSeconds
235
+ } from "@openreceive/core";
236
+ var OPENRECEIVE_ATTEMPT_REUSE_BUFFER_SECONDS = 60;
237
+ var OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS = 900;
238
+ function isReusablePaymentAttempt(expiresAt, now = unixSeconds()) {
239
+ return expiresAt - now > OPENRECEIVE_ATTEMPT_REUSE_BUFFER_SECONDS;
240
+ }
241
+ function liveAttemptCommitDecision(live, incoming, now = unixSeconds()) {
242
+ if (!sameRailAndAsset(live, incoming)) return "ignore";
243
+ return isReusablePaymentAttempt(live.expiresAt, now) ? "conflict" : "supersede";
244
+ }
245
+ function sameRailAndAsset(left, right) {
246
+ const leftSwap = left.swapData ?? null;
247
+ const rightSwap = right.swapData ?? null;
248
+ if (leftSwap === null && rightSwap === null) return true;
249
+ if (leftSwap === null || rightSwap === null) return false;
250
+ return leftSwap.providerOrder.pay_in_asset === rightSwap.providerOrder.pay_in_asset;
251
+ }
252
+ function reconciliationTransition(attempt, status, observedAt, transactionState) {
253
+ const paymentHash = attempt.paymentHash.toLowerCase();
254
+ if (status === "failed") {
255
+ return { paymentHash, status: "failed", observedAt, reason: "wallet_reported_failed" };
256
+ }
257
+ if (status === "expired") {
258
+ return { paymentHash, status: "expired", observedAt, reason: "wallet_reported_expired" };
259
+ }
260
+ if (observedAt < attempt.expiresAt + OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS) return null;
261
+ if (status === "not_found") {
262
+ return { paymentHash, status: "expired", observedAt, reason: "not_found_after_expiry" };
263
+ }
264
+ if (transactionState === "pending" || transactionState === "accepted") {
265
+ return { paymentHash, status: "attention", observedAt, reason: "unsettled_after_expiry" };
266
+ }
267
+ return { paymentHash, status: "expired", observedAt, reason: "no_finality_after_expiry" };
268
+ }
269
+ function paymentInsert(input) {
270
+ return {
271
+ reference: input.reference,
272
+ paymentHash: input.paymentHash.toLowerCase(),
273
+ createdAt: input.checkout.createdAt,
274
+ checkout: structuredClone(input.checkout),
275
+ expiresAt: input.swapData?.providerOrder.expires_at ?? input.checkout.expiresAt,
276
+ ...input.swapData === void 0 ? {} : { swapData: input.swapData },
277
+ ...input.clientIp === void 0 ? {} : { clientIp: input.clientIp }
278
+ };
279
+ }
280
+
281
+ // src/sql-payments.ts
282
+ import { randomUUID } from "crypto";
283
+ import {
284
+ OPENRECEIVE_PAYMENTS_SCHEMA_VERSION,
285
+ paymentsDdlStatements,
286
+ unixSeconds as unixSeconds2
287
+ } from "@openreceive/core";
288
+
289
+ // src/sql-adapters.ts
290
+ var SQLITE_BUSY_TIMEOUT_MS = 5e3;
291
+ function resolveSqlAdapter(db) {
292
+ if (isSqlAdapter(db)) return db;
293
+ if (isSqliteLike(db)) return sqliteAdapter(db);
294
+ if (isPgLike(db)) return pgAdapter(db);
295
+ throw new TypeError(
296
+ "Unsupported database handle. Pass a pg Pool/Client, a SQLite database (node:sqlite or better-sqlite3), or an SqlAdapter."
297
+ );
298
+ }
299
+ function isSqlAdapter(db) {
300
+ const candidate = db;
301
+ return (candidate.dialect === "postgres" || candidate.dialect === "sqlite") && typeof candidate.transaction === "function" && typeof candidate.query === "function";
302
+ }
303
+ function isSqliteLike(db) {
304
+ const candidate = db;
305
+ return typeof candidate.prepare === "function" && typeof candidate.exec === "function";
306
+ }
307
+ function isPgLike(db) {
308
+ return typeof db.query === "function";
309
+ }
310
+ function createSerialQueue() {
311
+ let tail = Promise.resolve();
312
+ return (task) => {
313
+ const result = tail.then(task, task);
314
+ tail = result.then(
315
+ () => void 0,
316
+ () => void 0
317
+ );
318
+ return result;
319
+ };
320
+ }
321
+ function sqliteAdapter(db) {
322
+ try {
323
+ db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
324
+ } catch {
325
+ }
326
+ const query = async (sql, params = []) => {
327
+ const statement = db.prepare(sql);
328
+ if (sqliteStatementReturnsRows(sql, statement)) {
329
+ return statement.all(...params);
330
+ }
331
+ statement.run(...params);
332
+ return [];
333
+ };
334
+ const enqueue = createSerialQueue();
335
+ return {
336
+ dialect: "sqlite",
337
+ query: (sql, params) => enqueue(() => query(sql, params)),
338
+ transaction(run) {
339
+ return enqueue(async () => {
340
+ db.exec("BEGIN IMMEDIATE");
341
+ try {
342
+ const result = await run({ query });
343
+ db.exec("COMMIT");
344
+ return result;
345
+ } catch (error) {
346
+ try {
347
+ db.exec("ROLLBACK");
348
+ } catch {
349
+ }
350
+ throw error;
351
+ }
352
+ });
353
+ }
354
+ };
355
+ }
356
+ function sqliteStatementReturnsRows(sql, statement) {
357
+ const reader = statement.reader;
358
+ if (typeof reader === "boolean") return reader;
359
+ return /^\s*(?:select|values|with)\b/i.test(sql) || /\breturning\b/i.test(sql);
360
+ }
361
+ function isPooledClient(value) {
362
+ return typeof value === "object" && value !== null && typeof value.query === "function" && typeof value.release === "function";
363
+ }
364
+ function pgAdapter(db) {
365
+ const queryOn = (client) => async (sql, params = []) => {
366
+ const result = await client.query(sql, [...params]);
367
+ return result.rows;
368
+ };
369
+ const directQuery = queryOn(db);
370
+ const enqueue = createSerialQueue();
371
+ let mode = db.connect === void 0 ? "single" : typeof db.totalCount === "number" ? "pool" : void 0;
372
+ let probe;
373
+ const probeConnect = async () => {
374
+ try {
375
+ const client = await db.connect?.();
376
+ if (isPooledClient(client)) {
377
+ mode = "pool";
378
+ return client;
379
+ }
380
+ mode = "single";
381
+ return void 0;
382
+ } catch (error) {
383
+ if (error instanceof Error && /already been connected/i.test(error.message)) {
384
+ mode = "single";
385
+ return void 0;
386
+ }
387
+ throw error;
388
+ }
389
+ };
390
+ const checkoutPooledClient = async () => {
391
+ while (mode === void 0) {
392
+ if (probe === void 0) {
393
+ probe = probeConnect();
394
+ try {
395
+ return await probe;
396
+ } finally {
397
+ if (mode === void 0) probe = void 0;
398
+ }
399
+ }
400
+ const pending = probe;
401
+ await pending.then(
402
+ () => void 0,
403
+ () => void 0
404
+ );
405
+ if (probe === pending && mode === void 0) probe = void 0;
406
+ }
407
+ if (mode === "single") return void 0;
408
+ const client = await db.connect?.();
409
+ return isPooledClient(client) ? client : void 0;
410
+ };
411
+ const ensureMode = async () => {
412
+ if (mode !== void 0) return;
413
+ const client = await checkoutPooledClient();
414
+ client?.release();
415
+ };
416
+ const runTransaction = async (query, run) => {
417
+ await query("BEGIN");
418
+ try {
419
+ const result = await run({ query });
420
+ await query("COMMIT");
421
+ return result;
422
+ } catch (error) {
423
+ try {
424
+ await query("ROLLBACK");
425
+ } catch {
426
+ }
427
+ throw error;
428
+ }
429
+ };
430
+ return {
431
+ dialect: "postgres",
432
+ // Pooled reads are isolated per checked-out connection; single-connection
433
+ // reads join the transaction queue so they never see uncommitted state.
434
+ query: async (sql, params) => {
435
+ await ensureMode();
436
+ return mode === "pool" ? directQuery(sql, params) : enqueue(() => directQuery(sql, params));
437
+ },
438
+ async transaction(run) {
439
+ const client = await checkoutPooledClient();
440
+ if (client !== void 0) {
441
+ try {
442
+ return await runTransaction(queryOn(client), run);
443
+ } finally {
444
+ client.release();
445
+ }
446
+ }
447
+ return enqueue(() => runTransaction(directQuery, run));
448
+ }
449
+ };
450
+ }
451
+ function toPgPlaceholders(sql) {
452
+ let index = 0;
453
+ return sql.replaceAll("?", () => {
454
+ index += 1;
455
+ return `$${index}`;
456
+ });
457
+ }
458
+
459
+ // src/sql-payments.ts
460
+ import { OPENRECEIVE_PAYMENTS_SCHEMA_VERSION as OPENRECEIVE_PAYMENTS_SCHEMA_VERSION2 } from "@openreceive/core";
461
+ var ADVISORY_LOCK_SEED = 8210223;
462
+ var RECONCILE_GATE_META_KEY = "transaction_scan_gate";
463
+ var RECONCILE_GATE_CAS_RETRIES = 6;
464
+ var META_CLOCK_SKEW_SECONDS = 60;
465
+ var SCHEMA_VERSION_META_KEY = "schema_version";
466
+ var OPENRECEIVE_RECONCILE_BATCH_SIZE = 200;
467
+ function parseClaimedAt(value) {
468
+ try {
469
+ const parsed = JSON.parse(String(value));
470
+ return typeof parsed.claimed_at === "number" && Number.isFinite(parsed.claimed_at) ? parsed.claimed_at : void 0;
471
+ } catch {
472
+ return void 0;
473
+ }
474
+ }
475
+ function paymentsSchemaSql(dialect, tableName = "openreceive_payments", metaTableName = "openreceive_meta") {
476
+ return paymentsDdlStatements({ dialect, tableName, metaTableName }).map((statement) => `${statement};`).join("\n");
477
+ }
478
+ function createSqlPayments(db, options = {}) {
479
+ const adapter = resolveSqlAdapter(db);
480
+ const table = options.tableName ?? "openreceive_payments";
481
+ assertSafeIdentifier(table);
482
+ const metaTable = options.metaTableName ?? "openreceive_meta";
483
+ assertSafeIdentifier(metaTable);
484
+ const clock = options.clock ?? unixSeconds2;
485
+ const statement = (sql) => adapter.dialect === "postgres" ? toPgPlaceholders(sql) : sql;
486
+ const lockReference = async (tx, reference) => {
487
+ if (adapter.dialect === "postgres") {
488
+ await tx.query(statement("SELECT pg_advisory_xact_lock(hashtextextended(?, ?))"), [
489
+ reference,
490
+ ADVISORY_LOCK_SEED
491
+ ]);
492
+ }
493
+ };
494
+ const rowsForReference = async (tx, reference) => {
495
+ const rows = await tx.query(
496
+ statement(
497
+ `SELECT * FROM ${table} WHERE reference = ? ORDER BY created_at DESC, payment_hash DESC`
498
+ ),
499
+ [reference]
500
+ );
501
+ return rows.map(recordFromRow);
502
+ };
503
+ let schemaVersionChecked;
504
+ const assertSupportedSchema = () => {
505
+ schemaVersionChecked ??= (async () => {
506
+ let stored;
507
+ try {
508
+ const rows = await adapter.query(
509
+ statement(`SELECT value FROM ${metaTable} WHERE key = ? LIMIT 1`),
510
+ [SCHEMA_VERSION_META_KEY]
511
+ );
512
+ const value = rows[0]?.value;
513
+ stored = value === void 0 ? void 0 : Number(asString(value, "value"));
514
+ } catch {
515
+ return;
516
+ }
517
+ if (stored === void 0 || !Number.isInteger(stored)) return;
518
+ if (stored > OPENRECEIVE_PAYMENTS_SCHEMA_VERSION) {
519
+ throw new TypeError(
520
+ `${metaTable} reports openreceive schema version ${stored}, newer than this library's ${OPENRECEIVE_PAYMENTS_SCHEMA_VERSION}. Upgrade @openreceive/http before serving this database.`
521
+ );
522
+ }
523
+ })();
524
+ return schemaVersionChecked;
525
+ };
526
+ return {
527
+ async listForReference(reference) {
528
+ await assertSupportedSchema();
529
+ return rowsForReference(adapter, reference);
530
+ },
531
+ async listReconcilableAttempts() {
532
+ await assertSupportedSchema();
533
+ const rows = await adapter.query(
534
+ statement(
535
+ `SELECT payment_hash, created_at, expires_at FROM ${table} WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?`
536
+ ),
537
+ [OPENRECEIVE_RECONCILE_BATCH_SIZE]
538
+ );
539
+ return rows.map(
540
+ (row) => ({
541
+ paymentHash: asString(row.payment_hash, "payment_hash"),
542
+ createdAt: asInteger(row.created_at, "created_at"),
543
+ expiresAt: asInteger(row.expires_at, "expires_at")
544
+ })
545
+ );
546
+ },
547
+ async findPendingAttempt(paymentHash) {
548
+ await assertSupportedSchema();
549
+ const rows = await adapter.query(
550
+ statement(
551
+ `SELECT payment_hash, created_at, expires_at FROM ${table} WHERE payment_hash = ? AND status = 'pending'`
552
+ ),
553
+ [paymentHash.toLowerCase()]
554
+ );
555
+ const row = rows[0];
556
+ if (row === void 0) return void 0;
557
+ return {
558
+ paymentHash: asString(row.payment_hash, "payment_hash"),
559
+ createdAt: asInteger(row.created_at, "created_at"),
560
+ expiresAt: asInteger(row.expires_at, "expires_at")
561
+ };
562
+ },
563
+ async claimReconcileGate({ now, intervalSeconds }) {
564
+ await assertSupportedSchema();
565
+ const token = randomUUID();
566
+ const claimValue = JSON.stringify({ claimed_at: now, token });
567
+ const insertIfAbsent = adapter.dialect === "postgres" ? `INSERT INTO ${metaTable} (key, value, rev) VALUES (?, ?, 0) ON CONFLICT (key) DO NOTHING` : `INSERT OR IGNORE INTO ${metaTable} (key, value, rev) VALUES (?, ?, 0)`;
568
+ for (let attempt = 0; attempt < RECONCILE_GATE_CAS_RETRIES; attempt += 1) {
569
+ const rows = await adapter.query(
570
+ statement(`SELECT value, rev FROM ${metaTable} WHERE key = ? LIMIT 1`),
571
+ [RECONCILE_GATE_META_KEY]
572
+ );
573
+ const current = rows[0];
574
+ if (current === void 0) {
575
+ await adapter.query(statement(insertIfAbsent), [RECONCILE_GATE_META_KEY, claimValue]);
576
+ } else {
577
+ const claimedAt = parseClaimedAt(current.value);
578
+ if (claimedAt !== void 0 && isFreshTimestamp(now, claimedAt, intervalSeconds)) {
579
+ return false;
580
+ }
581
+ await adapter.query(
582
+ statement(`UPDATE ${metaTable} SET value = ?, rev = rev + 1 WHERE key = ? AND rev = ?`),
583
+ [claimValue, RECONCILE_GATE_META_KEY, asInteger(current.rev, "rev")]
584
+ );
585
+ }
586
+ const readback = await adapter.query(
587
+ statement(`SELECT value FROM ${metaTable} WHERE key = ? LIMIT 1`),
588
+ [RECONCILE_GATE_META_KEY]
589
+ );
590
+ if (readback[0] !== void 0 && String(readback[0].value) === claimValue) return true;
591
+ }
592
+ return false;
593
+ },
594
+ async commitAttempt(input) {
595
+ await assertSupportedSchema();
596
+ const insert = paymentInsert(input);
597
+ const now = clock();
598
+ await adapter.transaction(async (tx) => {
599
+ await lockReference(tx, insert.reference);
600
+ const existing = await rowsForReference(tx, insert.reference);
601
+ if (existing.some((row) => row.paymentHash === insert.paymentHash)) return;
602
+ if (existing.some((row) => row.status === "settled")) {
603
+ throw hostError("This reference is already paid.", 409, "CONFLICT");
604
+ }
605
+ const live = existing.filter(
606
+ (row) => row.status === "pending" && row.expiresAt > now && row.statusReason !== "superseded"
607
+ );
608
+ for (const row of live) {
609
+ const decision = liveAttemptCommitDecision(row, insert, now);
610
+ if (decision === "conflict") {
611
+ throw hostError(
612
+ "An unpaid checkout for this payment method is already in progress for this reference.",
613
+ 409,
614
+ "CONFLICT"
615
+ );
616
+ }
617
+ if (decision === "supersede") {
618
+ await tx.query(
619
+ statement(
620
+ `UPDATE ${table} SET status_reason = 'superseded', updated_at = ? WHERE payment_hash = ? AND status = 'pending'`
621
+ ),
622
+ [now, row.paymentHash]
623
+ );
624
+ }
625
+ }
626
+ await tx.query(
627
+ statement(
628
+ `INSERT INTO ${table} (reference, payment_hash, status, paid_at, expires_at, created_at, updated_at, inserted_at, checkout_data, swap_data, client_ip)
629
+ VALUES (?, ?, 'pending', NULL, ?, ?, ?, ?, ?, ?, ?)`
630
+ ),
631
+ [
632
+ insert.reference,
633
+ insert.paymentHash,
634
+ insert.expiresAt,
635
+ insert.createdAt,
636
+ now,
637
+ now,
638
+ JSON.stringify(insert.checkout),
639
+ insert.swapData === void 0 ? null : JSON.stringify(insert.swapData),
640
+ insert.clientIp ?? null
641
+ ]
642
+ );
643
+ });
644
+ },
645
+ async countAttemptsFromIp(clientIp, sinceUnixSeconds) {
646
+ const rows = await adapter.query(
647
+ statement(`SELECT COUNT(*) AS n FROM ${table} WHERE client_ip = ? AND inserted_at >= ?`),
648
+ [clientIp, sinceUnixSeconds]
649
+ );
650
+ return asInteger(rows[0]?.n ?? 0, "n");
651
+ },
652
+ async recordReconciliation(transition) {
653
+ await assertSupportedSchema();
654
+ await adapter.transaction(async (tx) => {
655
+ await tx.query(
656
+ statement(
657
+ `UPDATE ${table} SET status = ?, status_reason = ?, updated_at = ? WHERE payment_hash = ? AND status = 'pending'`
658
+ ),
659
+ [
660
+ transition.status,
661
+ transition.reason,
662
+ transition.observedAt,
663
+ transition.paymentHash.toLowerCase()
664
+ ]
665
+ );
666
+ });
667
+ },
668
+ // Same write-once claim as markPaidOnce, without a fulfillment hook: in
669
+ // custom-repository mode the host's own handler runs outside this
670
+ // transaction, so it is handed no transactional query.
671
+ recordSettlement: (settlement) => markPaidOnce(settlement, () => void 0),
672
+ markPaidOnce
673
+ };
674
+ async function markPaidOnce(input, fulfill) {
675
+ await assertSupportedSchema();
676
+ const paymentHash = input.paymentHash.toLowerCase();
677
+ return adapter.transaction(async (tx) => {
678
+ const preliminary = await tx.query(
679
+ statement(`SELECT reference FROM ${table} WHERE payment_hash = ?`),
680
+ [paymentHash]
681
+ );
682
+ const reference = preliminary[0]?.reference;
683
+ if (reference === void 0) return false;
684
+ await lockReference(tx, asString(reference, "reference"));
685
+ const rows = await rowsForReference(tx, asString(reference, "reference"));
686
+ const row = rows.find((candidate) => candidate.paymentHash === paymentHash);
687
+ if (row === void 0 || row.status === "settled") return false;
688
+ const firstForReference = !rows.some((candidate) => candidate.status === "settled");
689
+ const now = clock();
690
+ await tx.query(
691
+ statement(
692
+ `UPDATE ${table} SET status = 'settled', status_reason = ?, paid_at = ?, updated_at = ? WHERE payment_hash = ?`
693
+ ),
694
+ [firstForReference ? null : "duplicate_settlement", input.paidAt, now, paymentHash]
695
+ );
696
+ if (firstForReference) {
697
+ await fulfill({
698
+ reference: row.reference,
699
+ paymentHash,
700
+ paidAt: input.paidAt,
701
+ ...input.details === void 0 ? {} : { details: input.details },
702
+ query: tx.query
703
+ });
704
+ }
705
+ return firstForReference;
706
+ });
707
+ }
708
+ }
709
+ function isFreshTimestamp(now, timestamp, windowSeconds) {
710
+ const age = now - timestamp;
711
+ if (age < -META_CLOCK_SKEW_SECONDS) return false;
712
+ return age < windowSeconds;
713
+ }
714
+ function recordFromRow(row) {
715
+ const swapData = row.swap_data;
716
+ const paymentHash = asString(row.payment_hash, "payment_hash");
717
+ return {
718
+ reference: asString(row.reference, "reference"),
719
+ paymentHash,
720
+ status: asStatus(row.status),
721
+ statusReason: row.status_reason === void 0 ? null : row.status_reason,
722
+ paidAt: row.paid_at === null || row.paid_at === void 0 ? null : asInteger(row.paid_at, "paid_at"),
723
+ expiresAt: asInteger(row.expires_at, "expires_at"),
724
+ createdAt: asInteger(row.created_at, "created_at"),
725
+ checkout: parseRowJson(
726
+ asString(row.checkout_data, "checkout_data"),
727
+ "checkout_data",
728
+ paymentHash
729
+ ),
730
+ swapData: swapData === null || swapData === void 0 ? null : parseRowJson(asString(swapData, "swap_data"), "swap_data", paymentHash)
731
+ };
732
+ }
733
+ function parseRowJson(value, column, paymentHash) {
734
+ try {
735
+ return JSON.parse(value);
736
+ } catch {
737
+ throw new TypeError(
738
+ `Corrupt ${column} JSON on openreceive payment attempt ${paymentHash}; the row cannot be read.`
739
+ );
740
+ }
741
+ }
742
+ var ATTEMPT_STATUSES = [
743
+ "pending",
744
+ "settled",
745
+ "expired",
746
+ "failed",
747
+ "attention"
748
+ ];
749
+ function asStatus(value) {
750
+ if (typeof value === "string" && ATTEMPT_STATUSES.includes(value)) {
751
+ return value;
752
+ }
753
+ throw new TypeError(`Unexpected openreceive_payments status: ${String(value)}`);
754
+ }
755
+ function asString(value, field) {
756
+ if (typeof value === "string") return value;
757
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
758
+ throw new TypeError(`Expected ${field} to be a string.`);
759
+ }
760
+ function asInteger(value, field) {
761
+ if (typeof value === "number" && Number.isSafeInteger(value)) return value;
762
+ if (typeof value === "bigint") {
763
+ if (value < BigInt(Number.MIN_SAFE_INTEGER) || value > BigInt(Number.MAX_SAFE_INTEGER)) {
764
+ throw new TypeError(`Expected ${field} to fit a safe integer.`);
765
+ }
766
+ return Number(value);
767
+ }
768
+ if (typeof value === "string" && /^\s*-?\d+\s*$/.test(value)) {
769
+ const parsed = Number(value.trim());
770
+ if (Number.isSafeInteger(parsed)) return parsed;
771
+ }
772
+ throw new TypeError(`Expected ${field} to be an integer.`);
773
+ }
774
+ function assertSafeIdentifier(name) {
775
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
776
+ throw new RangeError(`Unsafe SQL identifier: ${name}`);
777
+ }
778
+ }
779
+
780
+ // src/host-payments.ts
781
+ function warnFailure(event, prefix, error) {
782
+ const message = error instanceof Error ? error.message : String(error);
783
+ const sanitized = sanitizeEvent({ level: "warn", event, message });
784
+ console.warn(`[openreceive] ${prefix}: ${String(sanitized.message)}`);
785
+ }
786
+ function createHost(options) {
787
+ if (options?.amountFor === void 0) {
788
+ throw new TypeError("OpenReceive host requires amountFor.");
789
+ }
790
+ if (options.onPaid === void 0) {
791
+ throw new TypeError(
792
+ "OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw settlement event in custom repository mode)."
793
+ );
794
+ }
795
+ let payments;
796
+ let onPaid;
797
+ if (options.db !== void 0) {
798
+ const repository = createSqlPayments(options.db, {
799
+ ...options.tableName === void 0 ? {} : { tableName: options.tableName },
800
+ ...options.clock === void 0 ? {} : { clock: options.clock }
801
+ });
802
+ payments = repository;
803
+ const fulfill = options.onPaid;
804
+ onPaid = async (input) => {
805
+ await repository.markPaidOnce(input, fulfill);
806
+ };
807
+ } else {
808
+ if (options.payments?.listForReference === void 0) {
809
+ throw new TypeError("OpenReceive host requires db or payments.listForReference.");
810
+ }
811
+ if (options.payments.commitAttempt === void 0) {
812
+ throw new TypeError("OpenReceive host requires payments.commitAttempt.");
813
+ }
814
+ if (options.payments.listReconcilableAttempts === void 0) {
815
+ throw new TypeError("OpenReceive host requires payments.listReconcilableAttempts.");
816
+ }
817
+ if (options.payments.recordReconciliation === void 0) {
818
+ throw new TypeError("OpenReceive host requires payments.recordReconciliation.");
819
+ }
820
+ if (typeof options.payments.recordSettlement !== "function") {
821
+ throw new TypeError(
822
+ "OpenReceive host requires payments.recordSettlement (the write-once settlement claim)."
823
+ );
824
+ }
825
+ payments = options.payments;
826
+ const custom = options.payments;
827
+ const notify = options.onPaid;
828
+ onPaid = async (settlement) => {
829
+ const claimed = await custom.recordSettlement({
830
+ paymentHash: settlement.paymentHash,
831
+ paidAt: settlement.paidAt,
832
+ ...settlement.details === void 0 ? {} : { details: settlement.details }
833
+ });
834
+ if (claimed) await notify(settlement);
835
+ };
836
+ }
837
+ const clock = options.clock ?? unixSeconds3;
838
+ const priceFor = async (context) => {
839
+ const amount = await options.amountFor(context.reference, context);
840
+ if (amount === null) throw hostError("Unknown reference.", 404, "NOT_FOUND");
841
+ return amount;
842
+ };
843
+ const resolveCheckout = async (context) => {
844
+ if (context.action === "swap.quote" || context.action === "checkout.prepare") {
845
+ return { amount: await priceFor(context) };
846
+ }
847
+ const isCreate = context.action === "checkout.create" || context.action === "swap.create";
848
+ const amount = isCreate ? await priceFor(context) : void 0;
849
+ const attempts = normalizePayments(await payments.listForReference(context.reference));
850
+ const requestedHash = paymentHashHint(context.input);
851
+ if (requestedHash !== void 0) {
852
+ const selected = attempts.find((payment) => payment.paymentHash === requestedHash);
853
+ if (selected === void 0) {
854
+ throw hostError("Payment attempt not found for this reference.", 404, "NOT_FOUND");
855
+ }
856
+ if (context.action === "checkout.create" || context.action === "swap.create") {
857
+ if (attempts.some((payment) => payment.status === "settled")) {
858
+ throw hostError("This reference is already paid.", 409, "CONFLICT");
859
+ }
860
+ const now = clock();
861
+ if (!isLivePaymentAttempt(selected, now) || !isReusablePaymentAttempt(selected.expiresAt, now) || !matchesCreateAction(selected, context.action, context.payInAsset)) {
862
+ throw hostError(
863
+ "The selected payment attempt is not a reusable pending checkout.",
864
+ 409,
865
+ "CONFLICT"
866
+ );
867
+ }
868
+ }
869
+ return resolvedPayment(amount, selected);
870
+ }
871
+ if (context.action === "checkout.create" || context.action === "swap.create") {
872
+ if (attempts.some((payment) => payment.status === "settled")) {
873
+ throw hostError("This reference is already paid.", 409, "CONFLICT");
874
+ }
875
+ const now = clock();
876
+ const matching = attempts.filter(
877
+ (payment) => isLivePaymentAttempt(payment, now) && matchesCreateAction(payment, context.action, context.payInAsset)
878
+ );
879
+ if (matching.length > 1) {
880
+ throw hostError(
881
+ "This reference has multiple unpaid checkouts in progress for this payment method; wait for them to expire before creating another.",
882
+ 409,
883
+ "CONFLICT"
884
+ );
885
+ }
886
+ const selected = matching[0];
887
+ if (selected === void 0) return { amount };
888
+ if (!isReusablePaymentAttempt(selected.expiresAt, now)) return { amount };
889
+ return resolvedPayment(amount, selected);
890
+ }
891
+ throw hostError("payment_hash is required for this action.", 400, "INVALID_REQUEST");
892
+ };
893
+ return {
894
+ resolveCheckout,
895
+ onCheckoutCreated: (input) => payments.commitAttempt(input),
896
+ onPaid,
897
+ payments
898
+ };
899
+ }
900
+ function resolvedPayment(amount, payment) {
901
+ return {
902
+ ...amount === void 0 ? {} : { amount },
903
+ paymentHash: payment.paymentHash,
904
+ checkout: structuredClone(payment.checkout),
905
+ // Carried so `payments/check` can serve the row path without re-listing
906
+ // the rows this resolver just read.
907
+ attemptStatus: { status: payment.status, paidAt: payment.paidAt },
908
+ ...payment.swapData === void 0 || payment.swapData === null ? {} : { swapData: payment.swapData }
909
+ };
910
+ }
911
+ function isLivePaymentAttempt(payment, now) {
912
+ return payment.status === "pending" && payment.statusReason !== "superseded" && payment.expiresAt > now;
913
+ }
914
+ function matchesCreateAction(payment, action, payInAsset) {
915
+ const isSwap = payment.swapData !== void 0 && payment.swapData !== null;
916
+ if (action === "checkout.create") return !isSwap;
917
+ if (action !== "swap.create") return false;
918
+ if (!isSwap) return false;
919
+ if (payInAsset === void 0) return true;
920
+ return payment.swapData?.providerOrder.pay_in_asset === payInAsset;
921
+ }
922
+ function normalizePayments(values) {
923
+ return values.map((payment) => ({
924
+ ...payment,
925
+ paymentHash: storedPaymentHash(payment.paymentHash)
926
+ })).sort(
927
+ (left, right) => right.createdAt - left.createdAt || right.paymentHash.localeCompare(left.paymentHash)
928
+ );
929
+ }
930
+ function paymentHashHint(input) {
931
+ const value = input.payment_hash;
932
+ if (value === void 0) return void 0;
933
+ if (typeof value !== "string") {
934
+ throw hostError("payment_hash must be a string.", 400, "INVALID_REQUEST");
935
+ }
936
+ return normalizePaymentHash(value);
937
+ }
938
+ function normalizePaymentHash(value) {
939
+ const normalized = value.trim().toLowerCase();
940
+ if (!/^[0-9a-f]{64}$/.test(normalized)) {
941
+ throw hostError("payment_hash must be 64 hexadecimal characters.", 400, "INVALID_REQUEST");
942
+ }
943
+ return normalized;
944
+ }
945
+ function storedPaymentHash(value) {
946
+ const normalized = value.trim().toLowerCase();
947
+ if (!/^[0-9a-f]{64}$/.test(normalized)) {
948
+ throw hostError(
949
+ "The payment repository returned a row with a malformed payment hash.",
950
+ 500,
951
+ "INTERNAL"
952
+ );
953
+ }
954
+ return normalized;
955
+ }
956
+
957
+ // src/reconcile-loop.ts
958
+ import { unixSeconds as unixSeconds4 } from "@openreceive/core";
959
+ async function reconcileHostPayments(input) {
960
+ const clock = input.clock ?? unixSeconds4;
961
+ const attempts = input.attempts ?? await input.host.payments.listReconcilableAttempts();
962
+ if (attempts.length === 0) return [];
963
+ const byHash = new Map(
964
+ attempts.map((attempt) => [attempt.paymentHash.toLowerCase(), attempt])
965
+ );
966
+ const scannedAt = clock();
967
+ const checks = await input.service.reconcilePayments({
968
+ attempts,
969
+ overlapSeconds: input.overlapSeconds,
970
+ ...input.maxPages === void 0 ? {} : { maxPages: input.maxPages }
971
+ });
972
+ const failures = [];
973
+ for (const checked of checks) {
974
+ try {
975
+ const attempt = byHash.get(checked.paymentHash.toLowerCase());
976
+ if (attempt === void 0) continue;
977
+ if (checked.status === "settled") {
978
+ if (checked.paidAt === void 0) {
979
+ failures.push(
980
+ new Error(
981
+ `settled reconcile result for ${checked.paymentHash} carried no paidAt; the attempt stays pending and will be retried`
982
+ )
983
+ );
984
+ continue;
985
+ }
986
+ await input.host.onPaid({
987
+ paymentHash: checked.paymentHash,
988
+ paidAt: checked.paidAt,
989
+ details: checked.details
990
+ });
991
+ continue;
992
+ }
993
+ const walletTransaction = checked.details?.transaction;
994
+ const transition = reconciliationTransition(
995
+ attempt,
996
+ checked.status,
997
+ scannedAt,
998
+ walletTransaction?.state ?? walletTransaction?.transaction_state
999
+ );
1000
+ if (transition !== null) {
1001
+ await input.host.payments.recordReconciliation(transition);
1002
+ }
1003
+ } catch (error) {
1004
+ failures.push(error);
1005
+ }
1006
+ }
1007
+ if (failures.length > 0) {
1008
+ const first = failures[0];
1009
+ throw new AggregateError(
1010
+ failures,
1011
+ `reconciliation failed for ${failures.length} of ${checks.length} checks: ` + (first instanceof Error ? first.message : String(first))
1012
+ );
1013
+ }
1014
+ return checks;
1015
+ }
1016
+ async function startReconciler(input) {
1017
+ const pollIntervalMs = input.pollIntervalMs ?? 5e3;
1018
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250) {
1019
+ throw new RangeError("pollIntervalMs must be a safe integer of at least 250");
1020
+ }
1021
+ if (typeof input.host.payments.claimReconcileGate !== "function") {
1022
+ throw new TypeError(
1023
+ "The reconciler requires payments.claimReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
1024
+ );
1025
+ }
1026
+ const overlapSeconds = input.overlapSeconds ?? 60;
1027
+ const controller = new AbortController();
1028
+ const stop = () => controller.abort();
1029
+ input.signal?.addEventListener("abort", stop, { once: true });
1030
+ let lastWarnedMessage;
1031
+ const reportError = (error) => {
1032
+ if (input.onError !== void 0) {
1033
+ try {
1034
+ input.onError(error);
1035
+ } catch {
1036
+ }
1037
+ return;
1038
+ }
1039
+ const message = error instanceof Error ? error.message : String(error);
1040
+ if (message === lastWarnedMessage) return;
1041
+ lastWarnedMessage = message;
1042
+ warnFailure("payment.reconcile.failed", "reconciliation pass failed (will retry)", error);
1043
+ };
1044
+ const done = (async () => {
1045
+ try {
1046
+ while (!controller.signal.aborted) {
1047
+ const result = await maybeReconcilePayments({
1048
+ service: input.service,
1049
+ host: input.host,
1050
+ overlapSeconds,
1051
+ onError: reportError,
1052
+ ...input.clock === void 0 ? {} : { clock: input.clock }
1053
+ });
1054
+ if (result.reason === "ran") lastWarnedMessage = void 0;
1055
+ await abortableDelay(pollIntervalMs, controller.signal);
1056
+ }
1057
+ } finally {
1058
+ input.signal?.removeEventListener("abort", stop);
1059
+ }
1060
+ })();
1061
+ return { stop, done };
1062
+ }
1063
+ function abortableDelay(milliseconds, signal) {
1064
+ if (signal.aborted) return Promise.resolve();
1065
+ return new Promise((resolve) => {
1066
+ const timer = setTimeout(done, milliseconds);
1067
+ signal.addEventListener("abort", done, { once: true });
1068
+ function done() {
1069
+ clearTimeout(timer);
1070
+ signal.removeEventListener("abort", done);
1071
+ resolve();
1072
+ }
1073
+ });
1074
+ }
1075
+
1076
+ // src/reconcile-gate.ts
1077
+ var OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS = 2;
1078
+ var OPENRECEIVE_RECONCILE_SCAN_TIMEOUT_MS = 9e3;
1079
+ var OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES = 50;
1080
+ var EARLY_INVOICE_INTERVAL_SECONDS = 2;
1081
+ var MID_INVOICE_INTERVAL_SECONDS = 6;
1082
+ var LATE_INVOICE_INTERVAL_SECONDS = 12;
1083
+ var EARLY_INVOICE_WINDOW_SECONDS = 2 * 60;
1084
+ var MID_INVOICE_WINDOW_SECONDS = 5 * 60;
1085
+ function reconcileIntervalSeconds(attempts, now, minIntervalSeconds = OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS) {
1086
+ const floor = Math.max(OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS, minIntervalSeconds);
1087
+ if (attempts.length === 0) return floor;
1088
+ const ageStretch = Math.min(
1089
+ ...attempts.map((attempt) => intervalForInvoiceAge(Math.max(0, now - attempt.createdAt)))
1090
+ );
1091
+ return Math.max(floor, ageStretch);
1092
+ }
1093
+ function intervalForInvoiceAge(elapsedSeconds) {
1094
+ if (elapsedSeconds < EARLY_INVOICE_WINDOW_SECONDS) return EARLY_INVOICE_INTERVAL_SECONDS;
1095
+ if (elapsedSeconds < MID_INVOICE_WINDOW_SECONDS) return MID_INVOICE_INTERVAL_SECONDS;
1096
+ return LATE_INVOICE_INTERVAL_SECONDS;
1097
+ }
1098
+ async function maybeReconcilePayments(input) {
1099
+ const claimReconcileGate = input.host.payments.claimReconcileGate;
1100
+ if (typeof claimReconcileGate !== "function") {
1101
+ throw new TypeError(
1102
+ "Opportunistic reconcile requires payments.claimReconcileGate (a durable CAS gate); implement it on the repository or disable with opportunisticReconcile: false."
1103
+ );
1104
+ }
1105
+ const report = input.onError ?? ((error) => {
1106
+ warnFailure(
1107
+ "payment.reconcile.opportunistic.failed",
1108
+ "opportunistic reconcile failed (will retry)",
1109
+ error
1110
+ );
1111
+ });
1112
+ try {
1113
+ const attempts = await input.host.payments.listReconcilableAttempts();
1114
+ if (attempts.length === 0) return { reason: "no_pending" };
1115
+ const clock = input.clock ?? unixSeconds5;
1116
+ const now = clock();
1117
+ const intervalSeconds = reconcileIntervalSeconds(attempts, now, input.minIntervalSeconds);
1118
+ const claimed = await claimReconcileGate.call(input.host.payments, { now, intervalSeconds });
1119
+ if (!claimed) return { reason: "gate_busy" };
1120
+ const checks = await withScanTimeout(
1121
+ reconcileHostPayments({
1122
+ service: input.service,
1123
+ host: input.host,
1124
+ // Already read above to size the gate interval; the pass scans that
1125
+ // exact batch rather than repeating the query on the request path.
1126
+ attempts,
1127
+ overlapSeconds: input.overlapSeconds,
1128
+ maxPages: input.maxPages ?? OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES,
1129
+ ...compact({ clock: input.clock })
1130
+ }),
1131
+ input.scanTimeoutMs ?? OPENRECEIVE_RECONCILE_SCAN_TIMEOUT_MS
1132
+ );
1133
+ return { reason: "ran", checks };
1134
+ } catch (error) {
1135
+ report(error);
1136
+ return { reason: "scan_failed" };
1137
+ }
1138
+ }
1139
+ function withScanTimeout(work, timeoutMs) {
1140
+ return new Promise((resolve, reject) => {
1141
+ const timer = setTimeout(() => {
1142
+ reject(new Error(`reconcile scan exceeded ${timeoutMs}ms`));
1143
+ }, timeoutMs);
1144
+ work.then(
1145
+ (value) => {
1146
+ clearTimeout(timer);
1147
+ resolve(value);
1148
+ },
1149
+ (error) => {
1150
+ clearTimeout(timer);
1151
+ reject(error);
1152
+ }
1153
+ );
1154
+ });
1155
+ }
1156
+
1157
+ // src/http-request.ts
1158
+ import { isRecord, nonEmptyString } from "@openreceive/core";
1159
+ var MAX_REFERENCE_LENGTH = 200;
1160
+ var MAX_MEMO_LENGTH = 500;
1161
+ var MAX_BODY_BYTES = 64 * 1024;
1162
+ var ROUTE_BODY_FIELDS = {
1163
+ "checkout.prepare": ["reference"],
1164
+ "checkout.create": ["reference", "memo", "metadata"],
1165
+ "payment.check": ["reference", "payment_hash"],
1166
+ "swap.quote": ["reference", "pay_in_asset"],
1167
+ "swap.create": ["reference", "pay_in_asset", "memo", "metadata"],
1168
+ "swap.read": ["reference", "payment_hash"],
1169
+ "swap.refund": ["reference", "payment_hash", "refund_address"]
1170
+ };
1171
+ function assertDeclaredFields(routeKind, body) {
1172
+ const allowed = ROUTE_BODY_FIELDS[routeKind];
1173
+ if (allowed === void 0) return;
1174
+ for (const key of Object.keys(body)) {
1175
+ if (!allowed.includes(key)) {
1176
+ throw new HttpError(
1177
+ 400,
1178
+ "INVALID_REQUEST",
1179
+ `Unexpected request field for this route: ${key}.`
1180
+ );
1181
+ }
1182
+ }
1183
+ }
1184
+ function rejectPayerAmount(body) {
1185
+ if (body.amount !== void 0 || body.amount_msats !== void 0) {
1186
+ throw new HttpError(
1187
+ 400,
1188
+ "INVALID_REQUEST",
1189
+ "This route does not accept a payer-supplied amount; the host resolves its order price."
1190
+ );
1191
+ }
1192
+ }
1193
+ function assertJsonContentType(request) {
1194
+ const contentType = (request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
1195
+ if (contentType !== "application/json") {
1196
+ throw new HttpError(415, "INVALID_REQUEST", "Request content type must be application/json.");
1197
+ }
1198
+ }
1199
+ function assertNotCrossSite(request) {
1200
+ if (request.headers.get("sec-fetch-site")?.trim().toLowerCase() === "cross-site") {
1201
+ throw new HttpError(403, "FORBIDDEN", "Cross-site requests are not accepted.");
1202
+ }
1203
+ }
1204
+ async function readJsonBody(request) {
1205
+ assertNotCrossSite(request);
1206
+ assertJsonContentType(request);
1207
+ const declaredLength = Number(request.headers.get("content-length") ?? "0");
1208
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
1209
+ throw new HttpError(413, "INVALID_REQUEST", "Request body is too large.");
1210
+ }
1211
+ const text = await readCappedBodyText(request);
1212
+ try {
1213
+ const value = text.trim() === "" ? {} : JSON.parse(text);
1214
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error();
1215
+ return value;
1216
+ } catch {
1217
+ throw new HttpError(400, "INVALID_REQUEST", "Request body must be a JSON object.");
1218
+ }
1219
+ }
1220
+ async function readCappedBodyText(request) {
1221
+ const stream = request.body;
1222
+ if (stream === null) return "";
1223
+ const reader = stream.getReader();
1224
+ const chunks = [];
1225
+ let total = 0;
1226
+ for (; ; ) {
1227
+ let result;
1228
+ try {
1229
+ result = await reader.read();
1230
+ } catch {
1231
+ throw new HttpError(400, "INVALID_REQUEST", "Request body must be a JSON object.");
1232
+ }
1233
+ if (result.done) break;
1234
+ total += result.value.byteLength;
1235
+ if (total > MAX_BODY_BYTES) {
1236
+ await reader.cancel().catch(() => void 0);
1237
+ throw new HttpError(413, "INVALID_REQUEST", "Request body is too large.");
1238
+ }
1239
+ chunks.push(result.value);
1240
+ }
1241
+ const body = new Uint8Array(total);
1242
+ let offset = 0;
1243
+ for (const chunk of chunks) {
1244
+ body.set(chunk, offset);
1245
+ offset += chunk.byteLength;
1246
+ }
1247
+ return new TextDecoder().decode(body);
1248
+ }
1249
+ function ratesCurrencies(raw) {
1250
+ if (raw === null) return void 0;
1251
+ const currencies = raw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
1252
+ if (currencies.length === 0 || currencies.some((value) => !/^[A-Za-z]{3}$/.test(value))) {
1253
+ throw new HttpError(
1254
+ 400,
1255
+ "INVALID_REQUEST",
1256
+ "currencies must be a comma-separated list of three-letter currency codes."
1257
+ );
1258
+ }
1259
+ return currencies;
1260
+ }
1261
+ function optionalCheckoutFields(body) {
1262
+ const memo = trimmedField(body.memo);
1263
+ if (memo !== void 0 && memo.length > MAX_MEMO_LENGTH) {
1264
+ throw new HttpError(
1265
+ 400,
1266
+ "INVALID_REQUEST",
1267
+ `memo must be ${MAX_MEMO_LENGTH} characters or fewer.`
1268
+ );
1269
+ }
1270
+ const metadata = readRecord(body.metadata);
1271
+ return {
1272
+ ...memo === void 0 ? {} : { memo },
1273
+ ...metadata === void 0 ? {} : { metadata }
1274
+ };
1275
+ }
1276
+ function requiredPaymentHash(value) {
1277
+ const hash = requiredString(value, "payment_hash").toLowerCase();
1278
+ if (!/^[0-9a-f]{64}$/.test(hash)) {
1279
+ throw new HttpError(400, "INVALID_REQUEST", "payment_hash must be 64 hexadecimal characters.");
1280
+ }
1281
+ return hash;
1282
+ }
1283
+ function requiredString(value, field, maxLength) {
1284
+ const result = trimmedField(value);
1285
+ if (result === void 0) throw new HttpError(400, "INVALID_REQUEST", `${field} is required.`);
1286
+ if (maxLength !== void 0 && result.length > maxLength) {
1287
+ throw new HttpError(
1288
+ 400,
1289
+ "INVALID_REQUEST",
1290
+ `${field} must be ${maxLength} characters or fewer.`
1291
+ );
1292
+ }
1293
+ return result;
1294
+ }
1295
+ function trimmedField(value) {
1296
+ return typeof value === "string" ? nonEmptyString(value.trim()) : void 0;
1297
+ }
1298
+ function readRecord(value) {
1299
+ if (value === void 0) return void 0;
1300
+ if (!isRecord(value)) {
1301
+ throw new HttpError(400, "INVALID_REQUEST", "metadata must be an object.");
1302
+ }
1303
+ return value;
1304
+ }
1305
+
1306
+ // src/http-response.ts
1307
+ function toSnakeCase(value) {
1308
+ if (Array.isArray(value)) return value.map(toSnakeCase);
1309
+ if (typeof value === "bigint") return bigintToJsonNumber(value);
1310
+ if (value === null || typeof value !== "object") return value;
1311
+ return Object.fromEntries(
1312
+ Object.entries(value).map(([key, item]) => [
1313
+ key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
1314
+ toSnakeCase(item)
1315
+ ])
1316
+ );
1317
+ }
1318
+ function httpCheckout(checkout) {
1319
+ return {
1320
+ reference: checkout.reference,
1321
+ payment_hash: checkout.paymentHash,
1322
+ bolt11: checkout.bolt11,
1323
+ amount_msats: checkout.amountMsats,
1324
+ created_at: checkout.createdAt,
1325
+ expires_at: checkout.expiresAt,
1326
+ fiat_quote: checkout.fiatQuote === null ? null : toSnakeCase(checkout.fiatQuote)
1327
+ };
1328
+ }
1329
+ function httpSwap(swap) {
1330
+ const { checkout, swapData: _swapData, ...rest } = swap;
1331
+ return {
1332
+ ...toSnakeCase(rest),
1333
+ checkout: httpCheckout(checkout)
1334
+ };
1335
+ }
1336
+ function paymentCheckFromReconcilePass(checked) {
1337
+ const { details, ...checkedPublic } = checked;
1338
+ return {
1339
+ ...toSnakeCase(checkedPublic),
1340
+ ...details === void 0 ? {} : { details: publicPaymentDetails(details) }
1341
+ };
1342
+ }
1343
+ async function paymentCheckFromStoredAttempt(payments, reference, paymentHash, resolvedStatus) {
1344
+ const record = resolvedStatus ?? (await payments.listForReference(reference)).find(
1345
+ (row) => row.paymentHash.toLowerCase() === paymentHash.toLowerCase()
1346
+ );
1347
+ if (record === void 0) {
1348
+ throw new HttpError(404, "NOT_FOUND", "Payment attempt not found for this reference.");
1349
+ }
1350
+ return {
1351
+ payment_hash: paymentHash.toLowerCase(),
1352
+ status: record.status === "attention" ? "pending" : record.status,
1353
+ ...record.paidAt === null ? {} : { paid_at: record.paidAt }
1354
+ };
1355
+ }
1356
+ function publicPaymentDetails(details) {
1357
+ const transaction = details.transaction;
1358
+ const pick = (keys) => Object.fromEntries(
1359
+ keys.flatMap(
1360
+ (key) => transaction?.[key] === void 0 ? [] : [[key, transaction[key]]]
1361
+ )
1362
+ );
1363
+ return {
1364
+ ...transaction === void 0 ? {} : {
1365
+ // `transaction_state` only: the NWC client normalizes every wallet
1366
+ // spelling to it at the boundary, so no row here ever carries a bare
1367
+ // `state`. Ruby's PUBLIC_TRANSACTION_FIELDS already omitted it.
1368
+ transaction: pick([
1369
+ "payment_hash",
1370
+ "transaction_state",
1371
+ "amount_msats",
1372
+ "fees_paid_msats",
1373
+ "created_at",
1374
+ "settled_at",
1375
+ "expires_at"
1376
+ ])
1377
+ },
1378
+ observed_at: details.observed_at,
1379
+ ...details.paid_at_source === void 0 ? {} : { paid_at_source: details.paid_at_source }
1380
+ };
1381
+ }
1382
+
1383
+ // src/rate-limit.ts
1384
+ import { unixSeconds as unixSeconds6 } from "@openreceive/core";
1385
+ var OPENRECEIVE_DEFAULT_IP_RATE_LIMIT_PER_HOUR = 60;
1386
+ var HOUR_SECONDS = 3600;
1387
+ var DAY_SECONDS = 86400;
1388
+ var MINT_ACTIONS = /* @__PURE__ */ new Set(["checkout.create", "swap.create"]);
1389
+ var DEFAULT_ACTIONS = ["checkout.create", "swap.create"];
1390
+ var DEFAULT_MESSAGE = "Too many payment attempts. Please try again later.";
1391
+ function createIpRateLimit(config = {}) {
1392
+ const limitPerHour = config.limitPerHour ?? OPENRECEIVE_DEFAULT_IP_RATE_LIMIT_PER_HOUR;
1393
+ const limitPerDay = config.limitPerDay;
1394
+ if (!Number.isSafeInteger(limitPerHour) || limitPerHour < 1) {
1395
+ throw new TypeError("rateLimiting limitPerHour must be an integer >= 1.");
1396
+ }
1397
+ if (limitPerDay !== void 0 && (!Number.isSafeInteger(limitPerDay) || limitPerDay < 1)) {
1398
+ throw new TypeError("rateLimiting limitPerDay must be an integer >= 1.");
1399
+ }
1400
+ const actions = new Set(config.actions ?? DEFAULT_ACTIONS);
1401
+ for (const action of actions) {
1402
+ if (!MINT_ACTIONS.has(action)) {
1403
+ throw new TypeError(
1404
+ `rateLimiting actions only accept the invoice-minting actions (checkout.create, swap.create); got "${action}". Attempt-row counting counts mints, so a throttle on other actions could never trigger. Use a custom rateLimitHook to police other actions.`
1405
+ );
1406
+ }
1407
+ }
1408
+ const count = config.countAttemptsFromIp;
1409
+ if (count === void 0) {
1410
+ throw new TypeError(
1411
+ "rateLimiting requires persistent counting. Implement countAttemptsFromIp on the payment repository (the built-in SQL repository already does), pass countAttemptsFromIp in the rateLimiting config, or disable rateLimiting and use a custom rateLimitHook backed by your own store. There is deliberately no in-memory fallback: per-process counts reset on restart and multiply per instance behind a load balancer."
1412
+ );
1413
+ }
1414
+ const message = config.message ?? DEFAULT_MESSAGE;
1415
+ const extractIp = config.ip ?? resolveClientIp;
1416
+ const now = config.now ?? unixSeconds6;
1417
+ let warnedUnattributable = false;
1418
+ return async (context) => {
1419
+ if (!actions.has(context.action)) return true;
1420
+ const extracted = extractIp(context);
1421
+ const ip = extracted === void 0 ? void 0 : clientIpBucket(extracted);
1422
+ if (ip === void 0 || ip.length === 0) {
1423
+ if (!warnedUnattributable) {
1424
+ warnedUnattributable = true;
1425
+ console.warn(
1426
+ "[openreceive] rateLimiting allowed a request with no attributable client IP (fail-open). If every request logs no IP, the adapter is not supplying one and rate limiting is inactive \u2014 see the rate limiting guide."
1427
+ );
1428
+ }
1429
+ return true;
1430
+ }
1431
+ const at = now();
1432
+ if (await count(ip, at - HOUR_SECONDS) >= limitPerHour) {
1433
+ throw tooManyAttempts(message);
1434
+ }
1435
+ if (limitPerDay !== void 0 && await count(ip, at - DAY_SECONDS) >= limitPerDay) {
1436
+ throw tooManyAttempts(message);
1437
+ }
1438
+ return true;
1439
+ };
1440
+ }
1441
+ function createProxyRateLimitingConfig(rateLimiting, trustProxyIpHeader, options = {}) {
1442
+ if (rateLimiting === void 0 || rateLimiting === false) return {};
1443
+ const headerName = trustProxyIpHeader === true ? "x-forwarded-for" : typeof trustProxyIpHeader === "string" ? trustProxyIpHeader.toLowerCase() : void 0;
1444
+ const headerIp = headerName === void 0 ? void 0 : (context) => {
1445
+ const value = context.request.headers.get(headerName);
1446
+ const first = value?.split(",")[0]?.trim();
1447
+ return first !== void 0 && first.length > 0 ? first : void 0;
1448
+ };
1449
+ const { requireIpSource } = options;
1450
+ if (headerIp === void 0 && requireIpSource === void 0) return {};
1451
+ const config = rateLimiting === true ? {} : rateLimiting;
1452
+ if (headerIp === void 0 && config.ip === void 0 && requireIpSource !== void 0) {
1453
+ throw new TypeError(
1454
+ `rateLimiting on the ${requireIpSource} adapter needs a client IP source: a web Request has no socket IP. Pass trustProxyIpHeader: true to read x-forwarded-for (only behind your own reverse proxy), name another trusted header, or supply a custom rateLimiting.ip extractor.`
1455
+ );
1456
+ }
1457
+ return { rateLimiting: { ...config, ip: config.ip ?? headerIp } };
1458
+ }
1459
+ function resolveClientIp(context) {
1460
+ const native = context.native;
1461
+ const ip = native?.ip;
1462
+ return typeof ip === "string" && ip.length > 0 ? ip : void 0;
1463
+ }
1464
+ function clientIpBucket(ip) {
1465
+ let value = ip.trim().toLowerCase();
1466
+ if (value.startsWith("::ffff:") && value.includes(".")) value = value.slice("::ffff:".length);
1467
+ if (!value.includes(":")) return value;
1468
+ if (value.endsWith("/64")) return value;
1469
+ const [address] = value.split("%");
1470
+ const hextets = expandIpv6(address ?? "");
1471
+ if (hextets === void 0) return value;
1472
+ return `${hextets.slice(0, 4).join(":")}::/64`;
1473
+ }
1474
+ function expandIpv6(value) {
1475
+ const parts = value.split("::");
1476
+ if (parts.length > 2 || value.length === 0) return void 0;
1477
+ const toHextets = (segment) => {
1478
+ if (segment === "") return [];
1479
+ const groups = [];
1480
+ for (const group of segment.split(":")) {
1481
+ if (/^[0-9a-f]{1,4}$/.test(group)) {
1482
+ groups.push(group.replace(/^0+(?=.)/, ""));
1483
+ } else if (/^\d{1,3}(\.\d{1,3}){3}$/.test(group)) {
1484
+ const octets = group.split(".").map(Number);
1485
+ if (octets.some((octet) => octet > 255)) return void 0;
1486
+ groups.push(
1487
+ (octets[0] << 8 | octets[1]).toString(16),
1488
+ (octets[2] << 8 | octets[3]).toString(16)
1489
+ );
1490
+ } else {
1491
+ return void 0;
1492
+ }
1493
+ }
1494
+ return groups;
1495
+ };
1496
+ const head = toHextets(parts[0] ?? "");
1497
+ const tail = parts.length === 2 ? toHextets(parts[1] ?? "") : [];
1498
+ if (head === void 0 || tail === void 0) return void 0;
1499
+ if (parts.length === 1) return head.length === 8 ? head : void 0;
1500
+ const missing = 8 - head.length - tail.length;
1501
+ if (missing < 1) return void 0;
1502
+ return [...head, ...Array.from({ length: missing }, () => "0"), ...tail];
1503
+ }
1504
+ function tooManyAttempts(message) {
1505
+ return new HttpError(429, "RATE_LIMITED", message, {
1506
+ retryable: true,
1507
+ retryAfterSeconds: 60
1508
+ });
1509
+ }
1510
+
1511
+ // src/handler.ts
1512
+ function createHttpHandler(options) {
1513
+ if (options?.service === void 0) throw new TypeError("HTTP handler requires service.");
1514
+ if (options.authorize === void 0) {
1515
+ throw new TypeError("HTTP handler requires authorize; authentication belongs to the host.");
1516
+ }
1517
+ if (options.host === void 0) throw new TypeError("HTTP handler requires host.");
1518
+ if (options.rateLimiting !== void 0 && options.rateLimiting !== false && options.rateLimitHook !== void 0) {
1519
+ throw new TypeError("Pass either rateLimiting or a custom rateLimitHook, not both.");
1520
+ }
1521
+ const rateLimit = options.rateLimitHook ?? resolveRateLimiting(options);
1522
+ const reconcile = options.opportunisticReconcile === false ? void 0 : typeof options.opportunisticReconcile === "object" ? options.opportunisticReconcile : {};
1523
+ if (reconcile !== void 0 && typeof options.host.payments.claimReconcileGate !== "function") {
1524
+ throw new TypeError(
1525
+ "Opportunistic reconcile (on by default) requires payments.claimReconcileGate \u2014 a durable compare-and-set gate shared by every worker (the built-in SQL repository implements it over openreceive_meta). Implement it on the custom repository, or pass opportunisticReconcile: false and run your own settlement worker."
1526
+ );
1527
+ }
1528
+ const rawExtractClientIp = (typeof options.rateLimiting === "object" ? options.rateLimiting.ip : void 0) ?? resolveClientIp;
1529
+ const extractClientIp = (context) => {
1530
+ const ip = rawExtractClientIp(context);
1531
+ return ip === void 0 || ip.length === 0 ? void 0 : clientIpBucket(ip);
1532
+ };
1533
+ const runtime = {
1534
+ ...options,
1535
+ ...rateLimit === void 0 ? {} : { rateLimit },
1536
+ extractClientIp,
1537
+ reconcile,
1538
+ paymentMethods: /* @__PURE__ */ new Map(),
1539
+ prefix: normalizePrefix(options.prefix ?? "/openreceive")
1540
+ };
1541
+ const handle = async (request, extras) => {
1542
+ const requestId = createRequestId();
1543
+ try {
1544
+ return await dispatch(runtime, request, requestId, extras?.native);
1545
+ } catch (error) {
1546
+ return errorResponse(error, requestId);
1547
+ }
1548
+ };
1549
+ const handler = handle;
1550
+ Object.defineProperties(handler, {
1551
+ prefix: { value: runtime.prefix, enumerable: true },
1552
+ handle: { value: handle, enumerable: true }
1553
+ });
1554
+ return handler;
1555
+ }
1556
+ async function dispatch(runtime, request, requestId, native) {
1557
+ const url = new URL(request.url);
1558
+ const route = matchRoute(runtime.prefix, request.method, url.pathname);
1559
+ if (route === null)
1560
+ throw new HttpError(404, "NOT_FOUND", "No OpenReceive route matched this method and path.");
1561
+ if (route.kind === "rates") {
1562
+ const currencies = ratesCurrencies(url.searchParams.get("currencies"));
1563
+ return jsonResponse(
1564
+ 200,
1565
+ await runtime.service.listRates(currencies === void 0 ? void 0 : { currencies }),
1566
+ requestId
1567
+ );
1568
+ }
1569
+ const body = await readJsonBody(request);
1570
+ rejectPayerAmount(body);
1571
+ assertDeclaredFields(route.kind, body);
1572
+ const reference = requiredString(body.reference, "reference", MAX_REFERENCE_LENGTH);
1573
+ const reconcilePass = runtime.reconcile === void 0 ? void 0 : await maybeReconcilePayments({
1574
+ service: runtime.service,
1575
+ host: runtime.host,
1576
+ // `service`/`host` stay outside compact: it is recursive, and they
1577
+ // are handles whose identity the reconcile pass relies on.
1578
+ ...compact2({
1579
+ minIntervalSeconds: runtime.reconcile.minIntervalSeconds,
1580
+ clock: runtime.clock
1581
+ })
1582
+ });
1583
+ if (route.kind === "checkout.prepare") {
1584
+ await authorizeAndRateLimit(runtime, "checkout.prepare", request, { reference }, native);
1585
+ const resolved2 = await resolveHostCheckout(
1586
+ runtime,
1587
+ "checkout.prepare",
1588
+ request,
1589
+ reference,
1590
+ body
1591
+ );
1592
+ const prepared = await runtime.service.prepareCheckout({
1593
+ amount: requireResolvedAmount(resolved2)
1594
+ });
1595
+ const swapOptions = await runtime.service.listSwapOptions({
1596
+ amountMsats: prepared.amountMsats
1597
+ });
1598
+ return jsonResponse(
1599
+ 200,
1600
+ {
1601
+ reference,
1602
+ amount_msats: prepared.amountMsats,
1603
+ fiat_quote: prepared.fiatQuote === null ? null : toSnakeCase(prepared.fiatQuote),
1604
+ payment_methods: toSnakeCase(swapOptions.options)
1605
+ },
1606
+ requestId
1607
+ );
1608
+ }
1609
+ if (route.kind === "checkout.create") {
1610
+ await enforceAuthorize(runtime, "checkout.create", request, { reference }, native);
1611
+ const resolved2 = await resolveHostCheckout(
1612
+ runtime,
1613
+ "checkout.create",
1614
+ request,
1615
+ reference,
1616
+ body
1617
+ );
1618
+ const checkout = await commitNewAttempt(
1619
+ runtime,
1620
+ "checkout.create",
1621
+ request,
1622
+ reference,
1623
+ resolved2,
1624
+ native,
1625
+ {
1626
+ mint: () => runtime.service.createCheckout({
1627
+ reference,
1628
+ amount: requireResolvedAmount(resolved2),
1629
+ ...optionalCheckoutFields(body)
1630
+ }),
1631
+ recover: () => committedCheckout(reference, resolved2),
1632
+ attempt: (minted) => ({ paymentHash: minted.paymentHash, checkout: minted })
1633
+ }
1634
+ );
1635
+ return jsonResponse(201, { checkout: httpCheckout(checkout) }, requestId);
1636
+ }
1637
+ if (route.kind === "payment.check") {
1638
+ const requestedPaymentHash2 = requiredPaymentHash(
1639
+ requiredString(body.payment_hash, "payment_hash")
1640
+ );
1641
+ await authorizeAndRateLimit(
1642
+ runtime,
1643
+ "payment.check",
1644
+ request,
1645
+ { reference, paymentHash: requestedPaymentHash2 },
1646
+ native
1647
+ );
1648
+ const resolved2 = await resolveHostCheckout(runtime, "payment.check", request, reference, body);
1649
+ const paymentHash2 = selectedPaymentHash(resolved2, requestedPaymentHash2);
1650
+ const fromPass = reconcilePass !== void 0 && reconcilePass.reason === "ran" ? reconcilePass.checks.find(
1651
+ (check) => check.paymentHash.toLowerCase() === paymentHash2.toLowerCase()
1652
+ ) : void 0;
1653
+ const checkedBody = fromPass !== void 0 && fromPass.status !== "not_found" ? paymentCheckFromReconcilePass(fromPass) : await paymentCheckFromStoredAttempt(
1654
+ runtime.host.payments,
1655
+ reference,
1656
+ paymentHash2,
1657
+ resolved2.attemptStatus
1658
+ );
1659
+ const checkout = requireResolvedCheckout(resolved2);
1660
+ return jsonResponse(
1661
+ 200,
1662
+ {
1663
+ ...checkedBody,
1664
+ payment_methods: await checkPaymentMethods(runtime, checkout.amountMsats)
1665
+ },
1666
+ requestId
1667
+ );
1668
+ }
1669
+ if (route.kind === "swap.quote") {
1670
+ const payInAsset = requiredString(body.pay_in_asset, "pay_in_asset");
1671
+ await authorizeAndRateLimit(runtime, "swap.quote", request, { reference }, native);
1672
+ const resolved2 = await resolveHostCheckout(
1673
+ runtime,
1674
+ "swap.quote",
1675
+ request,
1676
+ reference,
1677
+ body,
1678
+ payInAsset
1679
+ );
1680
+ return jsonResponse(
1681
+ 200,
1682
+ toSnakeCase(
1683
+ await runtime.service.quoteSwap({
1684
+ amount: requireResolvedAmount(resolved2),
1685
+ payInAsset
1686
+ })
1687
+ ),
1688
+ requestId
1689
+ );
1690
+ }
1691
+ if (route.kind === "swap.create") {
1692
+ const payInAsset = requiredString(body.pay_in_asset, "pay_in_asset");
1693
+ await enforceAuthorize(runtime, "swap.create", request, { reference }, native);
1694
+ const resolved2 = await resolveHostCheckout(
1695
+ runtime,
1696
+ "swap.create",
1697
+ request,
1698
+ reference,
1699
+ body,
1700
+ payInAsset
1701
+ );
1702
+ const swap = await commitNewAttempt(
1703
+ runtime,
1704
+ "swap.create",
1705
+ request,
1706
+ reference,
1707
+ resolved2,
1708
+ native,
1709
+ {
1710
+ mint: () => runtime.service.createSwap({
1711
+ reference,
1712
+ amount: requireResolvedAmount(resolved2),
1713
+ payInAsset,
1714
+ ...optionalCheckoutFields(body)
1715
+ }),
1716
+ recover: () => recoverCommittedSwap(runtime, reference, resolved2),
1717
+ attempt: (minted) => ({
1718
+ paymentHash: minted.paymentHash,
1719
+ checkout: minted.checkout,
1720
+ swapData: minted.swapData
1721
+ })
1722
+ }
1723
+ );
1724
+ return jsonResponse(201, { swap: httpSwap(swap) }, requestId);
1725
+ }
1726
+ const action = route.kind === "swap.read" ? "swap.read" : "swap.refund";
1727
+ const requestedPaymentHash = requiredPaymentHash(
1728
+ requiredString(body.payment_hash, "payment_hash")
1729
+ );
1730
+ await authorizeAndRateLimit(
1731
+ runtime,
1732
+ action,
1733
+ request,
1734
+ { reference, paymentHash: requestedPaymentHash },
1735
+ native
1736
+ );
1737
+ const resolved = await resolveHostCheckout(runtime, action, request, reference, body);
1738
+ const swapData = requireResolvedSwapData(resolved.swapData);
1739
+ const paymentHash = selectedPaymentHash(resolved, requestedPaymentHash);
1740
+ if (route.kind === "swap.read") {
1741
+ return jsonResponse(
1742
+ 200,
1743
+ toSnakeCase(
1744
+ await runtime.service.getSwap({
1745
+ reference,
1746
+ paymentHash,
1747
+ swapData
1748
+ })
1749
+ ),
1750
+ requestId
1751
+ );
1752
+ }
1753
+ const refundAddress = requiredString(body.refund_address, "refund_address");
1754
+ return jsonResponse(
1755
+ 200,
1756
+ toSnakeCase(
1757
+ await runtime.service.refundSwap({
1758
+ reference,
1759
+ paymentHash,
1760
+ swapData,
1761
+ refundAddress
1762
+ })
1763
+ ),
1764
+ requestId
1765
+ );
1766
+ }
1767
+ async function commitNewAttempt(runtime, action, request, reference, resolved, native, arms) {
1768
+ if (resolved.paymentHash !== void 0) return arms.recover();
1769
+ await enforceRateLimit(runtime, action, request, { reference }, native);
1770
+ const minted = await arms.mint();
1771
+ const clientIp = runtime.extractClientIp({ action, request, resource: { reference }, native });
1772
+ await persistCheckoutAttempt(runtime, {
1773
+ reference,
1774
+ ...arms.attempt(minted),
1775
+ ...clientIp === void 0 ? {} : { clientIp }
1776
+ });
1777
+ return minted;
1778
+ }
1779
+ async function resolveHostCheckout(runtime, action, request, reference, input, payInAsset) {
1780
+ return runtime.host.resolveCheckout({
1781
+ action,
1782
+ request,
1783
+ reference,
1784
+ ...payInAsset === void 0 ? {} : { payInAsset },
1785
+ input
1786
+ });
1787
+ }
1788
+ function resolveRateLimiting(options) {
1789
+ if (options.rateLimiting === void 0 || options.rateLimiting === false) return void 0;
1790
+ const config = options.rateLimiting === true ? {} : options.rateLimiting;
1791
+ const payments = options.host.payments;
1792
+ const countAttemptsFromIp = config.countAttemptsFromIp ?? payments.countAttemptsFromIp?.bind(payments);
1793
+ return createIpRateLimit({
1794
+ ...config,
1795
+ ...countAttemptsFromIp === void 0 ? {} : { countAttemptsFromIp }
1796
+ });
1797
+ }
1798
+ async function authorizeAndRateLimit(runtime, action, request, resource, native) {
1799
+ await enforceRateLimit(runtime, action, request, resource, native);
1800
+ await enforceAuthorize(runtime, action, request, resource, native);
1801
+ }
1802
+ async function enforceRateLimit(runtime, action, request, resource, native) {
1803
+ if (runtime.rateLimit === void 0) return;
1804
+ if (!await runtime.rateLimit({ action, request, resource, native })) {
1805
+ throw new HttpError(429, "RATE_LIMITED", "Too many requests.", {
1806
+ retryable: true,
1807
+ retryAfterSeconds: 60
1808
+ });
1809
+ }
1810
+ }
1811
+ async function enforceAuthorize(runtime, action, request, resource, native) {
1812
+ if (!await runtime.authorize({ action, request, resource, native })) {
1813
+ throw new HttpError(403, "FORBIDDEN", "Not authorized for this action.");
1814
+ }
1815
+ }
1816
+ async function persistCheckoutAttempt(runtime, input) {
1817
+ try {
1818
+ await runtime.host.onCheckoutCreated(input);
1819
+ } catch (error) {
1820
+ if (error instanceof HttpError || isServiceErrorShape(error)) throw error;
1821
+ throw new HttpError(
1822
+ 503,
1823
+ "INTERNAL",
1824
+ "The host could not persist this payment attempt; payer instructions were withheld. Please retry.",
1825
+ { retryable: true }
1826
+ );
1827
+ }
1828
+ }
1829
+ var PAYMENT_METHODS_CACHE_SECONDS = 60;
1830
+ var PAYMENT_METHODS_CACHE_MAX_ENTRIES = 256;
1831
+ async function checkPaymentMethods(runtime, amountMsats) {
1832
+ const now = (runtime.clock ?? unixSeconds7)();
1833
+ const cached = runtime.paymentMethods.get(amountMsats);
1834
+ if (cached !== void 0 && now - cached.at < PAYMENT_METHODS_CACHE_SECONDS) {
1835
+ return cached.methods;
1836
+ }
1837
+ const swapOptions = await runtime.service.listSwapOptions({ amountMsats });
1838
+ const methods = toSnakeCase(swapOptions.options);
1839
+ if (runtime.paymentMethods.size >= PAYMENT_METHODS_CACHE_MAX_ENTRIES) {
1840
+ runtime.paymentMethods.clear();
1841
+ }
1842
+ runtime.paymentMethods.set(amountMsats, { at: now, methods });
1843
+ return methods;
1844
+ }
1845
+ function selectedPaymentHash(resolved, requestedPaymentHash) {
1846
+ const selected = hostPaymentHash(resolved.paymentHash);
1847
+ if (selected !== requestedPaymentHash) {
1848
+ throw new HttpError(
1849
+ 404,
1850
+ "NOT_FOUND",
1851
+ "The selected payment attempt does not belong to this order."
1852
+ );
1853
+ }
1854
+ return selected;
1855
+ }
1856
+ function hostPaymentHash(value) {
1857
+ if (typeof value === "string") {
1858
+ const hash = value.trim().toLowerCase();
1859
+ if (/^[0-9a-f]{64}$/.test(hash)) return hash;
1860
+ }
1861
+ throw new HttpError(
1862
+ 500,
1863
+ "INTERNAL",
1864
+ "The host resolver returned a missing or malformed payment hash for this reference."
1865
+ );
1866
+ }
1867
+ function requireResolvedAmount(value) {
1868
+ if (value.amount === void 0 || value.amount === null) {
1869
+ throw new HttpError(500, "INTERNAL", "The host resolved this order without an amount.");
1870
+ }
1871
+ return value.amount;
1872
+ }
1873
+ function requireResolvedCheckout(value) {
1874
+ if (value.checkout === void 0) {
1875
+ throw new HttpError(409, "CONFLICT", "The host payment attempt has no checkout snapshot.");
1876
+ }
1877
+ return value.checkout;
1878
+ }
1879
+ function requireResolvedSwapData(value) {
1880
+ if (value === void 0) {
1881
+ throw new HttpError(404, "NOT_FOUND", "The host order has no swap data.");
1882
+ }
1883
+ return value;
1884
+ }
1885
+ function committedCheckout(reference, resolved) {
1886
+ const paymentHash = hostPaymentHash(resolved.paymentHash);
1887
+ const checkout = requireResolvedCheckout(resolved);
1888
+ if (checkout.reference !== reference || checkout.paymentHash.toLowerCase() !== paymentHash) {
1889
+ throw new HttpError(
1890
+ 409,
1891
+ "CONFLICT",
1892
+ "The selected payment attempt is not a reusable pending checkout."
1893
+ );
1894
+ }
1895
+ return structuredClone(checkout);
1896
+ }
1897
+ async function recoverCommittedSwap(runtime, reference, resolved) {
1898
+ const paymentHash = hostPaymentHash(resolved.paymentHash);
1899
+ const swapData = requireResolvedSwapData(resolved.swapData);
1900
+ const status = await runtime.service.getSwap({ reference, paymentHash, swapData });
1901
+ const checkout = committedCheckout(reference, resolved);
1902
+ return { ...status, checkout, swapData };
1903
+ }
1904
+
1905
+ // src/notifications.ts
1906
+ import {
1907
+ classifyTransactionSettlement,
1908
+ OpenReceiveError,
1909
+ unixSeconds as unixSeconds8
1910
+ } from "@openreceive/core";
1911
+ async function startNotificationListener(input) {
1912
+ const subscribe = input.service.subscribeWalletNotifications?.bind(input.service);
1913
+ if (subscribe === void 0) {
1914
+ throw new OpenReceiveError({
1915
+ code: "UNSUPPORTED_METHOD",
1916
+ message: "OpenReceive service does not support wallet notifications (subscribeWalletNotifications). Keep polling reconciliation.",
1917
+ retryable: false
1918
+ });
1919
+ }
1920
+ if (typeof input.host.payments.claimReconcileGate !== "function") {
1921
+ throw new TypeError(
1922
+ "The notification listener requires payments.claimReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
1923
+ );
1924
+ }
1925
+ const reportError = (error) => {
1926
+ if (input.onError === void 0) {
1927
+ warnFailure(
1928
+ "payment.notification.failed",
1929
+ "notification listener failed (polling remains the safety net)",
1930
+ error
1931
+ );
1932
+ return;
1933
+ }
1934
+ try {
1935
+ input.onError(error);
1936
+ } catch {
1937
+ }
1938
+ };
1939
+ let stopped = false;
1940
+ let running = false;
1941
+ let queued = false;
1942
+ let inFlight = Promise.resolve();
1943
+ let settling = Promise.resolve();
1944
+ const wakeReconciliation = () => {
1945
+ if (stopped) return;
1946
+ if (running) {
1947
+ queued = true;
1948
+ return;
1949
+ }
1950
+ running = true;
1951
+ inFlight = (async () => {
1952
+ try {
1953
+ do {
1954
+ queued = false;
1955
+ await maybeReconcilePayments({
1956
+ service: input.service,
1957
+ host: input.host,
1958
+ onError: reportError,
1959
+ ...input.overlapSeconds === void 0 ? {} : { overlapSeconds: input.overlapSeconds }
1960
+ });
1961
+ } while (queued && !stopped);
1962
+ } finally {
1963
+ running = false;
1964
+ }
1965
+ })();
1966
+ };
1967
+ const isPendingAttempt = async (paymentHash) => {
1968
+ const payments = input.host.payments;
1969
+ if (payments.findPendingAttempt !== void 0) {
1970
+ return await payments.findPendingAttempt(paymentHash) !== void 0;
1971
+ }
1972
+ const attempts = await payments.listReconcilableAttempts();
1973
+ return attempts.some((attempt) => attempt.paymentHash.toLowerCase() === paymentHash);
1974
+ };
1975
+ const settleDirectly = async (transaction, notifiedHash) => {
1976
+ if (stopped) return;
1977
+ try {
1978
+ const detection = classifyTransactionSettlement(transaction);
1979
+ const paymentHash = (transaction.payment_hash ?? notifiedHash)?.toLowerCase();
1980
+ if (!detection.settled || paymentHash === void 0) {
1981
+ wakeReconciliation();
1982
+ return;
1983
+ }
1984
+ if (!await isPendingAttempt(paymentHash)) {
1985
+ wakeReconciliation();
1986
+ return;
1987
+ }
1988
+ const observedAt = unixSeconds8();
1989
+ await input.host.onPaid({
1990
+ paymentHash,
1991
+ paidAt: transaction.settled_at ?? observedAt,
1992
+ details: {
1993
+ transaction,
1994
+ observed_at: observedAt,
1995
+ paid_at_source: transaction.settled_at === void 0 ? "observed_at" : "settled_at"
1996
+ }
1997
+ });
1998
+ } catch (error) {
1999
+ reportError(error);
2000
+ wakeReconciliation();
2001
+ }
2002
+ };
2003
+ const unsubscribe = await subscribe((notification) => {
2004
+ if (notification.type !== "payment_received") return;
2005
+ const transaction = notification.transaction;
2006
+ if (transaction === void 0) {
2007
+ wakeReconciliation();
2008
+ return;
2009
+ }
2010
+ settling = settling.then(() => settleDirectly(transaction, notification.payment_hash));
2011
+ });
2012
+ return {
2013
+ async stop() {
2014
+ if (stopped) return;
2015
+ stopped = true;
2016
+ try {
2017
+ await unsubscribe();
2018
+ } catch (error) {
2019
+ reportError(error);
2020
+ }
2021
+ await settling;
2022
+ await inFlight;
2023
+ }
2024
+ };
2025
+ }
2026
+ async function startNotificationWorker(input) {
2027
+ const reconciler = await startReconciler({
2028
+ service: input.service,
2029
+ host: input.host,
2030
+ pollIntervalMs: input.pollIntervalMs ?? 15e3,
2031
+ ...input.overlapSeconds === void 0 ? {} : { overlapSeconds: input.overlapSeconds },
2032
+ ...input.onError === void 0 ? {} : { onError: input.onError }
2033
+ });
2034
+ let listener;
2035
+ try {
2036
+ listener = await startNotificationListener({
2037
+ service: input.service,
2038
+ host: input.host,
2039
+ ...input.overlapSeconds === void 0 ? {} : { overlapSeconds: input.overlapSeconds },
2040
+ ...input.onError === void 0 ? {} : { onError: input.onError }
2041
+ });
2042
+ } catch (error) {
2043
+ if (error instanceof OpenReceiveError && error.code === "UNSUPPORTED_METHOD") {
2044
+ if (input.onError === void 0) {
2045
+ warnFailure(
2046
+ "payment.notification.unsupported",
2047
+ "wallet lacks NWC notifications; running the periodic pass alone",
2048
+ error
2049
+ );
2050
+ } else {
2051
+ input.onError(error);
2052
+ }
2053
+ } else {
2054
+ reconciler.stop();
2055
+ await reconciler.done;
2056
+ throw error;
2057
+ }
2058
+ }
2059
+ return {
2060
+ async stop() {
2061
+ await listener?.stop();
2062
+ reconciler.stop();
2063
+ await reconciler.done;
2064
+ },
2065
+ done: reconciler.done
2066
+ };
2067
+ }
2068
+
2069
+ // src/orm-adapters.ts
2070
+ function knexDb(knex, dialect) {
2071
+ const queryOn = (executor) => async (sql, params = []) => {
2072
+ const result = await executor.raw(sql, [...params]);
2073
+ return dialect === "sqlite" ? result : result.rows ?? [];
2074
+ };
2075
+ return {
2076
+ dialect,
2077
+ query: queryOn(knex),
2078
+ transaction: (run) => knex.transaction((trx) => run({ query: queryOn(trx) }))
2079
+ };
2080
+ }
2081
+ function prismaStatementReturnsRows(sql) {
2082
+ return /^\s*(?:select|values|with)\b/i.test(sql) || /\breturning\b/i.test(sql);
2083
+ }
2084
+ function prismaDb(prisma, dialect) {
2085
+ const queryOn = (tx) => async (sql, params = []) => {
2086
+ if (prismaStatementReturnsRows(sql)) {
2087
+ return await tx.$queryRawUnsafe(sql, ...params);
2088
+ }
2089
+ await tx.$executeRawUnsafe(sql, ...params);
2090
+ return [];
2091
+ };
2092
+ return {
2093
+ dialect,
2094
+ query: queryOn(prisma),
2095
+ transaction: (run) => prisma.$transaction((tx) => run({ query: queryOn(tx) }))
2096
+ };
2097
+ }
2098
+ function typeOrmDb(dataSource, dialect) {
2099
+ const queryOn = (runner) => async (sql, params = []) => await runner.query(sql, [...params]) ?? [];
2100
+ return {
2101
+ dialect,
2102
+ query: queryOn(dataSource),
2103
+ transaction: (run) => dataSource.transaction((manager) => run({ query: queryOn(manager) }))
2104
+ };
2105
+ }
2106
+ function sequelizeDb(sequelize, dialect) {
2107
+ const queryOn = (transaction) => async (sql, params = []) => {
2108
+ const result = await sequelize.query(sql, {
2109
+ bind: [...params],
2110
+ ...transaction === void 0 ? {} : { transaction }
2111
+ });
2112
+ const rows = Array.isArray(result) ? result[0] : void 0;
2113
+ return Array.isArray(rows) ? rows : [];
2114
+ };
2115
+ return {
2116
+ dialect,
2117
+ query: queryOn(),
2118
+ transaction: (run) => sequelize.transaction((transaction) => run({ query: queryOn(transaction) }))
2119
+ };
2120
+ }
2121
+
2122
+ // src/stack.ts
2123
+ import { compact as compact3 } from "@openreceive/core";
2124
+ import { createOpenReceive } from "@openreceive/node";
2125
+ function createStack(options) {
2126
+ const { wallet, storage, amountFor, clock, onBootFailure, ...handlerOptions } = options;
2127
+ const host = createHost({
2128
+ ...storage,
2129
+ amountFor,
2130
+ ...compact3({ clock })
2131
+ });
2132
+ const prefix = normalizePrefix(options.prefix ?? "/openreceive");
2133
+ let ownedService;
2134
+ const boot = (async () => {
2135
+ const resolved = wallet.service !== void 0 ? await wallet.service : await createOpenReceive({ nwc: wallet.nwc });
2136
+ if (wallet.service === void 0) ownedService = resolved;
2137
+ return createHttpHandler({
2138
+ ...handlerOptions,
2139
+ ...compact3({ clock }),
2140
+ service: resolved,
2141
+ host
2142
+ });
2143
+ })();
2144
+ boot.catch((error) => {
2145
+ const message = `OpenReceive stack failed to start: ${error instanceof Error ? error.message : String(error)}`;
2146
+ if (onBootFailure === void 0) console.error(message);
2147
+ else onBootFailure(message);
2148
+ });
2149
+ const handle = async (request, extras) => {
2150
+ let booted;
2151
+ try {
2152
+ booted = await boot;
2153
+ } catch {
2154
+ return errorResponse(
2155
+ new HttpError(
2156
+ 503,
2157
+ "WALLET_UNAVAILABLE",
2158
+ "The payment service is not available. Please try again shortly.",
2159
+ { retryable: true }
2160
+ ),
2161
+ createRequestId()
2162
+ );
2163
+ }
2164
+ return booted(request, extras);
2165
+ };
2166
+ const handler = handle;
2167
+ Object.defineProperties(handler, {
2168
+ prefix: { value: prefix, enumerable: true },
2169
+ handle: { value: handle, enumerable: true }
2170
+ });
2171
+ return {
2172
+ handler,
2173
+ ready: boot.then(() => void 0),
2174
+ async close() {
2175
+ await boot.catch(() => {
2176
+ });
2177
+ await ownedService?.close();
2178
+ }
2179
+ };
2180
+ }
2181
+ function isStackOptions(options) {
2182
+ if (options.host !== void 0) return false;
2183
+ const flat = options;
2184
+ if (flat.wallet !== void 0 || flat.storage !== void 0 || flat.amountFor !== void 0) {
2185
+ return true;
2186
+ }
2187
+ throw new TypeError(
2188
+ "OpenReceive composed options require host: pass { service, host, authorize }, or use the all-in-one form with wallet/storage/amountFor."
2189
+ );
2190
+ }
2191
+
2192
+ export {
2193
+ HttpError,
2194
+ isServiceErrorShape,
2195
+ HostError,
2196
+ hostError,
2197
+ mapHostRouteError,
2198
+ createRequestId,
2199
+ jsonResponse,
2200
+ errorResponse,
2201
+ webRequest,
2202
+ isUnderPrefix,
2203
+ OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS,
2204
+ OPENRECEIVE_RECONCILE_BATCH_SIZE,
2205
+ paymentsSchemaSql,
2206
+ createSqlPayments,
2207
+ OPENRECEIVE_PAYMENTS_SCHEMA_VERSION2 as OPENRECEIVE_PAYMENTS_SCHEMA_VERSION,
2208
+ createHost,
2209
+ reconcileHostPayments,
2210
+ startReconciler,
2211
+ OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS,
2212
+ OPENRECEIVE_RECONCILE_SCAN_TIMEOUT_MS,
2213
+ OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES,
2214
+ reconcileIntervalSeconds,
2215
+ maybeReconcilePayments,
2216
+ createIpRateLimit,
2217
+ createProxyRateLimitingConfig,
2218
+ resolveClientIp,
2219
+ createHttpHandler,
2220
+ startNotificationListener,
2221
+ startNotificationWorker,
2222
+ knexDb,
2223
+ prismaDb,
2224
+ typeOrmDb,
2225
+ sequelizeDb,
2226
+ createStack,
2227
+ isStackOptions
2228
+ };