kiosk-test-support 0.5.20 → 0.6.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: 82f1f47c2715da7a7ef6897038c156d7033991f159d7dea50446ee3af8933d01
4
- data.tar.gz: 0b06db9324f1ea8d0e7e11036f756db6e4eef3fffc9899961506e95f0f999183
3
+ metadata.gz: 7c7eb9fc194090087a2bd0a1b771861d165f3fea55a0fbfd832ba2d1d0037a08
4
+ data.tar.gz: a862919840551f535559671b236ac98a84dc420e07dba3c706c30d105032b4a8
5
5
  SHA512:
6
- metadata.gz: 4b1c2beec651bb6bb2836c28cb5440903d113867d91f33bce3bdd42625c5d34336b0916332e39b3d92b60d5633668074decb3c5c2169597fd6c036c7b274595f
7
- data.tar.gz: 66c9376f8e27a37bd455f503fa34f1617d8e7e790eb348fc44075494cc81e386f9e1ec23593a86f011781ec537909ec5a14a2feba01f7cb4fa4cee1684ccbbc9
6
+ metadata.gz: 49f01347f6643ef9032955d55064c8555000336efa5a507073d29e16cd41e2c12cd851407b59ac4bc74e1c4c9dd6272e0c9cbe75aada517746c4484f8d0783ad
7
+ data.tar.gz: dbe42526621ccd03a3153db7d2ed4338892915d8b6cd2975478eb4079e8034c7edcbf7a083298f5cab0f3b13e8337bf8cbe3d9a9e935b13ea8cfc53dfa38a622
data/CHANGELOG.md CHANGED
@@ -12,6 +12,48 @@ already written is edited.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.6.1] — 2026-10-22
16
+
17
+ - 2026-10-22: Version 0.6.1, the tree cut that matches skill 0.6.1.
18
+ - 2026-10-21: Drops the `kiosk-pow-equihash` and `puma` dependencies; the host's bundle brings them. `Wire` names the gem to add, and refuses a toll in another proof of work.
19
+
20
+ ## [0.6.0] — 2026-10-21
21
+
22
+ - 2026-10-21: The helpers make Ed25519 assistant keys and sign EdDSA; `Principal#key` replaces `#rsa_key`, and `Dpop.generate_key('ES256')` makes a P-256 key.
23
+ - 2026-10-21: Version 0.6.0, the tree cut that matches skill 0.6.0.
24
+
25
+ ## [0.5.27] — 2026-10-21
26
+
27
+ - 2026-10-21: `Assistant` registers at `/kiosk/oauth/register` and gets tokens by client assertion and DPoP; `login`, `proof_of_possession` and the challenge are gone.
28
+ - 2026-10-21: Version 0.5.27, the tree cut that matches skill 0.5.27.
29
+
30
+ ## [0.5.26] — 2026-10-21
31
+
32
+ - 2026-10-21: Version 0.5.26, the tree cut that matches skill 0.5.26.
33
+
34
+ ## [0.5.25] — 2026-10-21
35
+
36
+ - 2026-10-21: Version 0.5.25, the tree cut that matches skill 0.5.25.
37
+
38
+ ## [0.5.24] — 2026-10-21
39
+
40
+ - 2026-10-21: The gemspec depends on kiosk-server and kiosk-user-idp-devise, which the in-process helpers and `Person` require, so a bundle with this gem alone loads them.
41
+ - 2026-10-21: Version 0.5.24, the tree cut that matches skill 0.5.24.
42
+
43
+ ## [0.5.23] — 2026-10-21
44
+
45
+ - 2026-10-21: `Wire#dpop` replaces `Wire#bearer`: each call sends the token as `Authorization: DPoP` with a new proof. `Assistant#rotate` puts a new key on a principal.
46
+ - 2026-10-21: Version 0.5.23, the tree cut that matches skill 0.5.23.
47
+
48
+ ## [0.5.22] — 2026-10-21
49
+
50
+ - 2026-10-21: `Kiosk::TestHelpers::Mandate` signs the two AP2 v0.2 mandates; `Assistant#pay` and `Customer#pays` take a `checkout_jwt`.
51
+ - 2026-10-21: Version 0.5.22, the tree cut that matches skill 0.5.22.
52
+
53
+ ## [0.5.21] — 2026-10-21
54
+
55
+ - 2026-10-21: Version 0.5.21, the tree cut that matches skill 0.5.21.
56
+
15
57
  ## [0.5.20] — 2026-10-20
16
58
 
17
59
  - 2026-10-20: `InProcessOrigin` and `InProcessExecutor` call a verb through its route with a linked assistant's token; `InProcessWire` does the call.
data/README.md CHANGED
@@ -29,6 +29,8 @@ end
29
29
 
30
30
  Both harnesses pull `kiosk-test-support` as a transitive dependency.
31
31
 
32
+ Your bundle brings what the helpers drive: `puma` for `LiveServer` (`rails new` writes it), and `kiosk-pow-equihash` for the solver that pays Equihash tolls. `Wire` solves no other proof of work.
33
+
32
34
  ## DSL
33
35
 
34
36
  ```ruby
@@ -69,7 +71,7 @@ The `NullExecutor` is the zero-dependency fallback for unit-shaped tests; produc
69
71
 
70
72
  ## Driving the wire as an assistant
71
73
 
72
- `Kiosk::TestHelpers::Assistant` registers, pays proof-of-work tolls, queries, runs, pays with signed mandates and listens on `<endpoint>/events`, against any origin — a test's own live server or a deployment:
74
+ `Kiosk::TestHelpers::Assistant` registers its key as an OAuth client and gets DPoP-bound tokens, pays proof-of-work tolls, queries, runs, pays with closed AP2 mandates (SD-JWT) and listens on `<endpoint>/events`, against any origin — a test's own live server or a deployment:
73
75
 
74
76
  ```ruby
75
77
  require "kiosk/test_helpers/assistant"
@@ -90,7 +92,7 @@ Every answer carries its status, parsed body and headers. `Kiosk::TestHelpers::W
90
92
  ```ruby
91
93
  class Shopper < Kiosk::TestHelpers::Customer
92
94
  def orders(*skus) = does(:create_order, items: skus.map { { sku: _1, qty: 1 } })
93
- def pays_for(order) = pays(total: order["total_cents"], scope: "grocery", line_items: [{ order_id: order["order_id"] }])
95
+ def pays_for(order) = pays(order["checkout_jwt"])
94
96
  end
95
97
 
96
98
  class ShopStory < Kiosk::StoryTest
@@ -101,11 +103,11 @@ class ShopStory < Kiosk::StoryTest
101
103
  end
102
104
  ```
103
105
 
104
- - `a_customer(as:)` registers an assistant; `a_newcomer(as:)` holds a key and no account yet; `published(path)` reads what the origin shows anyone.
106
+ - `a_customer(as:)` registers an assistant; `a_newcomer(as:)` registers a key and acts for no person yet; `published(path)` reads what the origin shows anyone.
105
107
  - `Customer#asks` and `#does` answer an `Answer` (`ok?`, `refused?(code)`, `detail`, `hint`, `rows`, `header`, `tolls_paid`, `solved_toll`, `next_page`); `asks(…, unpaid: true)` or `asks(…, proofs: […])` leaves the toll to the story.
106
108
  - `Kiosk::StoryTest#assert_refused(answer, code)` asserts the wire refused the `Answer` with `code`.
107
- - `Customer#sets_up_payment` and `#requests_verification` run the engine's `payment_setup` and `request_kyc`, and `#hears_verification_passed` waits for the outcome of the check it asked for last; `#pays(total:, scope:, line_items:, currency:)` signs the intent, cart and payment mandates for a quote and pays; on `payment_setup_required` it calls `payment_setup` and pays once more.
108
- - `Customer#account`, `#role` and `#claims` read its credential; `#signs_back_in`, `#with_a_fresh_credential`, `#redeems(link_code)`, `#asks_to_be_linked`, `#polls` and `#collects` are the sign-in and linking ceremonies.
109
+ - `Customer#sets_up_payment` and `#requests_verification` run the engine's `payment_setup` and `request_kyc`, and `#hears_verification_passed` waits for the outcome of the check it asked for last; `#pays(checkout_jwt)` signs the two closed AP2 mandates for the checkout an action answered, unchanged, and pays; on `payment_setup_required` it calls `payment_setup` and pays once more.
110
+ - `Customer#account`, `#role` and `#claims` read its credential; `#signs_back_in` and `#with_a_fresh_credential` ask the token endpoint for a new token; `#redeems(link_code)`, `#asks_to_be_linked`, `#polls` and `#collects` are the linking ceremonies.
109
111
  - `Customer#listens_for`, `#follows(*topics, subject:, since:)` and `#hears(topic, about:, on:, **data)` read the event stream, holding each heard payload to the schema the origin publishes for its topic.
110
112
  - `a_person(email:, password:)` signs a person in on the operator's site through `kiosk-user-idp-devise`'s `DeviseSession`: `links(customer)`, `link_code`, `approves(user_code)`, `unlinks(customer)`, `visits(path)`.
111
113
 
@@ -6,6 +6,7 @@ require 'openssl'
6
6
  require 'socket'
7
7
  require 'uri'
8
8
  require 'websocket/driver'
9
+ require 'kiosk/test_helpers/dpop'
9
10
 
10
11
  module Kiosk
11
12
  module TestHelpers
@@ -35,14 +36,15 @@ module Kiosk
35
36
 
36
37
  attr_reader :url
37
38
 
38
- def initialize(base_url:, token:, path: '/kiosk/events', timeout: 10)
39
- uri = URI(base_url)
40
- tls = uri.scheme == 'https'
41
- @url = "#{tls ? 'wss' : 'ws'}://#{uri.host}:#{uri.port}#{path}"
42
- @io = open_socket(uri.hostname, uri.port, tls)
39
+ # `key` signs the DPoP proof of the upgrade; its `htu` is the events URL
40
+ # with the HTTP scheme, because the upgrade is an HTTP request there.
41
+ def initialize(base_url:, token:, key:, path: '/kiosk/events', timeout: 10)
42
+ http = URI.join(base_url, path)
43
+ @url = http.to_s.sub(/\Ahttp/, 'ws')
44
+ @io = open_socket(http.hostname, http.port, http.scheme == 'https')
43
45
  @frames = []
44
46
  @closed = false
45
- @driver = start_driver(token)
47
+ @driver = start_driver(Dpop.headers(token, key, method: 'GET', url: http.to_s))
46
48
  pump(timeout) { @frames.any? { it['type'] == 'welcome' } } || raise(Error, "#{@url} did not welcome the connection")
47
49
  end
48
50
 
@@ -98,9 +100,9 @@ module Kiosk
98
100
  ssl
99
101
  end
100
102
 
101
- def start_driver(token)
103
+ def start_driver(headers)
102
104
  driver = WebSocket::Driver.client(self)
103
- driver.set_header('Authorization', "Bearer #{token}")
105
+ headers.each { |name, value| driver.set_header(name, value) }
104
106
  driver.on(:message) { @frames << JSON.parse(it.data) }
105
107
  driver.on(:close) { @closed = true }
106
108
  driver.on(:error) { @closed = true }
@@ -6,12 +6,14 @@ require 'securerandom'
6
6
  require 'uri'
7
7
  require 'jwt'
8
8
  require 'kiosk/test_helpers/wire'
9
+ require 'kiosk/test_helpers/mandate'
9
10
  require 'kiosk/test_helpers/assistant/events'
10
11
 
11
12
  module Kiosk
12
13
  module TestHelpers
13
- # An assistant on the wire: registers, then queries, runs and pays as the
14
- # principal it registered, paying every proof-of-work toll it is asked for.
14
+ # An assistant on the wire: registers its key as an OAuth client, then
15
+ # queries, runs and pays as the principal its token names, paying every
16
+ # proof-of-work toll it is asked for.
15
17
  #
16
18
  # assistant = Kiosk::TestHelpers::Assistant.new(base_url: live_url)
17
19
  # rider = assistant.register!
@@ -19,8 +21,12 @@ module Kiosk
19
21
  class Assistant
20
22
  class RegistrationError < StandardError; end
21
23
 
22
- # The account an assistant registered, and the key that signs its mandates.
23
- Principal = Data.define(:agent_id, :user_id, :token, :rsa_key)
24
+ # The account an assistant acts for, its client_id (the agent_id), and
25
+ # the key that signs its client assertions, DPoP proofs and mandates.
26
+ Principal = Data.define(:agent_id, :user_id, :token, :key)
27
+
28
+ ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
29
+ DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code'
24
30
 
25
31
  attr_reader :wire
26
32
 
@@ -30,119 +36,133 @@ module Kiosk
30
36
 
31
37
  def base_url = wire.base_url
32
38
 
33
- # `pow:` is `:solve`, `:skip` (send no proof) or a String sent verbatim as
34
- # the proof; `wire_role:` puts a `role` the engine must ignore into the body.
35
- def register_raw(pow: :solve, wire_role: nil)
36
- register(pow:, wire_role:).first
39
+ # Registers `key` (RFC 7591). `pow:` is `:solve`, `:skip` (send no proof)
40
+ # or a String sent verbatim as the proof; `wire_role:` puts a `role` the
41
+ # engine must ignore into the body.
42
+ def register_raw(pow: :solve, wire_role: nil, key: Dpop.generate_key)
43
+ body = { jwks: jwks(key), token_endpoint_auth_method: 'private_key_jwt' }
44
+ body[:role] = wire_role if wire_role
45
+ case pow
46
+ when :solve then wire.post('/kiosk/oauth/register', body)
47
+ when :skip then unpaid.post('/kiosk/oauth/register', body)
48
+ else unpaid.post('/kiosk/oauth/register', body, 'Kiosk-PoW' => pow)
49
+ end
50
+ end
51
+
52
+ # A new key, registered: the principal has a client_id and no token yet.
53
+ def register_client!(pow: :solve)
54
+ key = Dpop.generate_key
55
+ resp = register_raw(pow:, key:)
56
+ raise RegistrationError, "registration expected 201, got #{resp.status}: #{resp.body.inspect}" unless resp.status == 201
57
+
58
+ Principal.new(agent_id: resp.body.fetch('client_id'), user_id: nil, token: nil, key: key)
37
59
  end
38
60
 
39
- def register!(pow: :solve)
40
- response, key = register(pow:)
41
- raise RegistrationError, "register! expected 201, got #{response.status}: #{response.body.inspect}" unless response.status == 201
61
+ # A new key, registered, holding its first token.
62
+ def register!(pow: :solve) = signed_in(register_client!(pow:))
42
63
 
43
- Principal.new(agent_id: response.body.fetch('agent_id'), user_id: response.body.fetch('user_id'),
44
- token: response.body.fetch('access_token'), rsa_key: key)
64
+ # The principal holding a new token from the client_credentials grant.
65
+ def signed_in(principal)
66
+ response = token(principal)
67
+ raise RegistrationError, "token request expected 200, got #{response.status}: #{response.body.inspect}" unless response.status == 200
68
+
69
+ access_token = response.body.fetch('access_token')
70
+ principal.with(token: access_token, user_id: JWT.decode(access_token, nil, false).first['sub'])
45
71
  end
46
72
 
47
- # The unauthenticated request that opens the account-binding ceremony (RFC 8628 §3.1).
48
- def device_authorization(client_id:, public_key:, **extra)
49
- wire.post_form('/kiosk/oauth/device_authorization',
50
- { 'client_id' => client_id, 'public_key' => public_key }.merge(extra.transform_keys(&:to_s)))
73
+ # Asks the token endpoint for a token of the principal's client (RFC 6749 §4.4).
74
+ def token(principal) = token_request(principal, 'grant_type' => 'client_credentials')
75
+
76
+ # Polls for the token a person grants on the operator's site (RFC 8628 §3.4).
77
+ def device_token(device_code, principal)
78
+ token_request(principal, 'grant_type' => DEVICE_CODE, 'device_code' => device_code)
79
+ end
80
+
81
+ # Opens the account-binding ceremony (RFC 8628 §3.1), with whatever else the caller sends.
82
+ def device_authorization(principal, **extra)
83
+ wire.post_form('/kiosk/oauth/device_authorization', assertion_params(principal).merge(extra.transform_keys(&:to_s)))
51
84
  end
52
85
 
53
86
  def kyc(principal, attestation_jws:)
54
- wire.post('/kiosk/agents/kyc', { kyc_jws: attestation_jws }, wire.bearer(principal.token))
87
+ wire.post('/kiosk/agents/kyc', { kyc_jws: attestation_jws }, credential(principal))
55
88
  end
56
89
 
57
90
  # `pay_tolls: false` answers a proof-of-work toll instead of paying it.
58
91
  def query(principal, name:, headers: {}, pay_tolls: true, **params)
59
- (pay_tolls ? wire : unpaid).get("/kiosk/#{name}", params, wire.bearer(principal.token).merge(headers))
92
+ (pay_tolls ? wire : unpaid).get("/kiosk/#{name}", params, credential(principal).merge(headers))
60
93
  end
61
94
 
62
95
  def run(principal, name:, headers: {}, **args)
63
- wire.post("/kiosk/#{name}", args, wire.bearer(principal.token).merge(headers))
96
+ wire.post("/kiosk/#{name}", args, credential(principal).merge(headers))
64
97
  end
65
98
 
66
- # The intent and cart mandates for a quote of `total` cents, for #pay.
67
- def mandates(principal, total:, scope:, line_items:, currency: 'eur')
68
- now = Time.now.to_i
69
- common = { user_id: principal.user_id, agent_id: principal.agent_id, iss: base_url, currency:, iat: now, exp: now + 600 }
70
- intent = common.merge(id: SecureRandom.uuid, scope:, cap_amount_cents: total)
71
- { intent:, cart: common.merge(id: SecureRandom.uuid, intent_mandate_id: intent[:id], total_amount_cents: total, line_items:) }
72
- end
73
-
74
- def pay(principal, intent:, cart:, payment_method: 'pm_demo')
75
- payment = payment_mandate(principal, cart:, payment_method:)
76
- pay_raw(principal, intent_jws: sign_mandate(principal, intent), cart_jws: sign_mandate(principal, cart),
77
- payment_jws: sign_mandate(principal, payment))
99
+ # Signs the two closed AP2 mandates for `checkout_jwt`, unchanged, and pays.
100
+ def pay(principal, checkout_jwt, instrument: 'on_file')
101
+ pay_raw(principal, **Mandate.pay(principal.key, checkout_jwt, instrument:))
78
102
  end
79
103
 
80
104
  # On a 402 `payment_setup_required` it calls `payment_setup`, as the hint
81
105
  # asks, and sends the same mandates once more. Against stripe-mock the
82
106
  # Customer that `payment_setup` maps already lists a card, so that answer settles.
83
- def pay_raw(principal, intent_jws:, cart_jws:, payment_jws:)
84
- body = { intent_mandate_jws: intent_jws, cart_mandate_jws: cart_jws, payment_mandate_jws: payment_jws }
85
- answer = wire.post('/kiosk/pay', body, wire.bearer(principal.token))
107
+ def pay_raw(principal, checkout:, payment:)
108
+ body = { checkout_mandate: checkout, payment_mandate: payment }
109
+ answer = wire.post('/kiosk/pay', body, credential(principal))
86
110
  return answer unless setup_required?(answer)
87
111
 
88
112
  run(principal, name: 'payment_setup')
89
- wire.post('/kiosk/pay', body, wire.bearer(principal.token))
113
+ wire.post('/kiosk/pay', body, credential(principal))
90
114
  end
91
115
 
92
- def payment_mandate(principal, cart:, payment_method: 'pm_demo')
93
- cart = cart.transform_keys(&:to_sym)
94
- now = Time.now.to_i
95
- { id: SecureRandom.uuid, cart_mandate_id: cart[:id], user_id: principal.user_id, agent_id: principal.agent_id,
96
- iss: cart[:iss], payment_method:, amount_cents: cart[:total_amount_cents], currency: cart[:currency],
97
- exp: now + 600, iat: now }
116
+ # Puts a new key on the principal's client (spec Section 5.7): the old
117
+ # key's assertion names the new key, and the new key signs its own. The
118
+ # answer's Principal holds the new key and a token bound to it.
119
+ def rotate(principal, new_key: Dpop.generate_key)
120
+ body = { client_assertion_type: ASSERTION_TYPE,
121
+ client_assertion: client_assertion(principal, new_key: Dpop.thumbprint(new_key)),
122
+ new_client_assertion: client_assertion(principal.with(key: new_key)), jwks: jwks(new_key) }
123
+ response = wire.post('/kiosk/auth/rotate', body)
124
+ return response unless response.status == 200
125
+
126
+ signed_in(principal.with(key: new_key))
98
127
  end
99
128
 
100
- def sign_mandate(principal, payload) = JWT.encode(payload, principal.rsa_key, 'RS256')
101
-
102
- # Signs in again with the key it holds.
103
- def login(key) = wire.post('/kiosk/auth/login', possession(key))
104
-
105
- # Redeems a link code a person minted on the operator's site.
106
- def claim(code, key) = wire.post('/kiosk/auth/claim', { code: }.merge(possession(key)))
107
-
108
- # Polls for the credential a person grants on the operator's site (RFC 8628 §3.4).
109
- def device_token(device_code, key)
110
- wire.post_form('/kiosk/oauth/token', 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
111
- 'device_code' => device_code, 'signed' => proof_of_possession(key))
112
- end
129
+ # Redeems a link code a person minted on the operator's site; answers 204.
130
+ def claim(code, principal) = wire.post('/kiosk/auth/claim', { code:, **assertion_params(principal) })
113
131
 
114
- # A proof that the assistant holds `key`, over a fresh challenge and bound to this origin.
115
- def proof_of_possession(key)
116
- _, challenge = wire.get_json('/kiosk/auth/challenge', public_key: key.public_key.to_pem)
117
- JWT.encode({ aud: base_url, nonce: challenge['challenge'], jti: SecureRandom.uuid, iat: Time.now.to_i }, key, 'RS256')
132
+ # A client assertion (RFC 7523 §3) that the principal's key signs for this
133
+ # origin. `claims` adds or replaces members, such as the `new_key` of a rotation.
134
+ def client_assertion(principal, **claims)
135
+ payload = { iss: principal.agent_id, sub: principal.agent_id, aud: base_url, exp: Time.now.to_i + 60,
136
+ jti: SecureRandom.uuid, **claims }
137
+ JWT.encode(payload, principal.key, Dpop.algorithm(principal.key))
118
138
  end
119
139
 
120
140
  # What the origin publishes about itself at `/kiosk/schema`.
121
141
  def schema = @schema ||= wire.get_json('/kiosk/schema').last
122
142
 
123
- def events(principal) = Events.new(base_url:, token: principal.token).tap { (@connections ||= []) << it }
143
+ def events(principal) = Events.new(base_url:, token: principal.token, key: principal.key).tap { (@connections ||= []) << it }
124
144
 
125
145
  # Closes every event connection it opened.
126
146
  def disconnect = @connections&.each(&:close)
127
147
 
128
148
  private
129
149
 
130
- def register(pow:, wire_role: nil)
131
- key = OpenSSL::PKey::RSA.generate(2048)
132
- body = possession(key)
133
- body[:role] = wire_role if wire_role
134
- response = case pow
135
- when :solve then wire.post('/kiosk/auth/register', body)
136
- when :skip then unpaid.post('/kiosk/auth/register', body)
137
- else unpaid.post('/kiosk/auth/register', body, 'Kiosk-PoW' => pow)
138
- end
139
- [response, key]
150
+ def jwks(key) = { keys: [Dpop.jwk(key)] }
151
+
152
+ def assertion_params(principal)
153
+ { 'client_assertion_type' => ASSERTION_TYPE, 'client_assertion' => client_assertion(principal) }
140
154
  end
141
155
 
142
- def possession(key) = { public_key: key.public_key.to_pem, signed: proof_of_possession(key) }
156
+ # A token request carries a DPoP proof of the registered key, with no token yet.
157
+ def token_request(principal, form)
158
+ wire.post_form('/kiosk/oauth/token', form.merge(assertion_params(principal)), wire.dpop(nil, principal.key))
159
+ end
143
160
 
144
161
  def unpaid = Wire.new(base_url:)
145
162
 
163
+ # The principal's token and key, which the wire signs a DPoP proof with.
164
+ def credential(principal) = wire.dpop(principal.token, principal.key)
165
+
146
166
  def setup_required?(answer)
147
167
  answer.status == 402 && answer.body.is_a?(Hash) && answer.body['code'] == 'payment_setup_required'
148
168
  end
@@ -40,10 +40,8 @@ module Kiosk
40
40
  class Customer
41
41
  attr_reader :principal
42
42
 
43
- # An assistant holding a key and no account at its origin yet.
44
- def self.newcomer(assistant)
45
- new(assistant, Assistant::Principal.new(agent_id: nil, user_id: nil, token: nil, rsa_key: OpenSSL::PKey::RSA.generate(2048)))
46
- end
43
+ # An assistant that registered its key here and acts for no person yet.
44
+ def self.newcomer(assistant) = new(assistant, assistant.register_client!)
47
45
 
48
46
  def initialize(assistant, principal)
49
47
  @assistant = assistant
@@ -60,15 +58,8 @@ module Kiosk
60
58
 
61
59
  def does(action, headers: {}, **args) = Answer.new(@assistant.run(principal, name: action.to_s, headers:, **args))
62
60
 
63
- # Signs intent, cart and payment mandates for `total` cents and pays.
64
- def pays(total:, scope:, line_items:, currency: 'eur')
65
- Answer.new(@assistant.pay(principal, **@assistant.mandates(principal, total:, scope:, line_items:, currency:)))
66
- end
67
-
68
- # Signs the `cart` an action answered with, unchanged, and pays.
69
- def pays_cart(cart, scope:, currency: cart['currency'])
70
- pays(total: cart['total_amount_cents'], scope:, line_items: cart['line_items'], currency:)
71
- end
61
+ # Signs the checkout and payment mandates for the `checkout_jwt` an action answered, and pays.
62
+ def pays(checkout_jwt) = Answer.new(@assistant.pay(principal, checkout_jwt))
72
63
 
73
64
  # The answer says whether a card is on file, or carries the page where the person saves one.
74
65
  def sets_up_payment = does(:payment_setup)
@@ -92,21 +83,25 @@ module Kiosk
92
83
  def account = claims['sub']
93
84
  def role = claims['role']
94
85
 
95
- def signs_back_in = Answer.new(@assistant.login(principal.rsa_key))
86
+ # Asks the token endpoint for a new token with the key it holds.
87
+ def signs_back_in = Answer.new(@assistant.token(principal))
96
88
 
97
- # This assistant holding the credential a fresh sign-in issues.
89
+ # This assistant holding the credential a fresh token request issues.
98
90
  def with_a_fresh_credential = holding(signs_back_in)
99
91
 
100
92
  # Redeems the link code a person handed over; the assistant then acts for that person.
101
- def redeems(link_code) = holding(Answer.new(@assistant.claim(link_code, principal.rsa_key)))
93
+ def redeems(link_code)
94
+ claimed = Answer.new(@assistant.claim(link_code, principal))
95
+ raise "#{origin} did not redeem #{link_code}: #{claimed}" unless claimed.ok?
102
96
 
103
- # Asks to be linked to a person's account; the answer carries the code the person approves.
104
- def asks_to_be_linked(client_id: 'kiosk-story')
105
- Answer.new(@assistant.device_authorization(client_id:, public_key: principal.rsa_key.public_key.to_pem))
97
+ with_a_fresh_credential
106
98
  end
107
99
 
100
+ # Asks to be linked to a person's account; the answer carries the code the person approves.
101
+ def asks_to_be_linked = Answer.new(@assistant.device_authorization(principal))
102
+
108
103
  # Asks once whether the person has approved `request`.
109
- def polls(request) = Answer.new(@assistant.device_token(request['device_code'], principal.rsa_key))
104
+ def polls(request) = Answer.new(@assistant.device_token(request['device_code'], principal))
110
105
 
111
106
  # This assistant holding the credential the person granted through `request`.
112
107
  def collects(request) = holding(polls(request))
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'digest'
5
+ require 'jwt'
6
+ require 'openssl'
7
+ require 'securerandom'
8
+ require 'uri'
9
+ require 'kiosk/server/eddsa'
10
+
11
+ module Kiosk
12
+ module TestHelpers
13
+ # The two headers of a call that carries an access token (RFC 9449): the
14
+ # token as `Authorization: DPoP`, and a proof that the token's key signs
15
+ # for this method and URL. A token request has no token yet: it sends
16
+ # only the proof, with no `ath`.
17
+ #
18
+ # Dpop.headers(token, key, method: 'GET', url: 'https://shop.example/kiosk/catalog')
19
+ # Dpop.headers(nil, key, method: 'POST', url: 'https://shop.example/kiosk/oauth/token')
20
+ module Dpop
21
+ module_function
22
+
23
+ # A new assistant key: Ed25519, or P-256 for `ES256`.
24
+ def generate_key(algorithm = 'EdDSA')
25
+ algorithm == 'ES256' ? OpenSSL::PKey::EC.generate('prime256v1') : OpenSSL::PKey.generate_key('ED25519')
26
+ end
27
+
28
+ # The JOSE algorithm the key signs with: EdDSA or ES256.
29
+ def algorithm(key) = Kiosk::Server::Jwk.algorithm(key)
30
+
31
+ def headers(token, key, method:, url:, iat: Time.now.to_i)
32
+ proof = { 'DPoP' => proof(token, key, method:, url:, iat:) }
33
+ token ? { 'Authorization' => "DPoP #{token}", **proof } : proof
34
+ end
35
+
36
+ # The same two headers as Rack env entries.
37
+ def env(token, key, method:, url:, iat: Time.now.to_i)
38
+ { 'HTTP_AUTHORIZATION' => "DPoP #{token}", 'HTTP_DPOP' => proof(token, key, method:, url:, iat:) }
39
+ end
40
+
41
+ # `url` loses its query and fragment: `htu` names the resource, not the arguments.
42
+ def proof(token, key, method:, url:, iat: Time.now.to_i)
43
+ uri = URI(url.to_s)
44
+ htu = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}#{uri.path}"
45
+ payload = { jti: SecureRandom.uuid, htm: method.to_s.upcase, htu: htu, iat: iat }
46
+ payload[:ath] = ath(token) if token
47
+ JWT.encode(payload, key, algorithm(key), { typ: 'dpop+jwt', jwk: jwk(key) })
48
+ end
49
+
50
+ def ath(token) = Base64.urlsafe_encode64(Digest::SHA256.digest(token.to_s), padding: false)
51
+
52
+ # The RFC 7638 thumbprint of the key: a token's `cnf.jkt`, a rotation's `new_key`.
53
+ def thumbprint(key) = Kiosk::Server::Jwk.thumbprint(key)
54
+
55
+ # The public JWK (RFC 7517) of the key: a proof's `jwk`, a registration's `jwks` member.
56
+ def jwk(key) = Kiosk::Server::Jwk.public_members(key)
57
+ end
58
+ end
59
+ end
@@ -9,6 +9,7 @@ require 'rack/mock'
9
9
  require 'uri'
10
10
  require 'kiosk/server'
11
11
  require 'kiosk/test_helpers/wire'
12
+ require 'kiosk/test_helpers/dpop'
12
13
 
13
14
  module Kiosk
14
15
  module TestHelpers
@@ -24,10 +25,10 @@ module Kiosk
24
25
 
25
26
  # The verb's answer, or the wire error its refusal names.
26
27
  def call(name, kind:, params:, identity:)
27
- token = token_for(identity) if identity
28
- response = request(name, kind, params, token)
28
+ credential = credential_for(identity) if identity
29
+ response = request(name, kind, params, credential)
29
30
  challenges = toll_challenges(response)
30
- response = request(name, kind, params, token, Wire.solve(challenges)) if challenges
31
+ response = request(name, kind, params, credential, Wire.solve(challenges)) if challenges
31
32
  answer(response)
32
33
  end
33
34
 
@@ -35,27 +36,40 @@ module Kiosk
35
36
 
36
37
  def app = @app || ::Rails.application
37
38
 
38
- def request(name, kind, params, token, proofs = nil)
39
- env = credentials(token, proofs)
39
+ def request(name, kind, params, credential, proofs = nil)
40
+ query = kind.to_sym == :query
40
41
  url = "#{Kiosk.current_issuer.to_s.chomp('/')}#{Kiosk.configuration.mount_path}/#{name}"
41
- return Rack::MockRequest.new(app).get(query_url(url, params), env) if kind.to_sym == :query
42
+ env = credentials(credential, query ? 'GET' : 'POST', url, proofs)
43
+ return Rack::MockRequest.new(app).get(query_url(url, params), env) if query
42
44
 
43
45
  Rack::MockRequest.new(app).post(url, env.merge(input: JSON.generate(params), 'CONTENT_TYPE' => 'application/json'))
44
46
  end
45
47
 
46
- def credentials(token, proofs)
47
- { 'HTTP_AUTHORIZATION' => ("Bearer #{token}" if token),
48
- 'HTTP_KIOSK_POW' => (JSON.generate(proofs) if proofs) }.compact
48
+ # The access token with a DPoP proof its key signs for this request.
49
+ def credentials(credential, method, url, proofs)
50
+ env = credential ? Dpop.env(credential.token, credential.key, method:, url:) : {}
51
+ env['HTTP_KIOSK_POW'] = JSON.generate(proofs) if proofs
52
+ env
49
53
  end
50
54
 
51
55
  def query_url(url, params) = params.empty? ? url : "#{url}?#{URI.encode_www_form(params)}"
52
56
 
53
- # The token of an assistant linked to the principal for this call.
54
- def token_for(identity)
55
- Kiosk::Server::AccountBinding.bind!(
56
- public_key_pem: OpenSSL::PKey::RSA.generate(2048).public_key.to_pem,
57
- user_id: identity.user_id, requested_role: identity.role
58
- ).fetch(:access_token)
57
+ # The token and key of an assistant registered and linked to the principal for this call.
58
+ def credential_for(identity)
59
+ key = Dpop.generate_key
60
+ client = Kiosk::Server::AccountBinding.bind!(agent_id: register(key).id, user_id: identity.user_id,
61
+ requested_role: identity.role)
62
+ token = Kiosk::Server::AgentIdentityProviders::DefaultAgentIdp.new.issue(agent_id: client.id, role: client.role)
63
+ Wire::DpopCredential.new(token:, key:)
64
+ end
65
+
66
+ # Registers the key as a client, and pays the registration toll if the origin asks for one.
67
+ def register(key)
68
+ metadata = { 'jwks' => { 'keys' => [Kiosk::Server::AssistantKey.to_jwk(key)] } }
69
+ Kiosk::Server::ClientRegistration.call(metadata)
70
+ rescue Kiosk::Server::Errors::PowRequired => e
71
+ header = JSON.generate(Wire.solve(JSON.parse(JSON.generate(e.challenges))))
72
+ Kiosk::Server::ClientRegistration.call(metadata, pow: Kiosk::Server::PowGate.proofs_from_header(header))
59
73
  end
60
74
 
61
75
  def toll_challenges(response)
@@ -24,7 +24,7 @@ module Kiosk
24
24
  class Provider < Kiosk::KycProviders::Base
25
25
  Check = Data.define(:subject, :audience, :nonce, :callback_url)
26
26
 
27
- def self.key = @key ||= OpenSSL::PKey::RSA.generate(2048)
27
+ def self.key = @key ||= OpenSSL::PKey.generate_key('ED25519')
28
28
 
29
29
  def initialize
30
30
  super
@@ -47,7 +47,7 @@ module Kiosk
47
47
  def attest(subject, audience, attributes)
48
48
  now = Time.now.to_i
49
49
  JWT.encode({ sub: subject.to_s, level: 'verified', iss: ISSUER, aud: audience.to_s, attributes:,
50
- iat: now, exp: now + 3600 }, self.class.key, 'RS256')
50
+ iat: now, exp: now + 3600 }, self.class.key, 'EdDSA')
51
51
  end
52
52
  end
53
53
 
@@ -87,7 +87,7 @@ module Kiosk
87
87
  @kyc_provider = Provider.new
88
88
  config.kyc_provider = @kyc_provider
89
89
  config.kyc_issuer = ISSUER
90
- config.kyc_public_key = Provider.key.public_key
90
+ config.kyc_public_key = Provider.key
91
91
  end
92
92
 
93
93
  def restore_kyc_provider
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma'
4
3
  require 'socket'
4
+ begin
5
+ require 'puma'
6
+ rescue LoadError
7
+ raise LoadError, "Kiosk::TestHelpers::LiveServer serves the app with Puma. Add gem 'puma' to the Gemfile."
8
+ end
5
9
  require 'kiosk/test_helpers/assistant'
6
10
 
7
11
  module Kiosk
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'digest'
5
+ require 'json'
6
+ require 'jwt'
7
+ require 'securerandom'
8
+ require 'kiosk/test_helpers/dpop'
9
+
10
+ module Kiosk
11
+ module TestHelpers
12
+ # The two closed AP2 v0.2 mandates an assistant signs to pay an operator's
13
+ # checkout, as compact SD-JWTs: what an assistant following the skill sends.
14
+ module Mandate
15
+ module_function
16
+
17
+ # @param checkout_jwt [String] the checkout an action answered, unchanged
18
+ # @return [Hash] `checkout:` and `payment:`, each a compact SD-JWT
19
+ def pay(key, checkout_jwt, instrument: 'on_file', lifetime: 600)
20
+ checkout = JWT.decode(checkout_jwt, nil, false).first
21
+ checkout_hash = digest(checkout_jwt)
22
+ now = Time.now.to_i
23
+ times = { iat: now, exp: now + lifetime }
24
+ { checkout: sign(key, { vct: 'mandate.checkout.1', checkout_jwt:, checkout_hash:, **times }),
25
+ payment: sign(key, { vct: 'mandate.payment.1', transaction_id: checkout_hash, payee: checkout['merchant'],
26
+ payment_amount: { amount: checkout['total_amount_cents'], currency: checkout['currency'] },
27
+ payment_instrument: { id: instrument, type: 'card' }, **times }) }
28
+ end
29
+
30
+ # A closed mandate: an SD-JWT whose one disclosure is `content`.
31
+ def sign(key, content)
32
+ disclosure = b64(JSON.generate([b64(SecureRandom.random_bytes(16)), content]))
33
+ jwt = JWT.encode({ _sd_alg: 'sha-256', delegate_payload: [{ '...' => digest(disclosure) }] }, key, Dpop.algorithm(key),
34
+ { typ: 'dc+sd-jwt' })
35
+ "#{jwt}~#{disclosure}~"
36
+ end
37
+
38
+ # The disclosed mandate of `sd_jwt`, unverified.
39
+ def content(sd_jwt) = JSON.parse(Base64.urlsafe_decode64(sd_jwt.split('~')[1])).last
40
+
41
+ def digest(text) = b64(Digest::SHA256.digest(text))
42
+
43
+ def b64(bytes) = Base64.urlsafe_encode64(bytes, padding: false)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiosk
4
+ module TestHelpers
5
+ # The solver for a proof-of-work challenge. kiosk-pow-equihash ships the one
6
+ # solver here, and the host's bundle brings that gem.
7
+ module PowSolver
8
+ def self.for(challenge)
9
+ alg = challenge['alg']
10
+ raise ArgumentError, "The origin asks for a #{alg.inspect} proof of work. Wire solves only Equihash." unless alg == 'equihash'
11
+
12
+ require 'kiosk/pow/equihash/solver'
13
+ Kiosk::Pow::Equihash
14
+ rescue LoadError
15
+ raise LoadError, "Wire solves Equihash tolls with kiosk-pow-equihash. Add gem 'kiosk-pow-equihash' to the Gemfile."
16
+ end
17
+ end
18
+ end
19
+ end
@@ -18,7 +18,7 @@ module Kiosk
18
18
  # A newly registered assistant, as `as` (a demo's own Customer subclass).
19
19
  def a_customer(as: Customer) = as.new(assistant, register)
20
20
 
21
- # An assistant holding a key and no account here yet.
21
+ # An assistant that registered its key here and acts for no person yet.
22
22
  def a_newcomer(as: Customer) = as.newcomer(assistant)
23
23
 
24
24
  # Someone with an account on the operator's site, signed in there.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kiosk
4
4
  module TestHelpers
5
- VERSION = '0.5.20'
5
+ VERSION = '0.6.1'
6
6
  end
7
7
  end
@@ -3,7 +3,8 @@
3
3
  require 'json'
4
4
  require 'net/http'
5
5
  require 'uri'
6
- require 'kiosk/pow/equihash/solver'
6
+ require 'kiosk/test_helpers/dpop'
7
+ require 'kiosk/test_helpers/pow_solver'
7
8
 
8
9
  module Kiosk
9
10
  module TestHelpers
@@ -11,7 +12,7 @@ module Kiosk
11
12
  # `pay_tolls: true` it solves a proof-of-work toll and sends the request once more.
12
13
  #
13
14
  # wire = Kiosk::TestHelpers::Wire.new(base_url: "http://127.0.0.1:3006")
14
- # status, problem = wire.post_json("/kiosk/edit_listing", { listing_id: "junk" }, Wire.bearer(token))
15
+ # status, problem = wire.post_json("/kiosk/edit_listing", { listing_id: "junk" }, wire.dpop(token, key))
15
16
  class Wire
16
17
  # One answer: a body that is not JSON reads as `{}`, an unreachable origin as status 0.
17
18
  Response = Data.define(:status, :headers, :body, :raw_body, :proofs) do
@@ -42,10 +43,15 @@ module Kiosk
42
43
  http
43
44
  end
44
45
 
45
- def self.bearer(token) = { 'Authorization' => "Bearer #{token}" }
46
+ # An access token and the key it is bound to. As a header value, the wire
47
+ # sends it as `Authorization: DPoP <token>` with a proof for that request;
48
+ # with no token, as the proof alone (a token request).
49
+ DpopCredential = Data.define(:token, :key)
50
+
51
+ def self.dpop(token, key) = { 'Authorization' => DpopCredential.new(token:, key:) }
46
52
 
47
53
  # A solved proof of work for each challenge, as the `Kiosk-PoW` header carries them.
48
- def self.solve(challenges) = challenges.map { { challenge: it, nonce: Kiosk::Pow::Equihash.solve(it) } }
54
+ def self.solve(challenges) = challenges.map { { challenge: it, nonce: PowSolver.for(it).solve(it) } }
49
55
 
50
56
  attr_reader :base_url
51
57
 
@@ -56,7 +62,7 @@ module Kiosk
56
62
  @read_timeout = read_timeout
57
63
  end
58
64
 
59
- def bearer(token) = self.class.bearer(token)
65
+ def dpop(token, key) = self.class.dpop(token, key)
60
66
 
61
67
  def post_json(path, body = {}, headers = {}) = post(path, body, headers).then { [it.status, it.body] }
62
68
 
@@ -97,10 +103,8 @@ module Kiosk
97
103
  challenges if challenges.is_a?(Array) && challenges.any?
98
104
  end
99
105
 
100
- def deliver(klass, uri, body, headers)
101
- request = klass.new(uri, body.nil? ? headers : { 'Content-Type' => 'application/json' }.merge(headers))
102
- request.body = JSON.generate(body) unless body.nil?
103
- yield request if block_given?
106
+ def deliver(klass, uri, body, headers, &)
107
+ request = build(klass, uri, body, headers, &)
104
108
  begin
105
109
  answer = self.class.http_for(uri, open_timeout: @open_timeout, read_timeout: @read_timeout).request(request)
106
110
  rescue StandardError => e
@@ -109,6 +113,25 @@ module Kiosk
109
113
  response(answer)
110
114
  end
111
115
 
116
+ def build(klass, uri, body, headers)
117
+ headers = { 'Content-Type' => 'application/json' }.merge(headers) unless body.nil?
118
+ request = klass.new(uri, signed(headers, klass::METHOD, uri))
119
+ request.body = JSON.generate(body) unless body.nil?
120
+ yield request if block_given?
121
+ request
122
+ end
123
+
124
+ # Each delivery signs its own DPoP proof, so a retry never reuses one.
125
+ def signed(headers, method, uri)
126
+ headers.each_with_object({}) do |(name, value), out|
127
+ if value.is_a?(DpopCredential)
128
+ out.merge!(Dpop.headers(value.token, value.key, method:, url: uri.to_s))
129
+ else
130
+ out[name] = value
131
+ end
132
+ end
133
+ end
134
+
112
135
  def response(answer)
113
136
  raw_body = answer.body.to_s
114
137
  body = begin
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kiosk-test-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.20
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Pirozhkov
@@ -35,7 +35,7 @@ dependencies:
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '2.0'
38
+ version: '2.10'
39
39
  - - "<"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '4.0'
@@ -45,7 +45,7 @@ dependencies:
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '2.0'
48
+ version: '2.10'
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '4.0'
@@ -55,42 +55,42 @@ dependencies:
55
55
  requirements:
56
56
  - - "~>"
57
57
  - !ruby/object:Gem::Version
58
- version: 0.5.0
58
+ version: 0.6.0
59
59
  type: :runtime
60
60
  prerelease: false
61
61
  version_requirements: !ruby/object:Gem::Requirement
62
62
  requirements:
63
63
  - - "~>"
64
64
  - !ruby/object:Gem::Version
65
- version: 0.5.0
65
+ version: 0.6.0
66
66
  - !ruby/object:Gem::Dependency
67
- name: kiosk-pow-equihash
67
+ name: kiosk-server
68
68
  requirement: !ruby/object:Gem::Requirement
69
69
  requirements:
70
70
  - - "~>"
71
71
  - !ruby/object:Gem::Version
72
- version: 0.5.0
72
+ version: 0.6.0
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
76
76
  requirements:
77
77
  - - "~>"
78
78
  - !ruby/object:Gem::Version
79
- version: 0.5.0
79
+ version: 0.6.0
80
80
  - !ruby/object:Gem::Dependency
81
- name: puma
81
+ name: kiosk-user-idp-devise
82
82
  requirement: !ruby/object:Gem::Requirement
83
83
  requirements:
84
- - - ">="
84
+ - - "~>"
85
85
  - !ruby/object:Gem::Version
86
- version: '5.0'
86
+ version: 0.6.0
87
87
  type: :runtime
88
88
  prerelease: false
89
89
  version_requirements: !ruby/object:Gem::Requirement
90
90
  requirements:
91
- - - ">="
91
+ - - "~>"
92
92
  - !ruby/object:Gem::Version
93
- version: '5.0'
93
+ version: 0.6.0
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: websocket-driver
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -130,10 +130,12 @@ description: |
130
130
  mandates and listens on the event stream. `Kyc` stands in for the
131
131
  operator's KYC provider, and `StripeMock` fronts a local stripe-mock.
132
132
 
133
- No Postgres, no Rails, no test-framework dependency.
134
- `InProcessExecutor` and `Conformance::InProcessOrigin` run against
135
- the host's kiosk-server and are required explicitly; this gem does
136
- not depend on kiosk-server.
133
+ `InProcessExecutor`, `InProcessWire` and `Conformance::InProcessOrigin`
134
+ run against kiosk-server, and `Person` signs in through
135
+ kiosk-user-idp-devise; both are dependencies. The host's bundle brings
136
+ the rest: the Minitest and RSpec adapters use the test framework the
137
+ host runs, `LiveServer` serves the app with Puma, and `Wire` solves
138
+ Equihash tolls with kiosk-pow-equihash.
137
139
  email:
138
140
  - hello@fili.pp.ru
139
141
  executables: []
@@ -163,14 +165,17 @@ files:
163
165
  - lib/kiosk/test_helpers/conformance/verb.rb
164
166
  - lib/kiosk/test_helpers/customer.rb
165
167
  - lib/kiosk/test_helpers/descriptor_examples.rb
168
+ - lib/kiosk/test_helpers/dpop.rb
166
169
  - lib/kiosk/test_helpers/errors.rb
167
170
  - lib/kiosk/test_helpers/in_process_executor.rb
168
171
  - lib/kiosk/test_helpers/in_process_wire.rb
169
172
  - lib/kiosk/test_helpers/journey.rb
170
173
  - lib/kiosk/test_helpers/kyc.rb
171
174
  - lib/kiosk/test_helpers/live_server.rb
175
+ - lib/kiosk/test_helpers/mandate.rb
172
176
  - lib/kiosk/test_helpers/null_executor.rb
173
177
  - lib/kiosk/test_helpers/person.rb
178
+ - lib/kiosk/test_helpers/pow_solver.rb
174
179
  - lib/kiosk/test_helpers/small_equihash.rb
175
180
  - lib/kiosk/test_helpers/story.rb
176
181
  - lib/kiosk/test_helpers/stripe_mock.rb