@openreceive/http 0.3.2 → 0.3.3
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/dist/adapter-surface.js +1 -1
- package/dist/{chunk-EZAV3TC3.js → chunk-YASZ4HDW.js} +55 -18
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/skills/debug-openreceive-payment/SKILL.md +88 -0
- package/skills/integrate-openreceive/SKILL.md +116 -0
- package/skills/integrate-openreceive/references/node.md +480 -0
- package/skills/integrate-openreceive/references/rails.md +568 -0
package/dist/adapter-surface.js
CHANGED
|
@@ -464,6 +464,14 @@ var RECONCILE_GATE_CAS_RETRIES = 6;
|
|
|
464
464
|
var META_CLOCK_SKEW_SECONDS = 60;
|
|
465
465
|
var SCHEMA_VERSION_META_KEY = "schema_version";
|
|
466
466
|
var OPENRECEIVE_RECONCILE_BATCH_SIZE = 200;
|
|
467
|
+
function isMissingTableError(error, dialect) {
|
|
468
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
469
|
+
if (dialect === "postgres") {
|
|
470
|
+
const code = error?.code;
|
|
471
|
+
return code === "42P01" || /relation .+ does not exist/i.test(message);
|
|
472
|
+
}
|
|
473
|
+
return /no such table/i.test(message);
|
|
474
|
+
}
|
|
467
475
|
function parseClaimedAt(value) {
|
|
468
476
|
try {
|
|
469
477
|
const parsed = JSON.parse(String(value));
|
|
@@ -511,8 +519,11 @@ function createSqlPayments(db, options = {}) {
|
|
|
511
519
|
);
|
|
512
520
|
const value = rows[0]?.value;
|
|
513
521
|
stored = value === void 0 ? void 0 : Number(asString(value, "value"));
|
|
514
|
-
} catch {
|
|
515
|
-
return;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (!isMissingTableError(error, adapter.dialect)) return;
|
|
524
|
+
throw new TypeError(
|
|
525
|
+
`The ${metaTable} table does not exist \u2014 the OpenReceive tables have not been migrated in this database. Run \`npx openreceive scaffold payments --orm <your orm>\` and apply the emitted migration through your normal workflow (or execute paymentsSchemaSql(dialect) directly for bare drivers). https://openreceive.org/guides/storage.md`
|
|
526
|
+
);
|
|
516
527
|
}
|
|
517
528
|
if (stored === void 0 || !Number.isInteger(stored)) return;
|
|
518
529
|
if (stored > OPENRECEIVE_PAYMENTS_SCHEMA_VERSION) {
|
|
@@ -785,11 +796,13 @@ function warnFailure(event, prefix, error) {
|
|
|
785
796
|
}
|
|
786
797
|
function createHost(options) {
|
|
787
798
|
if (options?.amountFor === void 0) {
|
|
788
|
-
throw new TypeError(
|
|
799
|
+
throw new TypeError(
|
|
800
|
+
'OpenReceive host requires amountFor \u2014 the host owns prices. Pass amountFor(reference) returning the amount to charge (for example { currency: "USD", value: "9.99" }), or null for an unknown reference. https://openreceive.org/guides/api-reference.md#createhost'
|
|
801
|
+
);
|
|
789
802
|
}
|
|
790
803
|
if (options.onPaid === void 0) {
|
|
791
804
|
throw new TypeError(
|
|
792
|
-
"OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw settlement event in custom repository mode)."
|
|
805
|
+
"OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw settlement event in custom repository mode). Pass onPaid to fulfill the order \u2014 it runs exactly once per reference. https://openreceive.org/guides/api-reference.md#onpaid"
|
|
793
806
|
);
|
|
794
807
|
}
|
|
795
808
|
let payments;
|
|
@@ -806,20 +819,28 @@ function createHost(options) {
|
|
|
806
819
|
};
|
|
807
820
|
} else {
|
|
808
821
|
if (options.payments?.listForReference === void 0) {
|
|
809
|
-
throw new TypeError(
|
|
822
|
+
throw new TypeError(
|
|
823
|
+
"OpenReceive host requires db or payments.listForReference. Pass db (your database handle; the library owns the openreceive tables in it), or a complete custom PaymentRepository. https://openreceive.org/guides/storage.md"
|
|
824
|
+
);
|
|
810
825
|
}
|
|
811
826
|
if (options.payments.commitAttempt === void 0) {
|
|
812
|
-
throw new TypeError(
|
|
827
|
+
throw new TypeError(
|
|
828
|
+
"OpenReceive host requires payments.commitAttempt \u2014 the attempt write committed before payer instructions are exposed. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
829
|
+
);
|
|
813
830
|
}
|
|
814
831
|
if (options.payments.listReconcilableAttempts === void 0) {
|
|
815
|
-
throw new TypeError(
|
|
832
|
+
throw new TypeError(
|
|
833
|
+
"OpenReceive host requires payments.listReconcilableAttempts \u2014 the pending-attempt batch reconciliation scans. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
834
|
+
);
|
|
816
835
|
}
|
|
817
836
|
if (options.payments.recordReconciliation === void 0) {
|
|
818
|
-
throw new TypeError(
|
|
837
|
+
throw new TypeError(
|
|
838
|
+
"OpenReceive host requires payments.recordReconciliation \u2014 how a scan outcome (settled/expired/attention) is written back. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
839
|
+
);
|
|
819
840
|
}
|
|
820
841
|
if (typeof options.payments.recordSettlement !== "function") {
|
|
821
842
|
throw new TypeError(
|
|
822
|
-
"OpenReceive host requires payments.recordSettlement (the write-once settlement claim)."
|
|
843
|
+
"OpenReceive host requires payments.recordSettlement (the write-once settlement claim). Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
823
844
|
);
|
|
824
845
|
}
|
|
825
846
|
payments = options.payments;
|
|
@@ -1419,7 +1440,7 @@ function createIpRateLimit(config = {}) {
|
|
|
1419
1440
|
const count = config.countAttemptsFromIp;
|
|
1420
1441
|
if (count === void 0) {
|
|
1421
1442
|
throw new TypeError(
|
|
1422
|
-
"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."
|
|
1443
|
+
"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. https://openreceive.org/guides/rate-limiting.md"
|
|
1423
1444
|
);
|
|
1424
1445
|
}
|
|
1425
1446
|
const message = config.message ?? DEFAULT_MESSAGE;
|
|
@@ -1434,7 +1455,7 @@ function createIpRateLimit(config = {}) {
|
|
|
1434
1455
|
if (!warnedUnattributable) {
|
|
1435
1456
|
warnedUnattributable = true;
|
|
1436
1457
|
console.warn(
|
|
1437
|
-
"[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
|
|
1458
|
+
"[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 https://openreceive.org/guides/rate-limiting.md"
|
|
1438
1459
|
);
|
|
1439
1460
|
}
|
|
1440
1461
|
return true;
|
|
@@ -1521,19 +1542,31 @@ function tooManyAttempts(message) {
|
|
|
1521
1542
|
|
|
1522
1543
|
// src/handler.ts
|
|
1523
1544
|
function createHttpHandler(options) {
|
|
1524
|
-
if (options?.service === void 0)
|
|
1545
|
+
if (options?.service === void 0) {
|
|
1546
|
+
throw new TypeError(
|
|
1547
|
+
"HTTP handler requires service: pass the wallet service built by createOpenReceive(). https://openreceive.org/guides/api-reference.md#createopenreceive"
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1525
1550
|
if (options.authorize === void 0) {
|
|
1526
|
-
throw new TypeError(
|
|
1551
|
+
throw new TypeError(
|
|
1552
|
+
"HTTP handler requires authorize; authentication belongs to the host. Pass an authorize hook that checks the payer's session may act on context.resource.reference. https://openreceive.org/guides/authorization.md"
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
if (options.host === void 0) {
|
|
1556
|
+
throw new TypeError(
|
|
1557
|
+
"HTTP handler requires host: build it with createHost({ amountFor, onPaid, db }) and pass it beside service and authorize. https://openreceive.org/guides/api-reference.md#createhost"
|
|
1558
|
+
);
|
|
1527
1559
|
}
|
|
1528
|
-
if (options.host === void 0) throw new TypeError("HTTP handler requires host.");
|
|
1529
1560
|
if (options.rateLimiting !== void 0 && options.rateLimiting !== false && options.rateLimitHook !== void 0) {
|
|
1530
|
-
throw new TypeError(
|
|
1561
|
+
throw new TypeError(
|
|
1562
|
+
"Pass either rateLimiting or a custom rateLimitHook, not both: rateLimiting is the built-in per-IP limiter, rateLimitHook replaces it with your own policy. https://openreceive.org/guides/rate-limiting.md"
|
|
1563
|
+
);
|
|
1531
1564
|
}
|
|
1532
1565
|
const rateLimit = options.rateLimitHook ?? resolveRateLimiting(options);
|
|
1533
1566
|
const reconcile = options.opportunisticReconcile === false ? void 0 : typeof options.opportunisticReconcile === "object" ? options.opportunisticReconcile : {};
|
|
1534
1567
|
if (reconcile !== void 0 && typeof options.host.payments.claimReconcileGate !== "function") {
|
|
1535
1568
|
throw new TypeError(
|
|
1536
|
-
"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."
|
|
1569
|
+
"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. https://openreceive.org/guides/storage.md"
|
|
1537
1570
|
);
|
|
1538
1571
|
}
|
|
1539
1572
|
const rawExtractClientIp = (typeof options.rateLimiting === "object" ? options.rateLimiting.ip : void 0) ?? resolveClientIp;
|
|
@@ -1830,7 +1863,11 @@ async function enforceRateLimit(runtime, action, request, resource, native) {
|
|
|
1830
1863
|
}
|
|
1831
1864
|
async function enforceAuthorize(runtime, action, request, resource, native) {
|
|
1832
1865
|
if (!await runtime.authorize({ action, request, resource, native })) {
|
|
1833
|
-
throw new HttpError(
|
|
1866
|
+
throw new HttpError(
|
|
1867
|
+
403,
|
|
1868
|
+
"FORBIDDEN",
|
|
1869
|
+
"Not authorized for this action. The application's authorize hook denied it; if this is unexpected, check that the payer's session reaches the checkout routes. https://openreceive.org/guides/authorization.md"
|
|
1870
|
+
);
|
|
1834
1871
|
}
|
|
1835
1872
|
}
|
|
1836
1873
|
async function persistCheckoutAttempt(runtime, input) {
|
|
@@ -2205,7 +2242,7 @@ function isStackOptions(options) {
|
|
|
2205
2242
|
return true;
|
|
2206
2243
|
}
|
|
2207
2244
|
throw new TypeError(
|
|
2208
|
-
"OpenReceive composed options require host: pass { service, host, authorize }, or use the all-in-one form with wallet/storage/amountFor."
|
|
2245
|
+
"OpenReceive composed options require host: pass { service, host, authorize } (host built by createHost), or use the all-in-one form with wallet/storage/amountFor. https://openreceive.org/guides/api-reference.md#framework-adapters"
|
|
2209
2246
|
);
|
|
2210
2247
|
}
|
|
2211
2248
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openreceive/http",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Framework-neutral (Web Request/Response) HTTP handler for OpenReceive checkout that builds on @openreceive/node and requires its Node runtime: request routing, host integration, and payment-attempt persistence.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@openreceive/core": "0.3.
|
|
21
|
-
"@openreceive/node": "0.3.
|
|
20
|
+
"@openreceive/core": "0.3.3",
|
|
21
|
+
"@openreceive/node": "0.3.3"
|
|
22
22
|
},
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
|
+
"skills",
|
|
35
36
|
"README.md",
|
|
36
37
|
"LICENSE"
|
|
37
38
|
],
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debug-openreceive-payment
|
|
3
|
+
description: >
|
|
4
|
+
Diagnose a failing OpenReceive integration. Use when an OpenReceive-powered
|
|
5
|
+
checkout misbehaves: the server refuses to boot, checkout routes return 403,
|
|
6
|
+
404, 409, or 5xx, a paid invoice never settles, a swap refund seems
|
|
7
|
+
unreachable, or the checkout UI renders nothing.
|
|
8
|
+
license: MIT
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Debug an OpenReceive payment
|
|
12
|
+
|
|
13
|
+
Work top-down: configuration, then the request, then settlement. Every guide
|
|
14
|
+
URL below is raw markdown — fetch it when the step needs it.
|
|
15
|
+
|
|
16
|
+
## 1. Run the doctor first
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx openreceive doctor # Node version, NWC_URI, swap config, wallet probe
|
|
20
|
+
npx openreceive doctor --db <db> # + are openreceive_payments/openreceive_meta migrated?
|
|
21
|
+
npx openreceive doctor --url http://localhost:3000 # + are the routes actually mounted?
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each failing line states its own fix. `npx openreceive debug-report` prints the
|
|
25
|
+
same diagnostics redacted, always exit 0 — safe to share.
|
|
26
|
+
|
|
27
|
+
## 2. Boot failures
|
|
28
|
+
|
|
29
|
+
| Symptom | Cause and fix |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `MISSING_NWC` / "needs a receive-only NWC code" | `NWC_URI` is not in the server process env. A `.env` file alone is not enough — something must load it (`dotenv/config`, Next auto-load). Get a code: https://openreceive.org/get_a_nwc_code_to_receive_payments |
|
|
32
|
+
| `INVALID_NWC` / "not a valid NWC code" | The value is malformed (must be `nostr+walletconnect://` with 64-hex pubkey and secret, ≥1 `wss` relay). Re-copy it from the wallet. |
|
|
33
|
+
| "NOT receive-only" / spend methods advertised | The wallet minted a spend-capable code; OpenReceive fails closed because a leak would drain the wallet. Mint a receive-only code. Overriding (`allowSpendCapableWallet` / `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC`) is a last resort. |
|
|
34
|
+
| Wallet preflight failed (methods/encryption) | The wallet must advertise `make_invoice` + `list_transactions` and NIP-04 or NIP-44 v2. Use a compatible wallet. |
|
|
35
|
+
| "The openreceive_meta table does not exist" / raw `no such table: openreceive_payments` | The migration was never applied. Node: `npx openreceive scaffold payments --orm <yours>`, then run the emitted migration through the app's normal workflow. Rails: `bin/rails generate openreceive:install`, then `bin/rails db:migrate`. https://openreceive.org/guides/storage.md |
|
|
36
|
+
| "requires amountFor / onPaid / authorize / host" | The factory is missing a required hook — see the host contract in https://openreceive.org/guides/api-reference.md |
|
|
37
|
+
|
|
38
|
+
## 3. Request-time errors from the routes
|
|
39
|
+
|
|
40
|
+
| Status | Meaning | Where to look |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| 403 FORBIDDEN | Your own `authorize` hook denied it, or the request looked cross-site. Check the session/cookie actually reaches the checkout routes. https://openreceive.org/guides/authorization.md |
|
|
43
|
+
| 404 NOT_FOUND | `amountFor` returned `null` (unknown reference), or the `payment_hash` does not belong to that reference. |
|
|
44
|
+
| 409 CONFLICT | **Normal state, not a bug**: the reference already settled, or an unpaid checkout for that method is already live. Show it as order state; never retry-loop. |
|
|
45
|
+
| 503 retryable | The host hook failed while persisting the attempt (instructions withheld), or the wallet is unavailable. Read the server log for the underlying error. |
|
|
46
|
+
| Framework 404 / HTML error page | The router is not mounted, or mounted at a different prefix than the UI's `prefix` prop. `doctor --url` distinguishes these. |
|
|
47
|
+
|
|
48
|
+
## 4. Paid but never settles
|
|
49
|
+
|
|
50
|
+
- Settlement is opportunistic: any OpenReceive request runs one reconcile pass
|
|
51
|
+
through a durable gate (min 2s between wallet scans, stretched by invoice
|
|
52
|
+
age). A quiet server settles on the next request — or run the optional
|
|
53
|
+
notification worker. No timer is missing; that is the design.
|
|
54
|
+
- An unpaid attempt closes only after a successful wallet scan at/after expiry
|
|
55
|
+
plus a 900s grace constant — a local clock alone never closes one. `expired`
|
|
56
|
+
arriving "late" is correct.
|
|
57
|
+
- `onPaid` runs once per reference, first settled attempt only, inside the
|
|
58
|
+
settlement transaction. If your fulfillment did not run, check whether the
|
|
59
|
+
guarded `UPDATE … WHERE` matched zero rows (already transitioned).
|
|
60
|
+
https://openreceive.org/guides/storage.md
|
|
61
|
+
|
|
62
|
+
## 5. Swaps and refunds
|
|
63
|
+
|
|
64
|
+
- A deposit that arrives short or late becomes `refund_required`; the payer
|
|
65
|
+
claims it on a second visit. That needs a per-order URL you serve
|
|
66
|
+
(`/checkout/:reference`, `syncUrl` on the drop-ins). Keep the
|
|
67
|
+
`payment_hash`: `POST /swaps/status` reopens the attempt with no expiry
|
|
68
|
+
window, while re-picking the coin mints a new deposit after ~30 minutes.
|
|
69
|
+
- Refunds exist only for swap deposits from `refund_required`. There is **no
|
|
70
|
+
Lightning refund** — the wallet cannot spend. Do not chase one.
|
|
71
|
+
https://openreceive.org/guides/swap-refunds.md
|
|
72
|
+
|
|
73
|
+
## 6. Checkout UI shows nothing
|
|
74
|
+
|
|
75
|
+
- The components require `prefix` — the exact base path the routes are mounted
|
|
76
|
+
at (`"/openreceive"` unless you changed it).
|
|
77
|
+
- Import the stylesheet (`@openreceive/react/styles.css` or the elements
|
|
78
|
+
sheet).
|
|
79
|
+
- "invoice must not be an NWC connection string" means a server secret leaked
|
|
80
|
+
into a browser payload — stop and fix the server response; never render it.
|
|
81
|
+
https://openreceive.org/guides/frontend-checkout.md
|
|
82
|
+
|
|
83
|
+
## Still stuck
|
|
84
|
+
|
|
85
|
+
The full route/option/error reference:
|
|
86
|
+
https://openreceive.org/guides/api-reference.md · machine-readable contract:
|
|
87
|
+
https://openreceive.org/openapi.yaml · library bug reports:
|
|
88
|
+
https://openreceive.org/contact
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrate-openreceive
|
|
3
|
+
description: >
|
|
4
|
+
Integrate OpenReceive inbound Bitcoin Lightning payments into an application.
|
|
5
|
+
Use when adding Bitcoin, Lightning, or crypto checkout to a Node.js, Express,
|
|
6
|
+
Fastify, Next.js, Rails, React, Vue, Svelte, Angular, or plain-HTML
|
|
7
|
+
application with OpenReceive (the @openreceive/* npm packages or the
|
|
8
|
+
openreceive-rails gem).
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Integrate OpenReceive
|
|
13
|
+
|
|
14
|
+
OpenReceive is a payment library that runs inside the application you are
|
|
15
|
+
editing. It mounts HTTP routes there, issues Lightning invoices against a
|
|
16
|
+
wallet the merchant already controls, and calls back into your code when one
|
|
17
|
+
settles. There is no OpenReceive account and no API key; funds land directly in
|
|
18
|
+
the merchant's wallet. The one required credential is a **receive-only NWC
|
|
19
|
+
code** (`NWC_URI`).
|
|
20
|
+
|
|
21
|
+
## Pick the stack, then follow its directions
|
|
22
|
+
|
|
23
|
+
1. Identify the server stack of the application you are in.
|
|
24
|
+
2. Open the matching reference — it is complete (quickstart inlined) and needs
|
|
25
|
+
no network access:
|
|
26
|
+
- Node (Express / Fastify / Next.js): [references/node.md](references/node.md)
|
|
27
|
+
- Rails: [references/rails.md](references/rails.md)
|
|
28
|
+
3. Follow its **Step 0** first: confirm `NWC_URI` is set in the server
|
|
29
|
+
environment before writing code. Never print the value; never invent a
|
|
30
|
+
placeholder.
|
|
31
|
+
|
|
32
|
+
Install (Node): `npm install @openreceive/express @openreceive/react` — swap
|
|
33
|
+
the adapter (`fastify`, `next`) and UI package (`vue`, `svelte`, `angular`,
|
|
34
|
+
`elements`) for the stack. Install (Rails): `bundle add openreceive-rails`.
|
|
35
|
+
|
|
36
|
+
## The three server objects
|
|
37
|
+
|
|
38
|
+
| Object | Built with | Talks to |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Wallet client | `createOpenReceive()` | the merchant's wallet — mints invoices, reads settlement, holds the NWC code |
|
|
41
|
+
| Host | `createHost()` | your database — your hooks plus the `openreceive_payments` table |
|
|
42
|
+
| HTTP routes | `openReceiveExpress()` / `openReceiveFastify()` / `openReceiveNext()` / the Rails engine | the browser — mounted at `/openreceive` by default |
|
|
43
|
+
|
|
44
|
+
The quickstart's one-factory form (`openReceiveExpress({ wallet, storage,
|
|
45
|
+
amountFor, authorize })`) builds all three; compose them separately only for a
|
|
46
|
+
shared wallet client or a custom repository. The checkout UI
|
|
47
|
+
(`<Checkout reference={...} prefix="/openreceive" />`) is the optional fourth
|
|
48
|
+
piece.
|
|
49
|
+
|
|
50
|
+
## The host contract: authorize, amountFor, onPaid
|
|
51
|
+
|
|
52
|
+
Your application keeps orders, users, prices, and fulfillment. Three hooks are
|
|
53
|
+
the entire bridge — wire them to the models this app already has, never to
|
|
54
|
+
copied demo models:
|
|
55
|
+
|
|
56
|
+
- `amountFor(reference)` — the authoritative price, read from your own data.
|
|
57
|
+
Return `{ currency, value, description }` with `value` a **decimal string**
|
|
58
|
+
(never a float, never payer input), or `null` when there is nothing to pay
|
|
59
|
+
for. The `reference` is your order id: one per thing you fulfill, created
|
|
60
|
+
before checkout, kept across retries, never reused.
|
|
61
|
+
- `authorize({ action, request, resource })` — your own access check, run on
|
|
62
|
+
every request. `resource.reference` is a claim the payer made, not proof;
|
|
63
|
+
read a real session.
|
|
64
|
+
- `onPaid({ reference, paidAt, query })` — fulfillment, run once per reference
|
|
65
|
+
inside the settlement transaction, only for the first settled attempt. Use
|
|
66
|
+
the provided `query`, not your ORM's other connection, and guard the
|
|
67
|
+
transition (`UPDATE … WHERE state = 'awaiting_payment'`).
|
|
68
|
+
|
|
69
|
+
## 409 is a state, not a failure
|
|
70
|
+
|
|
71
|
+
The library serializes attempts per reference. A create that returns **409
|
|
72
|
+
CONFLICT** is normal checkout flow: the reference already settled, or an unpaid
|
|
73
|
+
checkout for that payment method is already in progress. Surface it as order
|
|
74
|
+
state; do not retry-loop it, and do not build an idempotency store around it —
|
|
75
|
+
that serialization is the library's job. (A hook failure while persisting an
|
|
76
|
+
attempt is a **503 retryable**, deliberately distinct.)
|
|
77
|
+
|
|
78
|
+
## Secrets
|
|
79
|
+
|
|
80
|
+
`NWC_URI` and `LSC_URI_*` are server-only. Never put them in browser code,
|
|
81
|
+
logs, assets, or tests. Boot fails closed if the NWC code advertises spend
|
|
82
|
+
methods such as `pay_invoice` — mint a receive-only code
|
|
83
|
+
(https://openreceive.org/get_a_nwc_code_to_receive_payments) instead of
|
|
84
|
+
overriding.
|
|
85
|
+
|
|
86
|
+
## Database tables
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx openreceive scaffold payments --orm prisma # or drizzle | typeorm | sequelize | knex
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
emits the `openreceive_payments` + `openreceive_meta` migration for THIS app's
|
|
93
|
+
database (Rails: `bin/rails generate openreceive:install`); run it through the
|
|
94
|
+
app's normal migration workflow. The tables sit beside your models — no
|
|
95
|
+
relations to them, no separate database, no Redis.
|
|
96
|
+
|
|
97
|
+
## Verify, and test without a real wallet
|
|
98
|
+
|
|
99
|
+
`npx openreceive doctor` checks the configuration and says what to fix.
|
|
100
|
+
|
|
101
|
+
For tests, inject a fake wallet at the stable seams — `client` on
|
|
102
|
+
`createOpenReceive` (any object with `preflight`, `makeInvoice`,
|
|
103
|
+
`listTransactions`) or `config.nwc_client` in Rails — plus
|
|
104
|
+
`StaticPriceProvider` for fiat pricing without a network. Your routes,
|
|
105
|
+
persistence, reconcile, and `onPaid` then run the production code paths.
|
|
106
|
+
Details: https://openreceive.org/guides/host-testing.md
|
|
107
|
+
|
|
108
|
+
## Deeper documentation
|
|
109
|
+
|
|
110
|
+
Fetch on demand — each URL is raw markdown:
|
|
111
|
+
https://openreceive.org/guides/authorization.md ·
|
|
112
|
+
https://openreceive.org/guides/storage.md ·
|
|
113
|
+
https://openreceive.org/guides/api-reference.md ·
|
|
114
|
+
https://openreceive.org/guides/security.md ·
|
|
115
|
+
https://openreceive.org/openapi.yaml (the normative HTTP contract) ·
|
|
116
|
+
https://openreceive.org/llms.txt (the full index)
|