kiosk-server 0.5.20 → 0.6.28
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 +4 -4
- data/CHANGELOG.md +196 -0
- data/README.md +192 -138
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +21 -65
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +24 -7
- data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
- data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +17 -34
- data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
- data/app/controllers/kiosk/server/oauth_token_controller.rb +36 -32
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +20 -0
- data/app/models/kiosk/settlement.rb +1 -3
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -69
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -15
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +19 -25
- data/lib/kiosk/server/account_binding.rb +32 -62
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
- data/lib/kiosk/server/agent_rotation.rb +62 -0
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/assistant_key.rb +86 -0
- data/lib/kiosk/server/auth_toll.rb +6 -6
- data/lib/kiosk/server/binding_module_gate.rb +1 -1
- data/lib/kiosk/server/checkout.rb +57 -0
- data/lib/kiosk/server/client.rb +61 -0
- data/lib/kiosk/server/client_assertion.rb +154 -0
- data/lib/kiosk/server/client_registration.rb +98 -0
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +64 -75
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +38 -69
- data/lib/kiosk/server/dpop.rb +126 -0
- data/lib/kiosk/server/eddsa.rb +38 -0
- data/lib/kiosk/server/engine.rb +16 -13
- data/lib/kiosk/server/errors.rb +28 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +38 -10
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +13 -12
- data/lib/kiosk/server/executor/spending_cap.rb +17 -76
- data/lib/kiosk/server/executor.rb +86 -105
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +12 -3
- data/lib/kiosk/server/jwk.rb +83 -0
- data/lib/kiosk/server/jwks.rb +6 -0
- data/lib/kiosk/server/jwt_issuer.rb +7 -13
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +28 -29
- data/lib/kiosk/server/link_code.rb +9 -18
- data/lib/kiosk/server/linked_assistants.rb +22 -17
- data/lib/kiosk/server/mandate_verifier.rb +97 -139
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +31 -0
- data/lib/kiosk/server/oauth_error.rb +33 -0
- data/lib/kiosk/server/open_api.rb +31 -29
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +47 -78
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +30 -0
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +3 -3
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +9 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +369 -58
- data/lib/kiosk/server/schemas/binding.schema.json +24 -59
- data/lib/kiosk/server/schemas/kyc.schema.json +40 -78
- data/lib/kiosk/server/schemas/mandates.schema.json +123 -39
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/sd_jwt.rb +104 -0
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/signing_key.rb +18 -52
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +4 -4
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +272 -205
- data/lib/kiosk/server/wire_rendering.rb +2 -2
- data/lib/kiosk/server.rb +8 -2
- data/lib/kiosk/skill_link.rb +6 -2
- data/listen.py +38 -8
- metadata +34 -24
- data/app/models/kiosk/cart_mandate.rb +0 -21
- data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/agent_login.rb +0 -49
- data/lib/kiosk/server/agent_registration.rb +0 -88
- data/lib/kiosk/server/auth_challenge.rb +0 -33
- data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
- data/lib/kiosk/server/pop_verifier.rb +0 -87
- data/lib/kiosk/server/schema_definitions.rb +0 -351
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'kiosk/uuid_check'
|
|
4
|
-
|
|
5
3
|
module Kiosk
|
|
6
4
|
module Server
|
|
7
5
|
# §11.6 over a PSP adapter: claims the operator's payable row `unpaid →
|
|
8
|
-
# paying` before the capture, checks the
|
|
6
|
+
# paying` before the capture, checks the checkout against the operator's price,
|
|
9
7
|
# flips the row to `paid` once the capture returns, tells its owner on the
|
|
10
8
|
# `payment` topic, and runs `c.after_payment` once the settlement is recorded.
|
|
11
9
|
# A refund through it is recorded against the settlement.
|
|
@@ -19,7 +17,7 @@ module Kiosk
|
|
|
19
17
|
attr_reader :currency, :reference
|
|
20
18
|
|
|
21
19
|
# @param model [String] the payable model's name, e.g. "Order"; it includes Kiosk::Owned and Kiosk::Payable
|
|
22
|
-
# @param reference [String] the
|
|
20
|
+
# @param reference [String] the checkout line-item key naming the row, e.g. "order_id"
|
|
23
21
|
# @param query [String] the per-user query that publishes the row's payment state
|
|
24
22
|
def initialize(psp, currency:, model:, reference:, query:)
|
|
25
23
|
@psp = psp
|
|
@@ -34,12 +32,12 @@ module Kiosk
|
|
|
34
32
|
# The same claim over another PSP adapter.
|
|
35
33
|
def over(psp) = self.class.new(psp, **@options)
|
|
36
34
|
|
|
37
|
-
def capture(
|
|
35
|
+
def capture(checkout_mandate, payment_method: nil) = charge_row(claim!(checkout_mandate), checkout_mandate, payment_method)
|
|
38
36
|
|
|
39
37
|
delegate :setup_required?, :setup_url, to: :@psp
|
|
40
38
|
|
|
41
|
-
# The settlements whose
|
|
42
|
-
def settlements(id) = Kiosk::Settlement.joins(:
|
|
39
|
+
# The settlements whose checkout names this row.
|
|
40
|
+
def settlements(id) = Kiosk::Settlement.joins(:checkout_mandate).merge(Kiosk::CheckoutMandate.referencing(@reference => id))
|
|
43
41
|
|
|
44
42
|
def settled?(id) = settlements(id).exists?
|
|
45
43
|
|
|
@@ -58,15 +56,15 @@ module Kiosk
|
|
|
58
56
|
|
|
59
57
|
private
|
|
60
58
|
|
|
61
|
-
def charge_row(id,
|
|
62
|
-
settled = charge(id,
|
|
59
|
+
def charge_row(id, checkout_mandate, payment_method)
|
|
60
|
+
settled = charge(id, checkout_mandate, payment_method)
|
|
63
61
|
mark_paid!(id)
|
|
64
62
|
settled
|
|
65
63
|
end
|
|
66
64
|
|
|
67
65
|
# Releases the claim when the PSP refuses before any money moved.
|
|
68
|
-
def charge(id,
|
|
69
|
-
@psp.capture(
|
|
66
|
+
def charge(id, checkout_mandate, payment_method)
|
|
67
|
+
@psp.capture(checkout_mandate, payment_method: payment_method)
|
|
70
68
|
rescue Kiosk::PaymentProviders::SetupRequired
|
|
71
69
|
release!(id)
|
|
72
70
|
raise
|
|
@@ -75,11 +73,11 @@ module Kiosk
|
|
|
75
73
|
raise
|
|
76
74
|
end
|
|
77
75
|
|
|
78
|
-
def claim!(
|
|
79
|
-
id =
|
|
80
|
-
refuse_unclaimable!(id,
|
|
76
|
+
def claim!(checkout)
|
|
77
|
+
id = Lines.reference(checkout.line_items, @reference)
|
|
78
|
+
refuse_unclaimable!(id, checkout.user_id.to_s) unless @row.take(id, checkout.user_id.to_s, from: UNPAID, to: PAYING).positive?
|
|
81
79
|
begin
|
|
82
|
-
check_price!(
|
|
80
|
+
check_price!(checkout, id)
|
|
83
81
|
rescue StandardError
|
|
84
82
|
release!(id)
|
|
85
83
|
raise
|
|
@@ -87,28 +85,20 @@ module Kiosk
|
|
|
87
85
|
id
|
|
88
86
|
end
|
|
89
87
|
|
|
90
|
-
# The
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def check_price!(cart, id)
|
|
97
|
-
total = cart.total_amount_cents.to_i
|
|
98
|
-
lines = Array(cart.line_items).reject { |li| li[@reference] }
|
|
99
|
-
Lines.check_sum!(total, lines)
|
|
100
|
-
|
|
101
|
-
price = checker.call(id, lines)
|
|
88
|
+
# The price may have moved since the checkout was signed.
|
|
89
|
+
def check_price!(checkout, id)
|
|
90
|
+
total = checkout.total_amount_cents
|
|
91
|
+
price = checker.call(id, checkout.line_items.reject { |li| li[@reference] })
|
|
102
92
|
deny price if price.is_a?(String)
|
|
103
93
|
return if total == price
|
|
104
94
|
|
|
105
|
-
deny "
|
|
106
|
-
'
|
|
95
|
+
deny "checkout total #{total} does not equal this operator's price #{price} for #{noun} #{id}: " \
|
|
96
|
+
'the price changed after the operator signed the checkout'
|
|
107
97
|
end
|
|
108
98
|
|
|
109
99
|
def checker
|
|
110
|
-
Kiosk.configuration.
|
|
111
|
-
raise Errors::ConfigurationError, "PaymentClaim needs c.
|
|
100
|
+
Kiosk.configuration.checkout_price_checker or
|
|
101
|
+
raise Errors::ConfigurationError, "PaymentClaim needs c.checkout_price_checker, the operator's price"
|
|
112
102
|
end
|
|
113
103
|
|
|
114
104
|
def refuse_unclaimable!(id, payer)
|
|
@@ -123,8 +113,8 @@ module Kiosk
|
|
|
123
113
|
mark_paid!(id)
|
|
124
114
|
deny "#{noun} #{id} is already paid"
|
|
125
115
|
end
|
|
126
|
-
deny "#{noun} #{id} has a payment in progress under
|
|
127
|
-
'byte for byte:
|
|
116
|
+
deny "#{noun} #{id} has a payment in progress under other mandates. Send those mandates again, " \
|
|
117
|
+
'byte for byte: they cannot charge twice. Do not sign new mandates. ' \
|
|
128
118
|
"GET <endpoint>/#{@query} shows the payment state."
|
|
129
119
|
end
|
|
130
120
|
|
|
@@ -134,62 +124,41 @@ module Kiosk
|
|
|
134
124
|
raise Errors::Forbidden, message
|
|
135
125
|
end
|
|
136
126
|
|
|
137
|
-
# The
|
|
127
|
+
# The checkout's line items: the one row they name.
|
|
138
128
|
module Lines
|
|
139
129
|
module_function
|
|
140
130
|
|
|
141
131
|
def reference(line_items, key)
|
|
142
|
-
refs =
|
|
143
|
-
|
|
144
|
-
id = refs.first
|
|
145
|
-
return id if Kiosk::UuidCheck.valid?(id)
|
|
146
|
-
|
|
147
|
-
raise Errors::BadRequest.new("cart line_items #{key} #{id.inspect} is not a uuid",
|
|
148
|
-
hint: "use the `#{key}` this operator returned, verbatim")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# The priced lines, when any, sum to the cart total.
|
|
152
|
-
def check_sum!(total, lines)
|
|
153
|
-
priced = lines.select { |li| li['price_cents'] }
|
|
154
|
-
return if priced.empty?
|
|
155
|
-
|
|
156
|
-
sum = priced.sum do |li|
|
|
157
|
-
qty = li['qty'].to_i
|
|
158
|
-
price = li['price_cents'].to_i
|
|
159
|
-
deny 'each priced line needs a positive qty and price_cents' if qty <= 0 || price <= 0
|
|
160
|
-
qty * price
|
|
161
|
-
end
|
|
162
|
-
deny "cart total #{total} does not equal the sum of its line items #{sum}" unless total == sum
|
|
163
|
-
end
|
|
132
|
+
refs = line_items.filter_map { |li| li[key] }.uniq
|
|
133
|
+
return refs.first.to_s if refs.one?
|
|
164
134
|
|
|
165
|
-
|
|
166
|
-
raise Errors::Forbidden, message
|
|
135
|
+
raise Errors::Forbidden, "checkout line_items must name exactly one #{key}"
|
|
167
136
|
end
|
|
168
137
|
end
|
|
169
138
|
|
|
170
|
-
# A repeat of
|
|
171
|
-
# the newest unfinished
|
|
172
|
-
#
|
|
139
|
+
# A repeat of mandates whose capture did not complete. The row stays with
|
|
140
|
+
# the newest unfinished checkout mandate, and the PSP keys the capture by
|
|
141
|
+
# its reference, so the repeat cannot charge twice.
|
|
173
142
|
module Resume
|
|
174
|
-
def resume(
|
|
143
|
+
def resume(checkout_mandate, payment_method: nil) = charge_row(reclaim!(checkout_mandate), checkout_mandate, payment_method)
|
|
175
144
|
|
|
176
145
|
private
|
|
177
146
|
|
|
178
|
-
# The row
|
|
179
|
-
def reclaim!(
|
|
180
|
-
id =
|
|
181
|
-
payer =
|
|
147
|
+
# The row these repeated mandates claimed, or the row they may claim now.
|
|
148
|
+
def reclaim!(checkout)
|
|
149
|
+
id = Lines.reference(checkout.line_items, @reference)
|
|
150
|
+
payer = checkout.user_id.to_s
|
|
182
151
|
status = @row.status(id, payer)
|
|
183
|
-
return claim!(
|
|
184
|
-
return id if [PAYING, PAID].include?(status) && !settled?(id) &&
|
|
152
|
+
return claim!(checkout) if status == UNPAID
|
|
153
|
+
return id if [PAYING, PAID].include?(status) && !settled?(id) && unfinished_mandate(id, payer) == checkout.reference
|
|
185
154
|
|
|
186
155
|
refuse_unclaimable!(id, payer)
|
|
187
156
|
end
|
|
188
157
|
|
|
189
|
-
# The
|
|
190
|
-
def
|
|
191
|
-
Kiosk::
|
|
192
|
-
|
|
158
|
+
# The reference of the newest checkout mandate for this row that is neither settled nor declined.
|
|
159
|
+
def unfinished_mandate(id, payer)
|
|
160
|
+
Kiosk::CheckoutMandate.referencing(@reference => id).where(user_id: payer, declined_at: nil)
|
|
161
|
+
.where.missing(:settlements).order(created_at: :desc, id: :desc).pick(:reference)
|
|
193
162
|
end
|
|
194
163
|
end
|
|
195
164
|
include Resume
|
|
@@ -208,8 +177,8 @@ module Kiosk
|
|
|
208
177
|
end
|
|
209
178
|
|
|
210
179
|
INSERT_SQL = <<~SQL.squish
|
|
211
|
-
INSERT INTO "%<schema>s".
|
|
212
|
-
SELECT id, $1, $2, $3 FROM "%<schema>s".
|
|
180
|
+
INSERT INTO "%<schema>s".kiosk_refunds (settlement_id, psp_reference, refunded_amount_cents, refunded_at)
|
|
181
|
+
SELECT id, $1, $2, $3 FROM "%<schema>s".kiosk_settlements WHERE psp_reference = $4
|
|
213
182
|
ON CONFLICT (psp_reference) DO NOTHING
|
|
214
183
|
SQL
|
|
215
184
|
end
|
|
@@ -218,10 +187,10 @@ module Kiosk
|
|
|
218
187
|
# The operator's work after a payment, which reads the settlement.
|
|
219
188
|
module AfterPayment
|
|
220
189
|
# The executor calls this once, after it records the settlement of
|
|
221
|
-
# `
|
|
222
|
-
# failure here never surfaces
|
|
223
|
-
def settled(
|
|
224
|
-
id = Lines.reference(
|
|
190
|
+
# `checkout_mandate`, so `c.after_payment` reads the row's `settlement`. A
|
|
191
|
+
# failure here never surfaces in the response: the log carries it.
|
|
192
|
+
def settled(checkout_mandate)
|
|
193
|
+
id = Lines.reference(checkout_mandate.line_items, @reference)
|
|
225
194
|
Kiosk.configuration.after_payment&.call(id)
|
|
226
195
|
rescue StandardError => e
|
|
227
196
|
FailureLog.report("#{self.class} after_payment failed for #{id}", e)
|
|
@@ -6,10 +6,10 @@ require 'json'
|
|
|
6
6
|
module Kiosk
|
|
7
7
|
module Server
|
|
8
8
|
# The proof-of-work toll, in front of every verb that reaches the executor.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# The policy decides on each call; a valid proof sets the account's count of
|
|
10
|
+
# tolled calls back to zero. With no `reputation_policy` it returns
|
|
11
|
+
# `:proceed` and touches nothing; `kiosk-reputation` is required only when a
|
|
12
|
+
# policy is set.
|
|
13
13
|
module PowGate
|
|
14
14
|
module_function
|
|
15
15
|
|
|
@@ -21,46 +21,25 @@ module Kiosk
|
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# `command` is the policy verb ({Executor::VERBS}); `verb` is the
|
|
24
|
+
# `command` is the policy verb ({Executor::VERBS}); `verb` is the published name
|
|
25
25
|
# the fingerprint binds.
|
|
26
|
-
def gate(identity:, command:, body:, pow:, method: 'POST', verb: nil) # rubocop:disable Metrics/ParameterLists --
|
|
26
|
+
def gate(identity:, command:, body:, pow:, method: 'POST', verb: nil) # rubocop:disable Metrics/ParameterLists -- WireController and the specs pass these six by name
|
|
27
27
|
config = Kiosk.configuration
|
|
28
28
|
policy = config.reputation_policy
|
|
29
|
-
return :proceed if policy.nil?
|
|
29
|
+
return :proceed if policy.nil?
|
|
30
30
|
|
|
31
|
-
secret
|
|
32
|
-
|
|
31
|
+
secret = pow_secret!(config)
|
|
32
|
+
factors = ReputationFactors.for(identity:, policy:)
|
|
33
|
+
spec = policy.challenge_for(identity: identity, verb: command.to_sym, factors: factors)
|
|
33
34
|
return :proceed if spec.nil?
|
|
34
35
|
|
|
35
36
|
fp = request_fingerprint(method: method, verb: verb || command, body: body)
|
|
36
37
|
PowToll.new(spec: spec, fingerprint: fp, secret: secret, config: config)
|
|
37
|
-
.collect!(pow).tap {
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def session_paid?(config, identity)
|
|
41
|
-
session = session_id(identity)
|
|
42
|
-
!session.nil? && config.pow_spent_store.spent?(session)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Records the token's session as paid.
|
|
46
|
-
def paid(config, identity)
|
|
47
|
-
session = session_id(identity)
|
|
48
|
-
config.pow_spent_store.mark_spent(session, identity.claims[:exp]) if session
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# The access token a toll pays for, by its `jti`, or nil for a caller without one.
|
|
52
|
-
def session_id(identity)
|
|
53
|
-
claims = identity&.claims || {}
|
|
54
|
-
"session:#{claims[:jti]}" if claims[:jti] && claims[:exp]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# The policy's toll for this caller and verb, or nil for none.
|
|
58
|
-
def toll_for(policy, identity, verb)
|
|
59
|
-
policy.challenge_for(identity: identity, verb: verb, factors: ReputationFactors.for(identity:, policy:))
|
|
38
|
+
.collect!(pow).tap { TolledCalls.reset!(identity.user_id) if factors.calls_since_proof }
|
|
60
39
|
end
|
|
61
40
|
|
|
62
41
|
def pow_secret!(config)
|
|
63
|
-
unless defined?(::Kiosk::Reputation)
|
|
42
|
+
unless defined?(::Kiosk::Reputation::Challenge)
|
|
64
43
|
raise Errors::ConfigurationError,
|
|
65
44
|
'Kiosk::Server: reputation_policy is set but kiosk-reputation is not loaded. ' \
|
|
66
45
|
"Add `require 'kiosk/reputation'` (and `gem 'kiosk-reputation'`) to your app."
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# The two AP2 v0.2 receipts a settled `pay` answers (§11.3): JWTs this
|
|
6
|
+
# operator signs as it signs a checkout, each naming its mandate by reference.
|
|
7
|
+
module Receipts
|
|
8
|
+
SUCCESS = 'Success'
|
|
9
|
+
# AP2 requires a network id; this text stands in when the PSP reports none.
|
|
10
|
+
NO_NETWORK_ID = 'not reported by the payment provider'
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @return [Hash] `checkout_receipt` and `payment_receipt`, compact JWTs
|
|
15
|
+
def issue(checkout:, payment:, settlement_id:, psp_reference:, network_confirmation_id:)
|
|
16
|
+
{ checkout_receipt: sign(checkout, order_id: order_id(checkout)),
|
|
17
|
+
payment_receipt: sign(payment, payment_id: settlement_id, psp_confirmation_id: psp_reference,
|
|
18
|
+
network_confirmation_id: network_confirmation_id || NO_NETWORK_ID) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The id in the checkout's first line item, which names the item paid for (§11.7).
|
|
22
|
+
def order_id(checkout) = checkout.line_items.first.values.first.to_s
|
|
23
|
+
|
|
24
|
+
def sign(mandate, **claims)
|
|
25
|
+
Checkout.signed({ status: SUCCESS, iss: Kiosk.current_issuer, iat: Time.now.to_i,
|
|
26
|
+
reference: mandate.reference, **claims })
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -3,36 +3,43 @@
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
5
|
# The reputation inputs the engine knows itself: what the customer paid
|
|
6
|
-
# here and kept paid, and the visits the operator recorded, within a policy's
|
|
6
|
+
# here and kept paid, and the visits the operator recorded, within a policy's
|
|
7
|
+
# window; and the account's tolled calls since its last proof.
|
|
7
8
|
module ReputationFactors
|
|
8
9
|
module_function
|
|
9
10
|
|
|
10
11
|
# Empty factors unless the policy names a window (`#within`, in seconds).
|
|
12
|
+
# Reading the factors counts an assistant's call as one more tolled call of its account.
|
|
11
13
|
def for(identity:, policy:)
|
|
12
14
|
return ::Kiosk::Reputation::Factors.empty unless policy.respond_to?(:within) && identity&.user_id
|
|
13
15
|
|
|
14
16
|
count, cents = recent_spend(identity.user_id, Time.current - policy.within.to_i)
|
|
15
|
-
::Kiosk::Reputation::Factors.
|
|
17
|
+
::Kiosk::Reputation::Factors.new(settled_purchases_count: count, settled_purchases_cents: cents,
|
|
18
|
+
calls_since_proof: identity.agent_id && TolledCalls.count!(identity.user_id))
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
# Settlements and visits of this customer since `since`: their count and their cents.
|
|
19
22
|
# A settlement counts less what was refunded of it; one refunded in full does not count.
|
|
23
|
+
# The settlements and visits of each assistant account merged into this account count too.
|
|
20
24
|
def recent_spend(user_id, since)
|
|
21
25
|
sql = format(RECENT_SPEND_SQL, schema: Kiosk.configuration.schema)
|
|
22
26
|
row = ::ActiveRecord::Base.lease_connection.exec_query(sql, 'Kiosk recent spend', [user_id, since]).to_a.first
|
|
23
27
|
[row.fetch('count').to_i, row.fetch('cents').to_i]
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
MERGED_SQL = 'SELECT user_id FROM "%<schema>s".kiosk_assistant_accounts WHERE merged_into = $1'
|
|
26
31
|
RECENT_SPEND_SQL = <<~SQL.squish
|
|
27
32
|
SELECT count(*) AS count, COALESCE(SUM(cents), 0) AS cents FROM (
|
|
28
33
|
SELECT s.settled_amount_cents - COALESCE(SUM(r.refunded_amount_cents), 0) AS cents
|
|
29
|
-
FROM "%<schema>s".
|
|
30
|
-
WHERE s.user_id = $1 AND s.settled_at >= $2 GROUP BY s.id
|
|
34
|
+
FROM "%<schema>s".kiosk_settlements s LEFT JOIN "%<schema>s".kiosk_refunds r ON r.settlement_id = s.id
|
|
35
|
+
WHERE (s.user_id = $1 OR s.user_id IN (#{MERGED_SQL})) AND s.settled_at >= $2 GROUP BY s.id
|
|
31
36
|
HAVING s.settled_amount_cents > COALESCE(SUM(r.refunded_amount_cents), 0)
|
|
32
37
|
UNION ALL
|
|
33
|
-
SELECT value_cents FROM "%<schema>s".
|
|
38
|
+
SELECT value_cents FROM "%<schema>s".kiosk_visits
|
|
39
|
+
WHERE (user_id = $1 OR user_id IN (#{MERGED_SQL})) AND visited_at >= $2
|
|
34
40
|
) spend
|
|
35
41
|
SQL
|
|
42
|
+
private_constant :MERGED_SQL
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
end
|
|
@@ -15,11 +15,11 @@ module Kiosk
|
|
|
15
15
|
# RESERVED-PLANE REQUEST BODY => the published object that types it.
|
|
16
16
|
# The `/oauth/*` requests are form-encoded and have no schema.
|
|
17
17
|
BODY_SCHEMAS = {
|
|
18
|
-
'POST <endpoint>/auth/
|
|
19
|
-
'POST <endpoint>/auth/login' => 'auth.schema.json#/$defs/credentialRequest',
|
|
18
|
+
'POST <endpoint>/auth/rotate' => 'auth.schema.json#/$defs/rotationRequest',
|
|
20
19
|
'POST <endpoint>/auth/claim' => 'binding.schema.json#/$defs/claimRequest',
|
|
21
20
|
'POST <endpoint>/auth/unlink' => 'binding.schema.json#/$defs/unlinkRequest',
|
|
22
21
|
'POST <endpoint>/agents/kyc' => 'kyc.schema.json#/$defs/request',
|
|
22
|
+
'POST <endpoint>/kyc/callback' => 'kyc.schema.json#/$defs/callback',
|
|
23
23
|
'POST <endpoint>/pay' => 'mandates.schema.json#/$defs/payRequest'
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
@@ -35,7 +35,7 @@ module Kiosk
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# A missing member keeps the
|
|
38
|
+
# A missing member keeps the protocol's own `missing field:` sentence.
|
|
39
39
|
def validate_body!(body, exchange:)
|
|
40
40
|
return unless Kiosk.configuration.validate_requests
|
|
41
41
|
|
data/lib/kiosk/server/result.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
5
|
# The executor's internal carrier for a successful call; only {#to_payload}
|
|
6
|
-
# reaches the
|
|
6
|
+
# reaches the protocol, as the body.
|
|
7
7
|
class Result < Data.define(:kind, :payload, :next_cursor, :total)
|
|
8
8
|
KINDS = %i[rows value].freeze
|
|
9
9
|
|
|
@@ -7,6 +7,14 @@ module Kiosk
|
|
|
7
7
|
# `revoked?(agent_id:, iat:)`: a token whose `iat` is strictly before the
|
|
8
8
|
# agent's watermark is revoked.
|
|
9
9
|
module RevocationStores
|
|
10
|
+
# Revokes every token of `agent_id` issued so far. JWT `iat` is whole
|
|
11
|
+
# seconds, the check is `iat < watermark`, and the IdP dates a token no
|
|
12
|
+
# earlier than the watermark, so the new watermark is the second after
|
|
13
|
+
# both now and the old watermark.
|
|
14
|
+
def self.revoke_issued(agent_id, store: Kiosk.configuration.revocation_store)
|
|
15
|
+
store.revoke_all(agent_id, at: [Time.now.to_i, store.watermark_for(agent_id).to_i].max + 1)
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
# The default store: the `<schema>.revocations` table, shared by every
|
|
11
19
|
# process and kept across restarts. A token of an unlinked or deleted
|
|
12
20
|
# agent is revoked too.
|
|
@@ -50,7 +58,7 @@ module Kiosk
|
|
|
50
58
|
private
|
|
51
59
|
|
|
52
60
|
def connection = ::ActiveRecord::Base.lease_connection
|
|
53
|
-
def table(name) = %("#{Kiosk.configuration.schema}"
|
|
61
|
+
def table(name) = %("#{Kiosk.configuration.schema}".kiosk_#{name})
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
64
|
end
|
data/lib/kiosk/server/routing.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Kiosk
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# The endpoint of one verb route. The controller action that declares the
|
|
41
|
-
# verb answers the
|
|
41
|
+
# verb answers the protocol request; the name is looked up per request, so a
|
|
42
42
|
# development reload is served. A name that no action answers goes to
|
|
43
43
|
# {VerbController}, which refuses it.
|
|
44
44
|
class VerbRoute
|