openreceive-rails 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7ef5df95a3cc1af66502a65a1b130aad1a88ab8ff93dcd26b4e93a1d732dca2
4
- data.tar.gz: 291e45e4efa083eee2345490086e97e757ae3009aa0f79ba3a7ef3ba55a04bb5
3
+ metadata.gz: 96a850e9b25d9797bff5f7b668b8e8bf26c891991058bf58ffd911c0abcdcbb4
4
+ data.tar.gz: 33d4e8872eb37baf2d77f0f47b5224dafa4851f056e43ff0c047f9b3e05ab647
5
5
  SHA512:
6
- metadata.gz: 36a33f19c0791f0e127c7cb4bc6b746aa377a124d5e1c9cef9c553826416f6fb51383029c0b8ba1b0ad11e33e4b069e3156bff2d751adbafe0718825c99a9105
7
- data.tar.gz: 18e1177d9a7974614eb8db63fe27c5414b21a0758c7e2f4f02d4e955970403217ebf6bbf3c94e1d7d840219ba99cdaf944f4bc0e64ad395fc7318937fef5e85f
6
+ metadata.gz: 2447535630f828c0185846df6f79b995ac0968c8a7a0e22ccf02104e3b9003333e3754476f65addb09823ae1678004639c8dfeb95e6f914f54c26fd610f575f0
7
+ data.tar.gz: c01e0905acbd634606783b0b6fe05eb16002238f7987cb92ac48c97da788f49a82b42242e82ca60d19624d359a6841583bf7a9caab73f2f8902c44f0a567a70d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 - 2026-09-02
4
+
5
+ ### A missing migration says so, with the fix
6
+
7
+ When the `openreceive_meta` table is absent, the engine now raises
8
+ `OpenReceive::ConfigurationError` naming the exact commands —
9
+ `bin/rails generate openreceive:install`, then `bin/rails db:migrate` — and
10
+ linking the storage guide, instead of leaking a raw
11
+ `ActiveRecord::StatementInvalid` from the first query. The check runs only on
12
+ request-serving paths, so `db:migrate`, `db:prepare`, and the generator are
13
+ unaffected, and a healed database is retried rather than remembered as broken.
14
+
15
+ ### NWC configuration errors reach Node parity
16
+
17
+ A missing `NWC_URI` explains the receive-only requirement and links the
18
+ get-a-code page (still starting with the pinned "Set NWC_URI"); a malformed
19
+ one is framed as "set, but not a valid NWC code" with the parse reason and the
20
+ same URL, instead of surfacing the bare parse error. `validate!` failures
21
+ (authorize, amount_for, on_paid, rate limiting, opportunistic reconcile) each
22
+ state their fix and link the owning guide, and the rate-limiting client-IP
23
+ warning links the rate-limiting guide.
24
+
25
+ ### The gem carries the agent skills
26
+
27
+ `skills/` ships in the gem — the integrate and debug playbooks for coding
28
+ agents, kept byte-identical to the repository tree by
29
+ `npm run generate:skills`.
30
+
3
31
  ## 0.3.2 - 2026-08-29
4
32
 
5
33
  No changes to this gem in 0.3.2. The release is the overpaid-deposit refund
@@ -24,9 +24,24 @@ class OpenReceiveMeta < ActiveRecord::Base
24
24
  # written by a NEWER library must not be operated by this one (columns or
25
25
  # state transitions it does not know about). An unreadable or absent marker
26
26
  # means "not versioned" — the pre-versioned migrations could not seed a row —
27
- # and is not a refusal. Mirrors the JS repository's assertSupportedSchema.
27
+ # and is not a refusal. A missing TABLE is different: that is diagnosable as
28
+ # "the install migration never ran here", and saying so beats the raw
29
+ # StatementInvalid the first payments query would raise a moment later.
30
+ # Mirrors the JS repository's assertSupportedSchema.
31
+ #
32
+ # Reached only from the request-serving paths (every OpenReceivePayment
33
+ # entry point and the reconcile gate) — never unconditionally at boot — so
34
+ # `db:migrate`, `db:prepare`, the install generator, and asset builds still
35
+ # run against an unmigrated database. A raise is not memoized: once the host
36
+ # runs the migration, the same process starts serving.
28
37
  def self.assert_supported_schema!
29
38
  @schema_version_checked ||= begin
39
+ unless table_exists?
40
+ raise OpenReceive::ConfigurationError,
41
+ "The openreceive_meta table does not exist — the OpenReceive tables have not been " \
42
+ "migrated in this database. Run `bin/rails generate openreceive:install`, then " \
43
+ "`bin/rails db:migrate`. https://openreceive.org/guides/storage.md"
44
+ end
30
45
  stored = stored_schema_version
31
46
  if !stored.nil? && stored > OpenReceive::Server::PAYMENTS_SCHEMA_VERSION
32
47
  raise OpenReceive::ConfigurationError,
@@ -147,16 +147,23 @@ module OpenReceive
147
147
  end
148
148
 
149
149
  def validate!
150
- raise ConfigurationError, "OpenReceive.config.authorize is required." if @authorize.nil?
150
+ if @authorize.nil?
151
+ raise ConfigurationError,
152
+ "OpenReceive.config.authorize is required — authentication belongs to the host. " \
153
+ "Set config.authorize in config/initializers/openreceive.rb to check the payer's " \
154
+ "session. https://openreceive.org/guides/authorization.md"
155
+ end
151
156
  if @rate_limiting && @rate_limit
152
157
  raise ConfigurationError,
153
- "Set either OpenReceive.config.rate_limiting or a custom rate_limit hook, not both."
158
+ "Set either OpenReceive.config.rate_limiting or a custom rate_limit hook, not " \
159
+ "both: rate_limiting is the built-in per-IP limiter, rate_limit replaces it with " \
160
+ "your own policy. https://openreceive.org/guides/rate-limiting.md"
154
161
  end
155
162
  if @rate_limiting && advanced_hooks?
156
163
  raise ConfigurationError,
157
164
  "config.rate_limiting counts engine-owned OpenReceivePayment rows; with a custom " \
158
165
  "repository (resolve_checkout/on_checkout_created), pass a custom rate_limit hook " \
159
- "backed by your own store instead."
166
+ "backed by your own store instead. https://openreceive.org/guides/rate-limiting.md"
160
167
  end
161
168
  if @opportunistic_reconcile && advanced_hooks?
162
169
  # Same fail-at-construction idiom as the JS handler: the default
@@ -166,19 +173,28 @@ module OpenReceive
166
173
  "config.opportunistic_reconcile (on by default) scans engine-owned " \
167
174
  "OpenReceivePayment rows through the shared openreceive_meta gate; with a custom " \
168
175
  "repository (resolve_checkout/on_checkout_created), set " \
169
- "config.opportunistic_reconcile = false and run your own settlement worker."
176
+ "config.opportunistic_reconcile = false and run your own settlement worker. " \
177
+ "https://openreceive.org/guides/storage.md"
170
178
  end
171
179
  if @on_paid.nil?
172
- raise ConfigurationError, "OpenReceive.config.on_paid is required to durably record settlement."
180
+ raise ConfigurationError,
181
+ "OpenReceive.config.on_paid is required to durably record settlement. Set " \
182
+ "config.on_paid to fulfill the order — it runs once, for the reference's first " \
183
+ "settled attempt. https://openreceive.org/guides/api-reference.md#openreceiveconfigure"
173
184
  end
174
185
  if @resolve_checkout.nil? != @on_checkout_created.nil?
175
186
  raise ConfigurationError,
176
- "OpenReceive.config.resolve_checkout and on_checkout_created must be configured together (advanced mode)."
187
+ "OpenReceive.config.resolve_checkout and on_checkout_created must be configured " \
188
+ "together (advanced mode). Set both, or neither and use quickstart amount_for. " \
189
+ "https://openreceive.org/guides/api-reference.md#openreceiveconfigure"
177
190
  end
178
191
  if !advanced_hooks? && @amount_for.nil?
179
192
  raise ConfigurationError,
180
193
  "Set OpenReceive.config.amount_for (quickstart), " \
181
- "or resolve_checkout and on_checkout_created (advanced)."
194
+ "or resolve_checkout and on_checkout_created (advanced). " \
195
+ "amount_for(reference) returns the amount to charge — e.g. { \"sats\" => 2100 } — " \
196
+ "or nil for an unknown reference. " \
197
+ "https://openreceive.org/guides/api-reference.md#openreceiveconfigure"
182
198
  end
183
199
  resolved_nwc_client
184
200
  true
@@ -234,7 +250,8 @@ module OpenReceive
234
250
  warned_unattributable = true
235
251
  rails_logger&.warn(
236
252
  "[openreceive] rate limiting is enabled but no client IP could be resolved; " \
237
- "attempts from this request are not counted. Configure config.client_ip."
253
+ "attempts from this request are not counted. Configure config.client_ip. " \
254
+ "https://openreceive.org/guides/rate-limiting.md"
238
255
  )
239
256
  end
240
257
  next true
@@ -377,21 +394,52 @@ module OpenReceive
377
394
  return @nwc_client unless @nwc_client.nil?
378
395
 
379
396
  connection = @nwc || ENV["NWC_URI"]&.strip
380
- if connection.nil? || connection.empty?
381
- raise ConfigurationError, "Set NWC_URI, or configure OpenReceive.config.nwc/nwc_client explicitly."
382
- end
397
+ raise ConfigurationError, missing_nwc_message if connection.nil? || connection.empty?
383
398
  return connection if connection.respond_to?(:make_invoice) || connection.respond_to?(:makeInvoice)
384
399
 
400
+ # Parse with the engine's own parser first, so a malformed code surfaces
401
+ # as a framed configuration error (what is wrong, the fix, the help URL)
402
+ # rather than a bare parse failure from whichever layer touched it first.
403
+ begin
404
+ OpenReceive.parse_nwc_uri(connection)
405
+ rescue OpenReceive::NwcUriParseError => e
406
+ raise ConfigurationError, invalid_nwc_message(e.message)
407
+ end
385
408
  OpenReceive::NwcRubyReceiveClient.new(
386
409
  client: build_nwc_ruby_client(connection), connection_uri: connection
387
410
  )
388
411
  end
389
412
 
413
+ # Mirrors the JS core formatMissingNwcMessage: what is missing, the
414
+ # concrete fix, and where to get a receive-only code.
415
+ def missing_nwc_message
416
+ [
417
+ "OpenReceive needs a receive-only NWC code to receive payments.",
418
+ "Set NWC_URI to your receive-only Nostr Wallet Connect connection string, " \
419
+ "or configure OpenReceive.config.nwc/nwc_client explicitly.",
420
+ "Get one here: #{OpenReceive::NWC_CODE_HELP_URL}"
421
+ ].join("\n")
422
+ end
423
+
424
+ # Mirrors the JS core formatInvalidNwcMessage; the subject names where the
425
+ # bad value came from so the operator knows which setting to fix.
426
+ def invalid_nwc_message(reason)
427
+ subject = @nwc.nil? ? "NWC_URI" : "OpenReceive.config.nwc"
428
+ [
429
+ "#{subject} is set, but it is not a valid NWC code.",
430
+ "Reason: #{reason}",
431
+ "Get a receive-only NWC code here: #{OpenReceive::NWC_CODE_HELP_URL}"
432
+ ].join("\n")
433
+ end
434
+
390
435
  def build_nwc_ruby_client(connection)
391
436
  require "nwc_ruby"
392
437
  ::NwcRuby::Client.from_uri(connection)
393
438
  rescue LoadError
394
- raise ConfigurationError, "Install nwc-ruby or configure nwc_client."
439
+ raise ConfigurationError,
440
+ "Install nwc-ruby (add `gem \"nwc-ruby\"` to the Gemfile) or configure " \
441
+ "OpenReceive.config.nwc_client with your own NWC client. " \
442
+ "https://openreceive.org/guides/api-reference.md#openreceiveconfigure"
395
443
  end
396
444
  end
397
445
 
@@ -5,6 +5,6 @@ module OpenReceive
5
5
  # top-level `::Rails` framework constant — engine code always references the framework as
6
6
  # `::Rails` to avoid shadowing.
7
7
  module Rails
8
- VERSION = "0.3.2"
8
+ VERSION = "0.3.3"
9
9
  end
10
10
  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)