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
@@ -4,7 +4,7 @@ require 'cgi'
4
4
 
5
5
  module Kiosk
6
6
  module Server
7
- # How a wire answer is written: headers, cache policy, body. Success is the
7
+ # How a protocol answer is written: headers, cache policy, body. Success is the
8
8
  # verb's payload; a refusal is an RFC 9457 problem document.
9
9
  module WireRendering
10
10
  private
@@ -108,7 +108,7 @@ module Kiosk
108
108
  when 'pow_required'
109
109
  %(Kiosk-PoW realm="#{issuer}")
110
110
  when 'payment_setup_required'
111
- %(Payment realm="#{issuer}", method="ap2")
111
+ %(Kiosk-Payment-Setup realm="#{issuer}")
112
112
  end
113
113
  end
114
114
  end
data/lib/kiosk/server.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # kiosk-server: the Rails engine, its wire, auth and discovery controllers, and
3
+ # kiosk-server: the Rails engine, its protocol, auth and discovery controllers, and
4
4
  # the install generator. See https://kiosk.tech.
5
5
 
6
6
  require 'kiosk'
@@ -10,12 +10,18 @@ require 'active_support/core_ext' # the core extensions a Rails host loads, whic
10
10
  require 'zeitwerk'
11
11
 
12
12
  Zeitwerk::Loader.for_gem_extension(Kiosk).setup
13
+ # Registers the JOSE algorithm EdDSA with ruby-jwt.
14
+ require_relative 'server/eddsa'
13
15
 
14
16
  module Kiosk
15
17
  module Server
16
18
  end
17
19
 
18
- # The user id and role of the principal the wire resolved.
20
+ # Kiosk's tables are `kiosk_*` in the configured schema; Rails names every
21
+ # model in this namespace with it: Kiosk::Settlement is `public.kiosk_settlements`.
22
+ def self.table_name_prefix = "#{configuration.schema}.kiosk_"
23
+
24
+ # The user id and role of the principal the protocol resolved.
19
25
  def self.current_user_id = Server::SessionContext.identity.user_id
20
26
  def self.current_role = Server::SessionContext.identity.role
21
27
 
@@ -5,12 +5,16 @@ require 'active_support/concern'
5
5
  module Kiosk
6
6
  # `include Kiosk::SkillLink` in a page controller: every answer carries
7
7
  # `Link: <skill_url>; rel="kiosk"`, the discovery signal for a client that
8
- # does not read HTML.
8
+ # does not read HTML, and the RFC 9727 `api-catalog` link.
9
9
  module SkillLink
10
10
  extend ActiveSupport::Concern
11
11
 
12
+ API_CATALOG = '</.well-known/api-catalog>; rel="api-catalog"'
13
+
12
14
  included do
13
- before_action { response.set_header('Link', %(<#{Kiosk.configuration.skill_url}>; rel="kiosk")) }
15
+ before_action do
16
+ response.set_header('Link', %(<#{Kiosk.configuration.skill_url}>; rel="kiosk", #{API_CATALOG}))
17
+ end
14
18
  end
15
19
  end
16
20
  end
data/listen.py CHANGED
@@ -8,10 +8,15 @@ cadence to invent.
8
8
 
9
9
  python3 listen.py --url wss://shop.example/kiosk/events \
10
10
  --token-file ~/.kiosk/https_shop.example_443/token \
11
+ --proof-command "python3 ~/.kiosk/dpop.py ~/.kiosk/https_shop.example_443/key.pem" \
11
12
  --topic order_delivery:<order-id>
12
13
 
13
- The bearer token is read from a file, never from the command line, where every
14
- process listing and shell history would show it.
14
+ The access token is read from a file, never from the command line, where every
15
+ process listing and shell history would show it. The token works only with a
16
+ DPoP proof (RFC 9449) that your private key signs. This file never reads that
17
+ key: before each connection it runs your --proof-command, writes the URL to
18
+ prove and the token to its stdin, one per line, and sends the proof that the
19
+ command prints.
15
20
 
16
21
  WAKE ON THE EVENT: run it as a background process your runtime TRACKS and wakes
17
22
  you on when it exits. With `--until-event` it prints the first event and exits 0
@@ -21,7 +26,7 @@ on the event, tell your human, and start it again with `--since <its id>`. Keep
21
26
  process the runtime does not track -- nohup, `&`, setsid -- wakes nobody, and
22
27
  neither does a line landing in a file.
23
28
 
24
- python3 listen.py --url … --token-file … --topic kyc_verification \
29
+ python3 listen.py --url … --token-file … --proof-command … --topic kyc_verification \
25
30
  --until-event --max-seconds 300
26
31
 
27
32
  Each line is a JSON object with a `type`:
@@ -36,7 +41,8 @@ Each line is a JSON object with a `type`:
36
41
  {"type":"error","message":…}
37
42
 
38
43
  EXIT CODES: 0 you asked it to stop (--max-seconds), or --until-event got its
39
- event; 2 the arguments or the URL are wrong; 3 the operator will not have this
44
+ event; 2 the arguments or the URL are wrong, or --proof-command printed no
45
+ proof; 3 the operator will not have this
40
46
  token back and no --token-command was given — mint a fresh one and start it
41
47
  again; 4 nothing you asked for can arrive: every topic you named was refused,
42
48
  or the origin serves no event stream (`501 module_not_served`); 5
@@ -88,6 +94,22 @@ def problem_code(response):
88
94
  return None
89
95
 
90
96
 
97
+ def proof_url(url):
98
+ """The `htu` of the upgrade: the events URL with `wss` written as `https`,
99
+ no query."""
100
+ parts = urlsplit(url)
101
+ scheme = {"wss": "https", "ws": "http"}[parts.scheme]
102
+ return urlunsplit((scheme, parts.netloc, parts.path, "", ""))
103
+
104
+
105
+ def dpop_proof(command, url, token):
106
+ """The proof your --proof-command prints for this URL and token, or None."""
107
+ made = subprocess.run(command, shell=True, input=f"{proof_url(url)}\n{token}\n",
108
+ capture_output=True, text=True, check=False)
109
+ proof = made.stdout.strip()
110
+ return proof if made.returncode == 0 and proof else None
111
+
112
+
91
113
  def stream_url(url, topics, since):
92
114
  """`?topic=` is how you subscribe: the operator reads it on connect and
93
115
  confirms each one, so a round trip per topic is not needed."""
@@ -145,7 +167,7 @@ def read_frames(socket, state, deadline):
145
167
  reconnect=frame.get("reconnect", True))
146
168
  # `reconnect: false` is the operator saying this credential is done:
147
169
  # it expired, or the human revoked the assistant. A retry with the
148
- # same bearer loses the same argument, so mint a new one or stop.
170
+ # same token loses the same argument, so mint a new one or stop.
149
171
  if frame.get("reconnect", True):
150
172
  return "retry"
151
173
  return "retry" if state["refresh"] else "credential"
@@ -191,12 +213,15 @@ def main():
191
213
  parser = argparse.ArgumentParser(description="Kiosk event-stream listener")
192
214
  parser.add_argument("--url", required=True, help="the operator's events_url")
193
215
  parser.add_argument("--token-file", required=True, metavar="PATH",
194
- help="a file holding your bearer token")
216
+ help="a file holding your access token")
217
+ parser.add_argument("--proof-command", required=True,
218
+ help="shell command that reads a URL and a token on stdin and prints "
219
+ "the DPoP proof your private key signs for them")
195
220
  parser.add_argument("--topic", action="append", required=True, metavar="NAME[:SUBJECT]",
196
221
  help="repeatable; NAME:SUBJECT narrows to one row")
197
222
  parser.add_argument("--since", type=int, help="resume after this event id")
198
223
  parser.add_argument("--token-command",
199
- help="shell command printing a fresh bearer token; run when the "
224
+ help="shell command printing a fresh access token; run when the "
200
225
  "operator says this one will not be accepted back")
201
226
  parser.add_argument("--max-seconds", type=float, help="stop after this long")
202
227
  parser.add_argument("--until-event", action="store_true",
@@ -224,10 +249,15 @@ def main():
224
249
  if deadline and time.monotonic() >= deadline:
225
250
  return 5 if args.until_event else 0
226
251
  renew = True
252
+ proof = dpop_proof(args.proof_command, args.url, state["token"])
253
+ if proof is None:
254
+ emit(type="error", message="--proof-command produced no proof")
255
+ return 2
227
256
  try:
228
257
  with connect(stream_url(args.url, args.topic, state["since"]),
229
258
  subprotocols=[SUBPROTOCOL],
230
- additional_headers={"Authorization": f"Bearer {state['token']}"},
259
+ additional_headers={"Authorization": f"DPoP {state['token']}",
260
+ "DPoP": proof},
231
261
  open_timeout=15, close_timeout=5) as socket:
232
262
  emit(type="open")
233
263
  attempt = 0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kiosk-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.20
4
+ version: 0.6.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Pirozhkov
@@ -15,21 +15,21 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.5.0
18
+ version: 0.6.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.5.0
25
+ version: 0.6.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: jwt
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '2.8'
32
+ version: '2.10'
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
35
  version: '4.0'
@@ -39,7 +39,7 @@ dependencies:
39
39
  requirements:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: '2.8'
42
+ version: '2.10'
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
45
  version: '4.0'
@@ -164,18 +164,19 @@ dependencies:
164
164
  description: |
165
165
  kiosk-server is the host-side surface for Kiosk. The full surface ships:
166
166
 
167
- - The controllers — the per-verb wire (GET <endpoint>/<query-name>,
167
+ - The controllers — the per-verb endpoints (GET <endpoint>/<query-name>,
168
168
  POST <endpoint>/<action-name>) and the reserved endpoints
169
169
  (/kiosk/schema, /kiosk/pay), a derived OpenAPI description of both,
170
- the register/login proof-of-possession auth plane, JWKS, the KYC
170
+ OAuth client registration (RFC 7591) and the token endpoint
171
+ (private_key_jwt client assertions, DPoP), JWKS, the KYC
171
172
  attestation endpoint, agents.txt / agents.json / kiosk.json
172
- discovery, and the account-binding ceremony (RFC 8628-shaped device
173
- authorization, the possession-proof-gated token poll, the verify and
174
- «Link an assistant» pages).
173
+ discovery, and the account-binding ceremony (RFC 8628 device
174
+ authorization and token poll, the verify and «Link an assistant»
175
+ pages).
175
176
  - Kiosk::Server::Executor — dispatches a resolved command to the
176
177
  queries and actions the host registered.
177
- - Agent registration and login, with a pluggable agent-IdP.
178
- - The PoW gate that enforces a kiosk-reputation policy's challenge
178
+ - Client registration and authentication, with a pluggable agent-IdP.
179
+ - The proof-of-work toll that a kiosk-reputation policy sets
179
180
  (soft dependency; zero overhead when no policy is set).
180
181
  - A Rack middleware injecting the Kiosk-Server-Version /
181
182
  Kiosk-API-Version / Kiosk-Min-Client response headers, a pure-Ruby
@@ -185,7 +186,7 @@ description: |
185
186
 
186
187
  The Rails engine serves that whole surface from one `kiosk` block in
187
188
  config/routes.rb, which mounts it at Kiosk.configuration.mount_path —
188
- drawing the wire/auth/JWKS/KYC/binding routes under the mount and
189
+ drawing the verb/auth/JWKS/KYC/binding routes under the mount and
189
190
  installing the root-relative discovery routes (agents.txt, .well-known)
190
191
  into the host — then one route per query and action the block names. It
191
192
  also auto-injects the headers middleware.
@@ -208,20 +209,21 @@ files:
208
209
  - app/controllers/kiosk/server/kyc_attestation_controller.rb
209
210
  - app/controllers/kiosk/server/kyc_callback_controller.rb
210
211
  - app/controllers/kiosk/server/oauth_device_authorization_controller.rb
212
+ - app/controllers/kiosk/server/oauth_registration_controller.rb
211
213
  - app/controllers/kiosk/server/oauth_token_controller.rb
212
214
  - app/controllers/kiosk/server/open_api_controller.rb
213
215
  - app/controllers/kiosk/server/payment_setup_controller.rb
214
216
  - app/controllers/kiosk/server/verb_controller.rb
215
217
  - app/controllers/kiosk/server/wire_controller.rb
216
- - app/models/kiosk/cart_mandate.rb
218
+ - app/models/kiosk/checkout_mandate.rb
217
219
  - app/models/kiosk/server/application_record.rb
218
220
  - app/models/kiosk/settlement.rb
219
221
  - app/models/kiosk/visit.rb
220
222
  - app/views/kiosk/server/assistants/show.html.erb
221
223
  - app/views/kiosk/server/device_verify/decided.html.erb
222
224
  - app/views/kiosk/server/device_verify/show.html.erb
225
+ - app/views/layouts/kiosk/server/application.html.erb
223
226
  - lib/generators/kiosk/install/install_generator.rb
224
- - lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt
225
227
  - lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt
226
228
  - lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt
227
229
  - lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt
@@ -230,7 +232,6 @@ files:
230
232
  - lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt
231
233
  - lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt
232
234
  - lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt
233
- - lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt
234
235
  - lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt
235
236
  - lib/generators/kiosk/install/templates/initializer.rb.tt
236
237
  - lib/kiosk/handler.rb
@@ -242,15 +243,18 @@ files:
242
243
  - lib/kiosk/server/action_event.rb
243
244
  - lib/kiosk/server/actions.rb
244
245
  - lib/kiosk/server/agent_identity_providers/default_agent_idp.rb
245
- - lib/kiosk/server/agent_login.rb
246
- - lib/kiosk/server/agent_registration.rb
246
+ - lib/kiosk/server/agent_rotation.rb
247
247
  - lib/kiosk/server/argument_decoder.rb
248
+ - lib/kiosk/server/assistant_accounts.rb
249
+ - lib/kiosk/server/assistant_key.rb
248
250
  - lib/kiosk/server/audit_sink.rb
249
- - lib/kiosk/server/auth_challenge.rb
250
- - lib/kiosk/server/auth_challenge_stores.rb
251
251
  - lib/kiosk/server/auth_toll.rb
252
252
  - lib/kiosk/server/binding_module_gate.rb
253
253
  - lib/kiosk/server/caller_timezone.rb
254
+ - lib/kiosk/server/checkout.rb
255
+ - lib/kiosk/server/client.rb
256
+ - lib/kiosk/server/client_assertion.rb
257
+ - lib/kiosk/server/client_registration.rb
254
258
  - lib/kiosk/server/column_spending_cap.rb
255
259
  - lib/kiosk/server/configuration_extension.rb
256
260
  - lib/kiosk/server/controller_verb.rb
@@ -260,12 +264,15 @@ files:
260
264
  - lib/kiosk/server/device_authorization_stores.rb
261
265
  - lib/kiosk/server/device_code_grant.rb
262
266
  - lib/kiosk/server/device_verification.rb
267
+ - lib/kiosk/server/dpop.rb
268
+ - lib/kiosk/server/eddsa.rb
263
269
  - lib/kiosk/server/engine.rb
264
270
  - lib/kiosk/server/errors.rb
265
271
  - lib/kiosk/server/event_stores.rb
266
272
  - lib/kiosk/server/events.rb
267
273
  - lib/kiosk/server/events_cable.rb
268
274
  - lib/kiosk/server/events_connection.rb
275
+ - lib/kiosk/server/events_replay.rb
269
276
  - lib/kiosk/server/executor.rb
270
277
  - lib/kiosk/server/executor/repeat.rb
271
278
  - lib/kiosk/server/executor/spending_cap.rb
@@ -277,6 +284,7 @@ files:
277
284
  - lib/kiosk/server/headers_middleware.rb
278
285
  - lib/kiosk/server/identity_resolution.rb
279
286
  - lib/kiosk/server/issuer_middleware.rb
287
+ - lib/kiosk/server/jwk.rb
280
288
  - lib/kiosk/server/jwks.rb
281
289
  - lib/kiosk/server/jwt_issuer.rb
282
290
  - lib/kiosk/server/kyc.rb
@@ -284,22 +292,24 @@ files:
284
292
  - lib/kiosk/server/link_code.rb
285
293
  - lib/kiosk/server/linked_assistants.rb
286
294
  - lib/kiosk/server/mandate_verifier.rb
295
+ - lib/kiosk/server/money.rb
296
+ - lib/kiosk/server/oauth_answers.rb
297
+ - lib/kiosk/server/oauth_error.rb
287
298
  - lib/kiosk/server/open_api.rb
288
299
  - lib/kiosk/server/page.rb
289
300
  - lib/kiosk/server/payment_claim.rb
290
301
  - lib/kiosk/server/payment_setup.rb
291
- - lib/kiosk/server/pop_verifier.rb
292
302
  - lib/kiosk/server/pow_gate.rb
293
303
  - lib/kiosk/server/pow_spent_stores.rb
294
304
  - lib/kiosk/server/pow_toll.rb
295
305
  - lib/kiosk/server/queries.rb
306
+ - lib/kiosk/server/receipts.rb
296
307
  - lib/kiosk/server/reputation_factors.rb
297
308
  - lib/kiosk/server/request_validation.rb
298
309
  - lib/kiosk/server/response_validation.rb
299
310
  - lib/kiosk/server/result.rb
300
311
  - lib/kiosk/server/revocation_stores.rb
301
312
  - lib/kiosk/server/routing.rb
302
- - lib/kiosk/server/schema_definitions.rb
303
313
  - lib/kiosk/server/schema_document.rb
304
314
  - lib/kiosk/server/schema_slots.rb
305
315
  - lib/kiosk/server/schemas/auth.schema.json
@@ -307,16 +317,16 @@ files:
307
317
  - lib/kiosk/server/schemas/kyc.schema.json
308
318
  - lib/kiosk/server/schemas/mandates.schema.json
309
319
  - lib/kiosk/server/schemas/pow.schema.json
320
+ - lib/kiosk/server/sd_jwt.rb
310
321
  - lib/kiosk/server/session_context.rb
311
- - lib/kiosk/server/settled_spend.rb
312
322
  - lib/kiosk/server/signing_key.rb
323
+ - lib/kiosk/server/tolled_calls.rb
313
324
  - lib/kiosk/server/verb_registry.rb
314
325
  - lib/kiosk/server/verb_vocabulary.rb
315
326
  - lib/kiosk/server/verb_wire.rb
316
327
  - lib/kiosk/server/version.rb
317
328
  - lib/kiosk/server/well_known.rb
318
329
  - lib/kiosk/server/wire_rendering.rb
319
- - lib/kiosk/server/wrong_door.rb
320
330
  - lib/kiosk/skill_link.rb
321
331
  - listen.py
322
332
  - requirements.txt
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kiosk
4
- # The signed cart the engine records when it settles a `pay`: the line items
5
- # the assistant agreed to pay for. Read-only for the operator.
6
- class CartMandate < Server::ApplicationRecord
7
- def self.table_name = "#{Kiosk.configuration.schema}.cart_mandates"
8
-
9
- has_many :settlements, class_name: 'Kiosk::Settlement', dependent: nil, inverse_of: :cart_mandate
10
-
11
- # Carts with a line item carrying these attributes, by jsonb containment:
12
- # `referencing(order_id: id)` matches `[{"order_id": id, "qty": 2}]`. The
13
- # values are quoted by the adapter, never interpolated.
14
- scope :referencing, lambda { |**attributes|
15
- where(Arel::Nodes::InfixOperation.new(
16
- '@>', arel_table[:line_items],
17
- Arel::Nodes.build_quoted([attributes.transform_values(&:to_s)].to_json)
18
- ))
19
- }
20
- end
21
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Migration 010 — create kiosk.auth_challenges.
4
- # Each public key's outstanding auth challenge, so a challenge issued by one
5
- # process is redeemed by any other (AuthChallengeStores::ActiveRecord, the default).
6
- class CreateKioskAuthChallenges < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
7
- def up
8
- execute <<~'SQL'
9
- <%= ddl(:auth_challenge) %>
10
- SQL
11
- end
12
-
13
- def down
14
- execute %(DROP TABLE IF EXISTS "<%= options[:schema] %>".auth_challenges)
15
- end
16
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Migration 001 — Kiosk schema + the four current_*() helper functions.
4
- #
5
- # `ActiveRecord::Migration[…]` resolves at host-app load time, so this
6
- # file picks up whatever Rails version the host is on.
7
- class CreateKioskSchema < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
8
- def up
9
- execute <<~'SQL'
10
- <%= ddl(:helper_functions, guc_namespace: options[:guc_namespace], user_id_type: user_id_type) %>
11
- SQL
12
- end
13
-
14
- def down
15
- execute %(DROP SCHEMA IF EXISTS "<%= options[:schema] %>" CASCADE)
16
- end
17
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kiosk
4
- module Server
5
- # Login with an already-registered public key: proves possession and mints a
6
- # fresh token. An unknown key is a 404, never a new account.
7
- module AgentLogin
8
- module_function
9
-
10
- def call(public_key_pem:, signed:)
11
- pem = public_key_pem.to_s.strip
12
-
13
- # Prove possession BEFORE any lookup or state change.
14
- payload = PopVerifier.verify!(public_key_pem: pem, signed: signed)
15
- AuthChallenge.consume!(public_key_pem: pem, nonce: payload.fetch(:nonce))
16
-
17
- row = registered_agent(pem)
18
- agent_id = row.fetch('id')
19
- role = primary_role(row.fetch('allowed_roles'))
20
- token = AgentIdentityProviders::DefaultAgentIdp.new.issue(agent_id: agent_id, role: role)
21
- { access_token: token }
22
- end
23
-
24
- def registered_agent(pem)
25
- row = ::ActiveRecord::Base.lease_connection.exec_query(<<~SQL.squish, 'Kiosk agent lookup by key', [pem, Kiosk.current_issuer]).to_a.first
26
- SELECT id, user_id, allowed_roles FROM #{Kiosk.configuration.schema}.agents
27
- WHERE public_key = $1 AND issuer = $2 AND revoked_at IS NULL
28
- LIMIT 1
29
- SQL
30
- return row if row
31
-
32
- raise Errors::NotFound.new(
33
- 'no agent registered for this public key',
34
- hint: 'POST /auth/register to create an identity for a new key'
35
- )
36
- end
37
- private_class_method :registered_agent
38
-
39
- # `allowed_roles` is an Array or a text[] literal ("{customer}"), by adapter.
40
- def primary_role(allowed_roles)
41
- case allowed_roles
42
- when Array then allowed_roles.first
43
- else allowed_roles.to_s.delete('{}').split(',').first
44
- end
45
- end
46
- private_class_method :primary_role
47
- end
48
- end
49
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_record'
4
- require 'active_support/core_ext/string/inflections' # String#constantize
5
-
6
- module Kiosk
7
- module Server
8
- # Self-registration with no human: creates the assistant account, binds an
9
- # agent to the key, and mints its first token.
10
- module AgentRegistration
11
- module_function
12
-
13
- def call(public_key_pem:, signed:, pow: nil)
14
- config = Kiosk.configuration
15
- role = registration_role(config)
16
- public_key_pem = public_key_pem.to_s.strip
17
-
18
- AuthToll.gate(verb: 'auth/register', body: { public_key: public_key_pem }, pow: pow, config: config)
19
-
20
- # Prove possession of the private key before any lookup or write.
21
- payload = PopVerifier.verify!(public_key_pem: public_key_pem, signed: signed)
22
- AuthChallenge.consume!(public_key_pem: public_key_pem, nonce: payload.fetch(:nonce))
23
-
24
- conn = ::ActiveRecord::Base.lease_connection
25
- refuse_known_key!(conn, config, public_key_pem)
26
- conn.transaction { register(conn, config, public_key_pem, role) }
27
- end
28
-
29
- # The operator's `assistant_creation` factory, else a bare `user_model.create!`.
30
- def create_assistant_account(config, public_key_pem)
31
- return config.user_model.constantize.create!.id unless config.assistant_creation
32
-
33
- id = config.assistant_creation.call(public_key_pem)
34
- return id unless id.nil?
35
-
36
- raise Errors::ConfigurationError,
37
- 'config.assistant_creation returned nil. The block must create the ' \
38
- 'assistant account and RETURN its id (used as agents.user_id), e.g. ' \
39
- 'c.assistant_creation = ->(pubkey) { AssistantAccount.create!(...).id }'
40
- end
41
-
42
- # The role is the server's choice, never the agent's.
43
- def registration_role(config)
44
- role = config.registration_role&.to_s
45
- return if role.blank?
46
- return role if config.roles.map(&:to_s).include?(role)
47
-
48
- raise Errors::ConfigurationError,
49
- "registration_role #{role.inspect} is not among configured roles " \
50
- "#{config.roles.inspect}"
51
- end
52
-
53
- # A known key logs in instead; it is never registered twice.
54
- def refuse_known_key!(conn, config, public_key_pem)
55
- existing = conn.exec_query(<<~SQL.squish, 'Kiosk agent lookup by key', [public_key_pem, Kiosk.current_issuer]).to_a.first
56
- SELECT id FROM #{config.schema}.agents
57
- WHERE public_key = $1 AND issuer = $2 AND revoked_at IS NULL
58
- LIMIT 1
59
- SQL
60
- return unless existing
61
-
62
- raise Errors::Conflict.new(
63
- 'public key already registered',
64
- hint: 'use POST /auth/login to refresh a token for an existing key'
65
- )
66
- end
67
-
68
- def register(conn, config, public_key_pem, role)
69
- assistant_account_id = create_assistant_account(config, public_key_pem)
70
- agent_id = insert_agent(conn, config, [assistant_account_id, public_key_pem, Kiosk.current_issuer], role)
71
- token = AgentIdentityProviders::DefaultAgentIdp.new.issue(agent_id: agent_id, role: role)
72
- { agent_id: agent_id, user_id: assistant_account_id.to_s, access_token: token }
73
- end
74
-
75
- # No role is the empty array: `allowed_roles` is NOT NULL.
76
- def insert_agent(conn, config, binds, role)
77
- allowed_roles_sql, role_binds = role ? ['ARRAY[$4]::text[]', [role]] : ["'{}'::text[]", []]
78
- rows = conn.exec_query(<<~SQL.squish, 'Kiosk agent insert', [*binds, *role_binds])
79
- INSERT INTO #{config.schema}.agents (user_id, allowed_roles, public_key, issuer)
80
- VALUES ($1, #{allowed_roles_sql}, $2, $3)
81
- RETURNING id
82
- SQL
83
- rows.to_a.first.fetch('id')
84
- end
85
- private_class_method :registration_role, :refuse_known_key!, :register, :insert_agent
86
- end
87
- end
88
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module Kiosk
6
- module Server
7
- # Server side of the proof-of-possession challenge: a single-use, short-lived
8
- # nonce per public key.
9
- module AuthChallenge
10
- module_function
11
-
12
- # The agent signs `challenge` into its JWS; `exp` is the Unix expiry.
13
- def issue(public_key_pem:, now: Time.current)
14
- config = Kiosk.configuration
15
- nonce = SecureRandom.urlsafe_base64(32)
16
- exp = now.to_i + config.auth_challenge_ttl
17
- config.auth_challenge_store.put(public_key_pem.to_s.strip, nonce, exp)
18
- { challenge: nonce, exp: exp }
19
- end
20
-
21
- def consume!(public_key_pem:, nonce:)
22
- ok = Kiosk.configuration.auth_challenge_store.take(public_key_pem.to_s.strip, nonce.to_s)
23
- return true if ok
24
-
25
- raise Errors::Unauthenticated.new(
26
- 'no matching auth challenge',
27
- hint: 'GET /auth/challenge?public_key=… first, then sign the returned nonce; ' \
28
- 'challenges are single-use and short-lived'
29
- )
30
- end
31
- end
32
- end
33
- end