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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +196 -0
  3. data/README.md +192 -138
  4. data/app/channels/kiosk_events.rb +30 -17
  5. data/app/controllers/kiosk/server/application_controller.rb +0 -1
  6. data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
  7. data/app/controllers/kiosk/server/auth_controller.rb +21 -65
  8. data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
  9. data/app/controllers/kiosk/server/discovery_controller.rb +24 -7
  10. data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
  11. data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
  12. data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
  13. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +17 -34
  14. data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
  15. data/app/controllers/kiosk/server/oauth_token_controller.rb +36 -32
  16. data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
  17. data/app/controllers/kiosk/server/wire_controller.rb +2 -2
  18. data/app/models/kiosk/checkout_mandate.rb +20 -0
  19. data/app/models/kiosk/settlement.rb +1 -3
  20. data/app/models/kiosk/visit.rb +0 -2
  21. data/app/views/kiosk/server/assistants/show.html.erb +20 -24
  22. data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
  23. data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
  24. data/app/views/layouts/kiosk/server/application.html.erb +14 -0
  25. data/lib/generators/kiosk/install/install_generator.rb +25 -69
  26. data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
  27. data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
  28. data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
  29. data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
  30. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -15
  31. data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
  32. data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
  33. data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
  34. data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
  35. data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
  36. data/lib/kiosk/payable.rb +19 -25
  37. data/lib/kiosk/server/account_binding.rb +32 -62
  38. data/lib/kiosk/server/account_holder_gate.rb +3 -1
  39. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
  40. data/lib/kiosk/server/agent_rotation.rb +62 -0
  41. data/lib/kiosk/server/argument_decoder.rb +2 -2
  42. data/lib/kiosk/server/assistant_accounts.rb +38 -0
  43. data/lib/kiosk/server/assistant_key.rb +86 -0
  44. data/lib/kiosk/server/auth_toll.rb +6 -6
  45. data/lib/kiosk/server/binding_module_gate.rb +1 -1
  46. data/lib/kiosk/server/checkout.rb +57 -0
  47. data/lib/kiosk/server/client.rb +61 -0
  48. data/lib/kiosk/server/client_assertion.rb +154 -0
  49. data/lib/kiosk/server/client_registration.rb +98 -0
  50. data/lib/kiosk/server/column_spending_cap.rb +1 -1
  51. data/lib/kiosk/server/configuration_extension.rb +64 -75
  52. data/lib/kiosk/server/current_request.rb +1 -1
  53. data/lib/kiosk/server/device_authorization.rb +1 -1
  54. data/lib/kiosk/server/device_authorization_stores.rb +1 -1
  55. data/lib/kiosk/server/device_code_grant.rb +38 -69
  56. data/lib/kiosk/server/dpop.rb +126 -0
  57. data/lib/kiosk/server/eddsa.rb +38 -0
  58. data/lib/kiosk/server/engine.rb +16 -13
  59. data/lib/kiosk/server/errors.rb +28 -7
  60. data/lib/kiosk/server/event_stores.rb +2 -2
  61. data/lib/kiosk/server/events.rb +4 -3
  62. data/lib/kiosk/server/events_connection.rb +38 -10
  63. data/lib/kiosk/server/events_replay.rb +79 -0
  64. data/lib/kiosk/server/executor/repeat.rb +13 -12
  65. data/lib/kiosk/server/executor/spending_cap.rb +17 -76
  66. data/lib/kiosk/server/executor.rb +86 -105
  67. data/lib/kiosk/server/failure_log.rb +3 -3
  68. data/lib/kiosk/server/handler_mixin.rb +9 -9
  69. data/lib/kiosk/server/handler_registrations.rb +1 -1
  70. data/lib/kiosk/server/handler_serving.rb +11 -11
  71. data/lib/kiosk/server/headers.rb +3 -3
  72. data/lib/kiosk/server/identity_resolution.rb +12 -3
  73. data/lib/kiosk/server/jwk.rb +83 -0
  74. data/lib/kiosk/server/jwks.rb +6 -0
  75. data/lib/kiosk/server/jwt_issuer.rb +7 -13
  76. data/lib/kiosk/server/kyc.rb +5 -7
  77. data/lib/kiosk/server/kyc_verifier.rb +28 -29
  78. data/lib/kiosk/server/link_code.rb +9 -18
  79. data/lib/kiosk/server/linked_assistants.rb +22 -17
  80. data/lib/kiosk/server/mandate_verifier.rb +97 -139
  81. data/lib/kiosk/server/money.rb +41 -0
  82. data/lib/kiosk/server/oauth_answers.rb +31 -0
  83. data/lib/kiosk/server/oauth_error.rb +33 -0
  84. data/lib/kiosk/server/open_api.rb +31 -29
  85. data/lib/kiosk/server/page.rb +1 -1
  86. data/lib/kiosk/server/payment_claim.rb +47 -78
  87. data/lib/kiosk/server/pow_gate.rb +12 -33
  88. data/lib/kiosk/server/pow_spent_stores.rb +1 -1
  89. data/lib/kiosk/server/receipts.rb +30 -0
  90. data/lib/kiosk/server/reputation_factors.rb +12 -5
  91. data/lib/kiosk/server/request_validation.rb +3 -3
  92. data/lib/kiosk/server/result.rb +1 -1
  93. data/lib/kiosk/server/revocation_stores.rb +9 -1
  94. data/lib/kiosk/server/routing.rb +1 -1
  95. data/lib/kiosk/server/schemas/auth.schema.json +369 -58
  96. data/lib/kiosk/server/schemas/binding.schema.json +24 -59
  97. data/lib/kiosk/server/schemas/kyc.schema.json +40 -78
  98. data/lib/kiosk/server/schemas/mandates.schema.json +123 -39
  99. data/lib/kiosk/server/schemas/pow.schema.json +2 -2
  100. data/lib/kiosk/server/sd_jwt.rb +104 -0
  101. data/lib/kiosk/server/session_context.rb +2 -2
  102. data/lib/kiosk/server/signing_key.rb +18 -52
  103. data/lib/kiosk/server/tolled_calls.rb +46 -0
  104. data/lib/kiosk/server/verb_wire.rb +4 -4
  105. data/lib/kiosk/server/version.rb +1 -1
  106. data/lib/kiosk/server/well_known.rb +272 -205
  107. data/lib/kiosk/server/wire_rendering.rb +2 -2
  108. data/lib/kiosk/server.rb +8 -2
  109. data/lib/kiosk/skill_link.rb +6 -2
  110. data/listen.py +38 -8
  111. metadata +34 -24
  112. data/app/models/kiosk/cart_mandate.rb +0 -21
  113. data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
  114. data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
  115. data/lib/kiosk/server/agent_login.rb +0 -49
  116. data/lib/kiosk/server/agent_registration.rb +0 -88
  117. data/lib/kiosk/server/auth_challenge.rb +0 -33
  118. data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
  119. data/lib/kiosk/server/pop_verifier.rb +0 -87
  120. data/lib/kiosk/server/schema_definitions.rb +0 -351
  121. data/lib/kiosk/server/settled_spend.rb +0 -21
  122. 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 cart against the operator's price,
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 cart line-item key naming the row, e.g. "order_id"
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(cart_mandate, payment_method: nil) = charge_row(claim!(cart_mandate), cart_mandate, payment_method)
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 cart names this row.
42
- def settlements(id) = Kiosk::Settlement.joins(:cart_mandate).merge(Kiosk::CartMandate.referencing(@reference => id))
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, cart_mandate, payment_method)
62
- settled = charge(id, cart_mandate, payment_method)
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, cart_mandate, payment_method)
69
- @psp.capture(cart_mandate, payment_method: payment_method)
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!(cart)
79
- id = referenced_id(cart)
80
- refuse_unclaimable!(id, cart.user_id.to_s) unless @row.take(id, cart.user_id.to_s, from: UNPAID, to: PAYING).positive?
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!(cart, id)
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 one row the cart's line items name, in the operator's currency.
91
- def referenced_id(cart)
92
- deny "cart currency #{cart.currency.inspect} rejected — this operator prices in #{@currency.upcase}" unless cart.currency.to_s.downcase == @currency
93
- Lines.reference(cart.line_items, @reference)
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 "cart total #{total} does not equal this operator's price #{price} for #{noun} #{id} — " \
106
- 'sign the `cart` that the action which created it returned'
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.cart_price_checker or
111
- raise Errors::ConfigurationError, "PaymentClaim needs c.cart_price_checker, the operator's price"
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 another chain. Send that chain again, " \
127
- 'byte for byte: it cannot charge twice. Do not sign a new chain. ' \
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 cart's line items: the one row they name, and the priced lines' sum.
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 = Array(line_items).filter_map { |li| li[key] }.map(&:to_s).uniq
143
- deny "cart line_items must reference exactly one #{key}" unless refs.size == 1
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
- def deny(message)
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 a chain whose capture did not complete. The row stays with
171
- # the newest unfinished chain, and the PSP keys the capture by the cart
172
- # mandate id, so the repeat cannot charge twice.
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(cart_mandate, payment_method: nil) = charge_row(reclaim!(cart_mandate), cart_mandate, payment_method)
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 this repeated chain claimed, or the row it may claim now.
179
- def reclaim!(cart)
180
- id = referenced_id(cart)
181
- payer = cart.user_id.to_s
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!(cart) if status == UNPAID
184
- return id if [PAYING, PAID].include?(status) && !settled?(id) && unfinished_chain(id, payer) == cart.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 signed id of the newest cart for this row that is neither settled nor declined.
190
- def unfinished_chain(id, payer)
191
- Kiosk::CartMandate.referencing(@reference => id).where(user_id: payer, declined_at: nil)
192
- .where.missing(:settlements).order(created_at: :desc, id: :desc).pick(:mandate_id)
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".refunds (settlement_id, psp_reference, refunded_amount_cents, refunded_at)
212
- SELECT id, $1, $2, $3 FROM "%<schema>s".settlements WHERE psp_reference = $4
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
- # `cart_mandate`, so `c.after_payment` reads the row's `settlement`. A
222
- # failure here never surfaces on the wire: the log carries it.
223
- def settled(cart_mandate)
224
- id = Lines.reference(cart_mandate.line_items, @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
- # It is paid once per access token: after one paid toll, every call made
10
- # with that token proceeds until the token expires. With no
11
- # `reputation_policy` it returns `:proceed` and touches nothing;
12
- # `kiosk-reputation` is required only when a policy is set.
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 wire name
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 -- the wire controller and the specs pass these six by name
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? || session_paid?(config, identity)
29
+ return :proceed if policy.nil?
30
30
 
31
- secret = pow_secret!(config)
32
- spec = toll_for(policy, identity, command.to_sym)
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 { paid(config, identity) }
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."
@@ -89,7 +89,7 @@ module Kiosk
89
89
  end
90
90
 
91
91
  def connection = ::ActiveRecord::Base.lease_connection
92
- def table = %("#{Kiosk.configuration.schema}".pow_spent)
92
+ def table = %("#{Kiosk.configuration.schema}".kiosk_pow_spent)
93
93
  end
94
94
  end
95
95
  end
@@ -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 window.
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.empty.with(settled_purchases_count: count, settled_purchases_cents: cents)
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".settlements s LEFT JOIN "%<schema>s".refunds r ON r.settlement_id = s.id
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".visits WHERE user_id = $1 AND visited_at >= $2
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/register' => 'auth.schema.json#/$defs/credentialRequest',
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 wire's own `missing field:` sentence.
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
 
@@ -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 wire, as the body.
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}".#{name})
61
+ def table(name) = %("#{Kiosk.configuration.schema}".kiosk_#{name})
54
62
  end
55
63
  end
56
64
  end
@@ -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 wire request; the name is looked up per request, so a
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