openreceive-rails 0.2.3 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fd30ded365e254b6529fc4c006d8ec597a83f659cdf4b60b34127817d7fc995
4
- data.tar.gz: b272234e0a5eadb74ac2c63a9df71d3af0cc62d290497005af206653a489743d
3
+ metadata.gz: 0b3e0d9d08672c321e8e58648f9b59b3ffe28f373a6cf53c832d25dbeeb5cd32
4
+ data.tar.gz: 5f5919293c2fa6ded10cbaa28e6fd6406cb84d7348e180c0d8fc62dd762c9473
5
5
  SHA512:
6
- metadata.gz: 85bdd9b23bc16d13d4e1b3e4d7bc788cf2987c44431a9c345c9a6f48c63aea11f27e8c8ab7d40b1343e787048a15768eed8baaf40bf60d2e6b6b27fedb1d5d47
7
- data.tar.gz: 8ed6a9cd303864c971b5b8597e34cbe2e1e6b2cf9e5b1651a38f6183e43f4f69ab1a05af90c364f237b3698432c101c059eaf61bba9738cae692a4aea65ffb93
6
+ metadata.gz: 19f46e2352fcb7362a53b5ace86f263800b729b8c044e388ffde0c8636cac2d1f2dc9bf88879501da93a7c254bd03843e7742e68b5524e3115ab57016ffe1359
7
+ data.tar.gz: 5786094d5d457e28251ff6cac9467b7561d4ab466a83df93ab3d0086e5c6bd2ac29f2328ae6bd1ee259878f5fd2085e164a7685436a6b3905aaae574084dcae2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 - 2026-08-28
4
+
5
+ ### The generated allow-all `config.authorize` is no longer silent
6
+
7
+ `openreceive:install` writes a `config.authorize` that allows every request,
8
+ treating possession of the reference as the authorization. It is now the named
9
+ constant `OpenReceive::ALLOW_ALL_AUTHORIZE`, for the same reason
10
+ `LOGGING_ON_PAID` is a constant: the engine detects it at boot by identity and
11
+ warns that anyone holding an order id can mint invoices, poll status and request
12
+ refunds for it. Safe only while references are unguessable. The five-minute demo
13
+ still works; it just stops being quiet about what it is.
14
+
15
+ ### `bin/rails openreceive:doctor`
16
+
17
+ Step 0 of the agent directions as one command. It reports credential PRESENCE
18
+ only — every line is `set` or `unset`, and no secret is printed, echoed or
19
+ partially shown, which is what makes it safe to run in a shared terminal or
20
+ paste into an issue. Alongside that: whether `OpenReceive.configure` ran, which
21
+ hooks are missing or still the generated placeholders, where the engine is
22
+ mounted, and a wallet preflight.
23
+
24
+ The wallet line is last and best-effort. It builds the service — the same eager
25
+ preflight production boot runs, so a missing `NWC_URI`, a dead relay or a
26
+ SPEND-CAPABLE connection all answer here rather than on a payer's first checkout
27
+ — and reports the failure sanitized rather than raising it, because a doctor
28
+ that dies at line six tells an operator less than one that finishes.
29
+
30
+ "Look for `NWC_URI` in this app's server environment" has a different answer on
31
+ every host shape, and in a containerised app grepping the repo finds the name in
32
+ a compose file and proves nothing about the value. This asks the process, which
33
+ is the only place that always knows.
34
+
35
+ ## 0.3.0 - 2026-08-26
36
+
37
+ ### `config.amount_for` may return a `:description` beside the price
38
+
39
+ One optional display string — what the payer is buying — returned from the same
40
+ hook that already answers the price:
41
+
42
+ ```ruby
43
+ config.amount_for = lambda do |reference|
44
+ order = Order.find_by(reference: reference)
45
+ next nil unless order
46
+ { currency: "USD", value: order.total.to_s, description: "#{order.line_items.size} items" }
47
+ end
48
+ ```
49
+
50
+ The engine peels it off before the amount reaches the minting service, and the
51
+ handler echoes it on the prepare and create responses, where both shipped
52
+ checkout renderers draw it above the amount. Deliberately one string and not a
53
+ line-item schema: OpenReceive owns no orders. The install generator's
54
+ initializer documents it inline.
55
+
56
+ The full release narrative lives in the repository-root
57
+ [CHANGELOG](https://github.com/openreceive/openreceive/blob/master/CHANGELOG.md).
58
+
59
+ ## 0.2.4 - 2026-08-26
60
+
61
+ No Ruby changes in this release. 0.2.4 is browser-side only —
62
+ `@openreceive/browser`, `@openreceive/elements`, `@openreceive/react` and
63
+ `@openreceive/provider-data`. This gem is byte-identical to 0.2.3 and ships to
64
+ keep the one-version-for-everything rule, so the engine, the HTTP contract and
65
+ the settlement path are unchanged. The full release narrative lives in the
66
+ repository-root
67
+ [CHANGELOG](https://github.com/openreceive/openreceive/blob/master/CHANGELOG.md).
68
+
3
69
  ## 0.2.3 - 2026-08-25
4
70
 
5
71
  The Ruby gems release in lockstep with the npm workspace version. The full
@@ -21,8 +21,9 @@ OpenReceive.configure do |config|
21
21
  # prove this caller owns it.
22
22
  # Return true to allow the request, false for a 403.
23
23
  #
24
- # The default below allows every request, treating possession of the
25
- # reference as the authorization (an unknown reference is still a 404,
24
+ # The default below is a NAMED PLACEHOLDER, and the engine warns at boot for
25
+ # as long as it is still here. It allows every request, treating possession of
26
+ # the reference as the authorization (an unknown reference is still a 404,
26
27
  # because amount_for returns nil for it). That is only safe while your
27
28
  # references are unguessable (UUIDs, not sequential integers). If they are
28
29
  # enumerable, or an order should only be visible to the customer who placed
@@ -32,7 +33,7 @@ OpenReceive.configure do |config|
32
33
  # order = Order.find_by(id: context[:resource][:reference])
33
34
  # order && order.user_id == context[:request].session[:user_id]
34
35
  # end
35
- config.authorize = ->(_context) { true }
36
+ config.authorize = OpenReceive::ALLOW_ALL_AUTHORIZE
36
37
 
37
38
  # The price for a reference — the string your checkout passes, typically
38
39
  # your order id. Your application is the only price authority; payer input
@@ -40,10 +41,17 @@ OpenReceive.configure do |config|
40
41
  # { sats: 1200 }, or nil when there is nothing to pay for (a 404). The
41
42
  # engine refuses to serve checkouts until this is set.
42
43
  #
44
+ # An optional :description beside the price is what the payer is BUYING, in
45
+ # your own words — one display string the checkout renders above the amount.
46
+ # Without it the payer sees a QR and a number and no sign of what the number
47
+ # is for; OpenReceive owns no line items, so this is the whole of what it can
48
+ # show on its own.
49
+ #
43
50
  # TODO(price): look the reference up in your own application, e.g.
44
51
  # config.amount_for = lambda do |reference|
45
52
  # order = Order.find_by(id: reference)
46
- # order && { currency: "USD", value: order.total.to_s }
53
+ # order && { currency: "USD", value: order.total.to_s,
54
+ # description: "#{order.line_items.size} items" }
47
55
  # end
48
56
 
49
57
  # Runs inside the settlement transaction, only for the order's first settled
@@ -3,6 +3,14 @@
3
3
  require "openreceive/server"
4
4
 
5
5
  module OpenReceive
6
+ # The generated initializer's placeholder `config.authorize`: it allows every
7
+ # request, treating possession of the reference as the authorization. Kept as
8
+ # a named constant for the same reason LOGGING_ON_PAID is — the engine detects
9
+ # it at boot and says out loud that anyone holding an order id can mint
10
+ # invoices, poll status and request refunds for it. The five-minute demo still
11
+ # works; it just stops being silent.
12
+ ALLOW_ALL_AUTHORIZE = ->(_context) { true }
13
+
6
14
  class ConfigurationError < StandardError; end
7
15
 
8
16
  # Passed to the quickstart `config.on_paid` inside the settlement transaction,
@@ -264,9 +272,16 @@ module OpenReceive
264
272
  def engine_resolve_checkout
265
273
  lambda do |action:, request:, reference:, input:, pay_in_asset: nil|
266
274
  pricing = %w[checkout.prepare swap.quote checkout.create swap.create].include?(action)
267
- amount = pricing ? @amount_for.call(reference) : nil
268
- raise OpenReceive::Server::NotFoundError, "Unknown reference." if pricing && amount.nil?
269
- next({ amount: amount }) if %w[checkout.prepare swap.quote].include?(action)
275
+ price = pricing ? @amount_for.call(reference) : nil
276
+ raise OpenReceive::Server::NotFoundError, "Unknown reference." if pricing && price.nil?
277
+ # `description` what the payer is buying, in the host's own words — is
278
+ # returned beside the price and peeled off here, so the amount handed to
279
+ # the minting service stays exactly the price.
280
+ amount = price_only(price)
281
+ description = price_description(price)
282
+ if %w[checkout.prepare swap.quote].include?(action)
283
+ next({ amount: amount, description: description }.compact)
284
+ end
270
285
 
271
286
  requested_hash = input["payment_hash"] || input[:payment_hash]
272
287
  begin
@@ -285,6 +300,7 @@ module OpenReceive
285
300
 
286
301
  {
287
302
  amount: amount,
303
+ description: description,
288
304
  payment_hash: payment&.payment_hash,
289
305
  checkout: payment&.checkout_data,
290
306
  swap_data: payment&.swap_data
@@ -292,6 +308,25 @@ module OpenReceive
292
308
  end
293
309
  end
294
310
 
311
+ # The price alone. A host that returns a display string beside it must not
312
+ # have that string reach the amount resolver.
313
+ def price_only(price)
314
+ return price unless price.is_a?(Hash)
315
+
316
+ price.reject { |key, _| key.to_s == "description" }
317
+ end
318
+
319
+ # One optional display string, blank treated as absent.
320
+ def price_description(price)
321
+ return nil unless price.is_a?(Hash)
322
+
323
+ value = price[:description] || price["description"]
324
+ return nil unless value.is_a?(String)
325
+
326
+ trimmed = value.strip
327
+ trimmed.empty? ? nil : trimmed
328
+ end
329
+
295
330
  def engine_on_checkout_created
296
331
  lambda do |reference:, payment_hash:, checkout:, swap_data: nil, client_ip: nil, **|
297
332
  begin
@@ -388,6 +423,24 @@ module OpenReceive
388
423
  preflight_skip_reason.nil?
389
424
  end
390
425
 
426
+ # One line for `bin/rails openreceive:doctor`: did the wallet check pass?
427
+ #
428
+ # It builds the service, which is the same eager preflight the engine runs
429
+ # at production boot — a missing NWC_URI, a dead relay, or a SPEND-CAPABLE
430
+ # connection all answer here rather than on a payer's first checkout. The
431
+ # failure is reported, never raised: an operator running a doctor wants
432
+ # every other line too, and a doctor that dies at line six tells them less
433
+ # than one that finishes. Sanitized for the same reason the reconcile logs
434
+ # are — a connect failure can quote the NWC URI, secret and all.
435
+ def doctor_wallet_report
436
+ return "skipped — OpenReceive.configure has not run" unless configured?
437
+
438
+ config.service
439
+ "ok — the wallet answered and is receive-only"
440
+ rescue StandardError => error
441
+ "FAILED — #{sanitize_failure_message(error)}"
442
+ end
443
+
391
444
  # nil when the boot preflight should run; otherwise the short reason the
392
445
  # engine logs, so an operator who expected a fail-closed boot and did not
393
446
  # get one can see why in the same log line.
@@ -33,6 +33,14 @@ module OpenReceive
33
33
  "config/initializers/openreceive.rb."
34
34
  )
35
35
  end
36
+ if OpenReceive.config.authorize.equal?(OpenReceive::ALLOW_ALL_AUTHORIZE)
37
+ ::Rails.logger&.warn(
38
+ "[openreceive] config.authorize is still the generated allow-all placeholder — " \
39
+ "anyone holding an order id can mint invoices, poll status, and request refunds " \
40
+ "for it. Safe only while your references are unguessable. Replace it in " \
41
+ "config/initializers/openreceive.rb."
42
+ )
43
+ end
36
44
  if ::Rails.env.production?
37
45
  skipped = OpenReceive.preflight_skip_reason
38
46
  if skipped.nil?
@@ -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.2.3"
8
+ VERSION = "0.3.1"
9
9
  end
10
10
  end
@@ -1,6 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :openreceive do
4
+ # Step 0 of the agent directions, as one command.
5
+ #
6
+ # "Look for NWC_URI in this app's server environment" is a SEARCH, and it has
7
+ # a different answer on every host shape: a .env file, Rails credentials, a
8
+ # deploy config, or — in a containerised app — a value that exists only in the
9
+ # running process, fetched by a launcher script, where grepping the repo finds
10
+ # the NAME in a compose file and proves nothing about the value. This asks the
11
+ # process, which is the only place that always knows.
12
+ #
13
+ # PRESENCE ONLY. No secret is ever printed, echoed, or partially shown; every
14
+ # line is "set" or "unset". That rule is what makes this safe to run in a
15
+ # shared terminal, paste into an issue, or hand to a coding agent.
16
+ desc "Report OpenReceive's install state: credentials (set/unset only), engine mount, hooks"
17
+ task doctor: :environment do
18
+ set = ->(name) { ENV[name].to_s.strip.empty? ? "unset" : "set" }
19
+ lines = [
20
+ "openreceive:doctor",
21
+ " NWC_URI: #{set.call('NWC_URI')}",
22
+ " LSC_URI_PRIMARY: #{set.call('LSC_URI_PRIMARY')}",
23
+ " LSC_URI_BACKUP: #{set.call('LSC_URI_BACKUP')}"
24
+ ]
25
+
26
+ if OpenReceive.configured?
27
+ config = OpenReceive.config
28
+ hook = lambda do |value, placeholder, name|
29
+ return "MISSING — the engine refuses to serve checkouts without it" if value.nil?
30
+ return "the generated placeholder (#{name}) — replace it" if value.equal?(placeholder)
31
+
32
+ "set"
33
+ end
34
+ lines << " configure: run"
35
+ lines << " authorize: #{hook.call(config.authorize, OpenReceive::ALLOW_ALL_AUTHORIZE,
36
+ 'allow-all')}"
37
+ lines << " amount_for: #{config.amount_for.nil? ? 'MISSING — the engine refuses to serve checkouts without it' : 'set'}"
38
+ lines << " on_paid: #{hook.call(config.on_paid, OpenReceive::LOGGING_ON_PAID,
39
+ 'logging-only')}"
40
+ else
41
+ lines << " configure: NOT RUN — run `bin/rails generate openreceive:install`"
42
+ end
43
+
44
+ mount = Rails.application.routes.routes.find do |route|
45
+ route.app.respond_to?(:app) && route.app.app == OpenReceive::Engine
46
+ end
47
+ lines << " engine mounted: #{mount.nil? ? 'no — add `mount OpenReceive::Engine` to config/routes.rb' : "at #{mount.path.spec}"}"
48
+
49
+ # The wallet check, last and best-effort: it is the only line that talks to
50
+ # a relay, and a doctor that raises tells an operator less than one that
51
+ # reports. The receive-only verdict is the whole point — a spend-capable
52
+ # connection is refused, not warned about.
53
+ lines << " wallet preflight: #{OpenReceive.doctor_wallet_report}"
54
+
55
+ puts lines.join("\n")
56
+ end
57
+
4
58
  desc "Run one OpenReceive reconciliation pass over pending payment attempts"
5
59
  task reconcile: :environment do
6
60
  checks = OpenReceive.reconcile!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openreceive-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenReceive
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.3
18
+ version: 0.3.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.2.3
25
+ version: 0.3.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: openreceive-server
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.2.3
32
+ version: 0.3.1
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.2.3
39
+ version: 0.3.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rails
42
42
  requirement: !ruby/object:Gem::Requirement