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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b3e0d9d08672c321e8e58648f9b59b3ffe28f373a6cf53c832d25dbeeb5cd32
|
|
4
|
+
data.tar.gz: 5f5919293c2fa6ded10cbaa28e6fd6406cb84d7348e180c0d8fc62dd762c9473
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
25
|
-
#
|
|
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 =
|
|
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
|
-
|
|
268
|
-
raise OpenReceive::Server::NotFoundError, "Unknown reference." if pricing &&
|
|
269
|
-
|
|
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.
|
data/lib/openreceive/engine.rb
CHANGED
|
@@ -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?
|
data/lib/tasks/openreceive.rake
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
+
version: 0.3.1
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: rails
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|