openreceive-server 0.3.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d476e033b4974c2349800f3a8aae6b3a097027b912b208dfeffd7a79e9a19b0f
4
- data.tar.gz: e6a8d6732c35b93f6414d85c0aec841cc81d400c187c0d7fa50cab817a675614
3
+ metadata.gz: 500b212ec0e8a9bd58dfa38b19e90ede798db5ded9d1cb5c92d8da2b86ce186c
4
+ data.tar.gz: bf86d6d2f53a8df863e814a7452968a89d2a941391f024389a52c31a7e3856b5
5
5
  SHA512:
6
- metadata.gz: 1e86a0afe927a1d99b70f1e1b59c92f1b89622353796f5e0b8a59a5e874eb31e55aa8d2517a17e63d6cb00d167b7e0819f4a2364f10b1668660db36342a0cd55
7
- data.tar.gz: 3f00557f4b1aaf1593d3f0ac39d69a35c81c318f80f9e98657d6661bd2e39bcaf047c2dc7bbf7d2d0fcb608e20524eb0bd6a593bd658adf63043197f10d7ac9c
6
+ metadata.gz: 68a97e57ae6190037bcd5a6251c47db9e343ce3792138644140e1f490ec18c805c06752f9fcacd1e58a52c244564516f10a1d3adeccd5011373da1f2fb2cfce8
7
+ data.tar.gz: fd7687df474ee570860c72685ca107648f4ae77c56f4e177a3f5d45968463ef7100d0e8db97f1b886c15203178fd7fecd8c2728ecc9aa7d4fe4902be39efa276
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 - 2026-09-02
4
+
5
+ ### Error messages state the fix and link the doc
6
+
7
+ The spend-capable refusal now mirrors the Node engine: the receive-only
8
+ framing, the leaked-code-drains-the-wallet rationale, the get-a-code URL, and
9
+ the override spelled `OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC=true` (previously
10
+ `=1`). Wallet preflight failures carry the same URL. The 403 for a denied
11
+ reference names the host's authorize hook and links the authorization guide,
12
+ and the handler's required-hook `ArgumentError`s say how to fix themselves.
13
+
14
+ ### The gem carries the agent skills
15
+
16
+ `skills/` ships in the gem — the integrate and debug playbooks for coding
17
+ agents, kept byte-identical to the repository tree by
18
+ `npm run generate:skills`.
19
+
20
+ ## 0.3.2 - 2026-08-29
21
+
22
+ ### An overpaid swap deposit is a refund, not a support ticket
23
+
24
+ `normalize_status` mapped a FixedFloat-compatible `EMERGENCY` carrying `MORE`
25
+ to `attention`, which is terminal and has no refund form. The provider itself
26
+ opens `refund_required` for an overpay and accepts a full `choice=REFUND`, so
27
+ the gem now does the same: `MORE` / `OVER` / `OVERPAID` fall through to
28
+ `refund_required`, and `refund_reason` gains `overpaid` and
29
+ `overpaid_and_late`. The refund returns the whole deposit — the payout is a
30
+ fixed-amount bolt11, so a surplus has nothing to convert into.
31
+
3
32
  ## 0.3.1 - 2026-08-28
4
33
 
5
34
  No changes to this gem in 0.3.1. It is byte-identical to 0.3.0 and ships to keep
@@ -198,7 +198,8 @@ module OpenReceive
198
198
  def initialize(reason)
199
199
  super(
200
200
  "OpenReceive wallet preflight failed: #{reason} Use a receive-only " \
201
- "NWC connection advertising make_invoice and list_transactions."
201
+ "NWC connection advertising make_invoice and list_transactions. " \
202
+ "Get one here: #{OpenReceive::NWC_CODE_HELP_URL}"
202
203
  )
203
204
  end
204
205
  end
@@ -206,14 +207,17 @@ module OpenReceive
206
207
  # Boot-time refusal: the configured NWC connection advertises spend methods.
207
208
  # OpenReceive is receive-only; a spend-capable code in a receive deployment
208
209
  # is a live theft risk, so preflight fails closed instead of booting.
210
+ # Mirrors the JS core formatSpendCapabilityRefusedMessage.
209
211
  class SpendCapableWalletError < StandardError
210
212
  def initialize(methods)
211
213
  super(
212
- "The configured NWC connection advertises spend methods " \
213
- "(#{Array(methods).join(', ')}). OpenReceive is receive-only; use a " \
214
- "receive-only NWC code, or override explicitly with " \
215
- "config.allow_spend_capable_wallet = true or " \
216
- "OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC=1."
214
+ "This NWC connection is NOT receive-only.\n" \
215
+ "The wallet info event advertises spend method(s): #{Array(methods).join(', ')}.\n" \
216
+ "A leaked spend-capable NWC code lets an attacker drain the wallet, " \
217
+ "so OpenReceive refuses to boot with it.\n" \
218
+ "Get a receive-only NWC code here: #{OpenReceive::NWC_CODE_HELP_URL}\n" \
219
+ "If this wallet cannot mint a receive-only code and you accept the risk, set " \
220
+ "allow_spend_capable_wallet: true (or OPENRECEIVE_ALLOW_SPEND_CAPABLE_NWC=true)."
217
221
  )
218
222
  end
219
223
  end
@@ -30,10 +30,26 @@ module OpenReceive
30
30
  # returns nil the limiter fails open for that request.
31
31
  def initialize(service:, authorize:, resolve_checkout:, on_checkout_created:, on_paid:,
32
32
  rate_limit: nil, client_ip: nil)
33
- raise ArgumentError, "authorize is required" if authorize.nil?
34
- raise ArgumentError, "resolve_checkout is required" if resolve_checkout.nil?
35
- raise ArgumentError, "on_checkout_created is required" if on_checkout_created.nil?
36
- raise ArgumentError, "on_paid is required" if on_paid.nil?
33
+ if authorize.nil?
34
+ raise ArgumentError,
35
+ "authorize is required authentication belongs to the host application. " \
36
+ "https://openreceive.org/guides/authorization.md"
37
+ end
38
+ if resolve_checkout.nil?
39
+ raise ArgumentError,
40
+ "resolve_checkout is required — it resolves a reference to the host-owned " \
41
+ "amount and attempt. https://openreceive.org/guides/api-reference.md"
42
+ end
43
+ if on_checkout_created.nil?
44
+ raise ArgumentError,
45
+ "on_checkout_created is required — it persists the attempt before payer " \
46
+ "instructions are returned. https://openreceive.org/guides/api-reference.md"
47
+ end
48
+ if on_paid.nil?
49
+ raise ArgumentError,
50
+ "on_paid is required — it durably records settlement. " \
51
+ "https://openreceive.org/guides/api-reference.md"
52
+ end
37
53
  @service = service
38
54
  @authorize = authorize
39
55
  @resolve_checkout = resolve_checkout
@@ -350,7 +366,13 @@ module OpenReceive
350
366
 
351
367
  def authorize!(action, request, resource)
352
368
  context = { action: action, request: request, resource: resource }
353
- raise ForbiddenError, "Not authorized for this action." unless @authorize.call(context)
369
+ return if @authorize.call(context)
370
+
371
+ # Byte-identical to the JS handler's FORBIDDEN message (same wire contract).
372
+ raise ForbiddenError,
373
+ "Not authorized for this action. The application's authorize hook denied it; " \
374
+ "if this is unexpected, check that the payer's session reaches the checkout " \
375
+ "routes. https://openreceive.org/guides/authorization.md"
354
376
  end
355
377
 
356
378
  def commit(checkout, swap_data = nil, request = nil)
@@ -642,12 +642,11 @@ module OpenReceive
642
642
  "attention_reason" => "provider_reported_emergency"
643
643
  }
644
644
  end
645
- if (statuses & %w[MORE OVER OVERPAID]).any?
646
- return {
647
- "state" => "attention", "attention" => true,
648
- "attention_reason" => "provider_reported_emergency"
649
- }
650
- end
645
+ # An overpay (MORE) takes the same self-serve full-refund path as
646
+ # LESS and EXPIRED — it is what the provider itself does with it.
647
+ # There is no partial refund of the surplus: the payout invoice is
648
+ # a fixed amount, so an emergency deposit is returned whole or not
649
+ # at all.
651
650
  result = { "state" => "refund_required" }
652
651
  result["refund_reason"] = refund_reason unless refund_reason.nil?
653
652
  return result
@@ -662,11 +661,17 @@ module OpenReceive
662
661
  }
663
662
  end
664
663
 
664
+ # LIMIT rides along with LESS/MORE when the deposit fell outside the
665
+ # pair's limits. It says nothing the payer can act on beyond the
666
+ # amount itself, so it names no reason of its own.
665
667
  def refund_reason_from_emergency_statuses(statuses)
666
668
  less = statuses.include?("LESS")
669
+ more = (statuses & %w[MORE OVER OVERPAID]).any?
667
670
  expired = statuses.include?("EXPIRED")
668
671
  return "underpaid_and_late" if less && expired
672
+ return "overpaid_and_late" if more && expired
669
673
  return "underpaid" if less
674
+ return "overpaid" if more
670
675
  return "late_deposit" if expired
671
676
 
672
677
  nil
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenReceive
4
4
  module Server
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.3"
6
6
  end
7
7
  end
@@ -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)