kiosk-server 0.5.20

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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +549 -0
  3. data/LICENSE.txt +202 -0
  4. data/README.md +635 -0
  5. data/app/channels/kiosk_events.rb +138 -0
  6. data/app/controllers/kiosk/server/application_controller.rb +17 -0
  7. data/app/controllers/kiosk/server/assistants_controller.rb +96 -0
  8. data/app/controllers/kiosk/server/auth_controller.rb +187 -0
  9. data/app/controllers/kiosk/server/device_verify_controller.rb +101 -0
  10. data/app/controllers/kiosk/server/discovery_controller.rb +66 -0
  11. data/app/controllers/kiosk/server/jwks_controller.rb +15 -0
  12. data/app/controllers/kiosk/server/kyc_attestation_controller.rb +69 -0
  13. data/app/controllers/kiosk/server/kyc_callback_controller.rb +15 -0
  14. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +73 -0
  15. data/app/controllers/kiosk/server/oauth_token_controller.rb +59 -0
  16. data/app/controllers/kiosk/server/open_api_controller.rb +20 -0
  17. data/app/controllers/kiosk/server/payment_setup_controller.rb +31 -0
  18. data/app/controllers/kiosk/server/verb_controller.rb +61 -0
  19. data/app/controllers/kiosk/server/wire_controller.rb +43 -0
  20. data/app/models/kiosk/cart_mandate.rb +21 -0
  21. data/app/models/kiosk/server/application_record.rb +10 -0
  22. data/app/models/kiosk/settlement.rb +12 -0
  23. data/app/models/kiosk/visit.rb +13 -0
  24. data/app/views/kiosk/server/assistants/show.html.erb +94 -0
  25. data/app/views/kiosk/server/device_verify/decided.html.erb +14 -0
  26. data/app/views/kiosk/server/device_verify/show.html.erb +53 -0
  27. data/lib/generators/kiosk/install/install_generator.rb +125 -0
  28. data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +16 -0
  29. data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +17 -0
  30. data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +22 -0
  31. data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +16 -0
  32. data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +20 -0
  33. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +21 -0
  34. data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +16 -0
  35. data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +16 -0
  36. data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +16 -0
  37. data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +17 -0
  38. data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +16 -0
  39. data/lib/generators/kiosk/install/templates/initializer.rb.tt +53 -0
  40. data/lib/kiosk/handler.rb +43 -0
  41. data/lib/kiosk/owned.rb +17 -0
  42. data/lib/kiosk/payable.rb +90 -0
  43. data/lib/kiosk/server/account_binding.rb +188 -0
  44. data/lib/kiosk/server/account_holder_gate.rb +58 -0
  45. data/lib/kiosk/server/action_event.rb +60 -0
  46. data/lib/kiosk/server/actions.rb +14 -0
  47. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +89 -0
  48. data/lib/kiosk/server/agent_login.rb +49 -0
  49. data/lib/kiosk/server/agent_registration.rb +88 -0
  50. data/lib/kiosk/server/argument_decoder.rb +260 -0
  51. data/lib/kiosk/server/audit_sink.rb +42 -0
  52. data/lib/kiosk/server/auth_challenge.rb +33 -0
  53. data/lib/kiosk/server/auth_challenge_stores.rb +74 -0
  54. data/lib/kiosk/server/auth_toll.rb +44 -0
  55. data/lib/kiosk/server/binding_module_gate.rb +25 -0
  56. data/lib/kiosk/server/caller_timezone.rb +50 -0
  57. data/lib/kiosk/server/column_spending_cap.rb +26 -0
  58. data/lib/kiosk/server/configuration_extension.rb +328 -0
  59. data/lib/kiosk/server/controller_verb.rb +63 -0
  60. data/lib/kiosk/server/current_request.rb +31 -0
  61. data/lib/kiosk/server/cursor.rb +33 -0
  62. data/lib/kiosk/server/device_authorization.rb +136 -0
  63. data/lib/kiosk/server/device_authorization_stores.rb +184 -0
  64. data/lib/kiosk/server/device_code_grant.rb +145 -0
  65. data/lib/kiosk/server/device_verification.rb +46 -0
  66. data/lib/kiosk/server/engine.rb +203 -0
  67. data/lib/kiosk/server/errors.rb +339 -0
  68. data/lib/kiosk/server/event_stores.rb +124 -0
  69. data/lib/kiosk/server/events.rb +86 -0
  70. data/lib/kiosk/server/events_cable.rb +102 -0
  71. data/lib/kiosk/server/events_connection.rb +170 -0
  72. data/lib/kiosk/server/executor/repeat.rb +42 -0
  73. data/lib/kiosk/server/executor/spending_cap.rb +102 -0
  74. data/lib/kiosk/server/executor.rb +397 -0
  75. data/lib/kiosk/server/failure_log.rb +28 -0
  76. data/lib/kiosk/server/handler_mixin.rb +418 -0
  77. data/lib/kiosk/server/handler_registrations.rb +57 -0
  78. data/lib/kiosk/server/handler_serving.rb +197 -0
  79. data/lib/kiosk/server/headers.rb +82 -0
  80. data/lib/kiosk/server/headers_middleware.rb +31 -0
  81. data/lib/kiosk/server/identity_resolution.rb +20 -0
  82. data/lib/kiosk/server/issuer_middleware.rb +22 -0
  83. data/lib/kiosk/server/jwks.rb +14 -0
  84. data/lib/kiosk/server/jwt_issuer.rb +98 -0
  85. data/lib/kiosk/server/kyc.rb +261 -0
  86. data/lib/kiosk/server/kyc_verifier.rb +106 -0
  87. data/lib/kiosk/server/link_code.rb +90 -0
  88. data/lib/kiosk/server/linked_assistants.rb +116 -0
  89. data/lib/kiosk/server/mandate_verifier.rb +221 -0
  90. data/lib/kiosk/server/open_api.rb +543 -0
  91. data/lib/kiosk/server/page.rb +19 -0
  92. data/lib/kiosk/server/payment_claim.rb +293 -0
  93. data/lib/kiosk/server/payment_setup.rb +102 -0
  94. data/lib/kiosk/server/pop_verifier.rb +87 -0
  95. data/lib/kiosk/server/pow_gate.rb +121 -0
  96. data/lib/kiosk/server/pow_spent_stores.rb +96 -0
  97. data/lib/kiosk/server/pow_toll.rb +136 -0
  98. data/lib/kiosk/server/queries.rb +14 -0
  99. data/lib/kiosk/server/reputation_factors.rb +38 -0
  100. data/lib/kiosk/server/request_validation.rb +171 -0
  101. data/lib/kiosk/server/response_validation.rb +45 -0
  102. data/lib/kiosk/server/result.rb +25 -0
  103. data/lib/kiosk/server/revocation_stores.rb +57 -0
  104. data/lib/kiosk/server/routing.rb +76 -0
  105. data/lib/kiosk/server/schema_definitions.rb +351 -0
  106. data/lib/kiosk/server/schema_document.rb +116 -0
  107. data/lib/kiosk/server/schema_slots.rb +119 -0
  108. data/lib/kiosk/server/schemas/auth.schema.json +135 -0
  109. data/lib/kiosk/server/schemas/binding.schema.json +145 -0
  110. data/lib/kiosk/server/schemas/kyc.schema.json +114 -0
  111. data/lib/kiosk/server/schemas/mandates.schema.json +75 -0
  112. data/lib/kiosk/server/schemas/pow.schema.json +94 -0
  113. data/lib/kiosk/server/session_context.rb +88 -0
  114. data/lib/kiosk/server/settled_spend.rb +21 -0
  115. data/lib/kiosk/server/signing_key.rb +82 -0
  116. data/lib/kiosk/server/verb_registry.rb +77 -0
  117. data/lib/kiosk/server/verb_vocabulary.rb +163 -0
  118. data/lib/kiosk/server/verb_wire.rb +85 -0
  119. data/lib/kiosk/server/version.rb +7 -0
  120. data/lib/kiosk/server/well_known.rb +416 -0
  121. data/lib/kiosk/server/wire_rendering.rb +116 -0
  122. data/lib/kiosk/server/wrong_door.rb +42 -0
  123. data/lib/kiosk/server.rb +42 -0
  124. data/lib/kiosk/skill_link.rb +16 -0
  125. data/listen.py +278 -0
  126. data/requirements.txt +1 -0
  127. metadata +349 -0
data/README.md ADDED
@@ -0,0 +1,635 @@
1
+ # kiosk-server
2
+
3
+ The Kiosk Rails engine — host-side surface for [Kiosk](https://kiosk.tech).
4
+
5
+
6
+ ## What's in this release
7
+
8
+ The full host-side surface is shipped and covered by the gem's own suite
9
+ (`bundle exec rspec` in this directory runs it and prints how many examples that is):
10
+
11
+ - **Wire-protocol controllers** — each verb route (`GET <mount>/<query-name>`, `POST <mount>/<action-name>`) reaches the operator's controller action that declares the verb, and `Kiosk::Handler` runs the wire's checks around it; `VerbController` refuses a verb route that no action answers; `WireController` serves the two reserved endpoints `GET <mount>/schema` and `POST <mount>/pay`; `VerbController` also serves `POST <mount>/payment_setup` against the payment provider port, and `PaymentSetupController` the page the provider returns the human to; `VerbController` serves `POST <mount>/request_kyc` against the KYC provider port, and `KycCallbackController` the provider's callback; `OpenApiController` serves a derived OpenAPI description of both at `GET <mount>/openapi.json`; `AuthController` runs the register/login proof-of-possession challenge-response (kiosk-pop — the auth story); JWKS backs stateless token verification.
12
+ - **Account binding** — the claim/link ceremonies bind an agent's public key to an existing assistant-account holder's account: OAuth/RFC 8628-shaped device authorization + possession-proof-gated token poll, a session-authenticated verify page and «Link an assistant» page (minimal overridable engine views), link-code mint/redeem, and unlink. Tokens stay kiosk-pop-minted; the durable `DeviceAuthorizationStores::ActiveRecord` store (migration 004) is the default.
13
+ - **`Kiosk::Server::Executor`** — dispatches resolved commands to the host's registered queries and Actions.
14
+ - **`Kiosk::Handler`** — the mixin an operator includes into a controller of their own to declare verbs as ordinary Rails actions; each declaration's `kind` says whether it is a query or an action, so one controller may declare both. The engine registers the controllers in `app/controllers/kiosk/` at boot and after every reload (see [Declaring queries and actions](#declaring-queries-and-actions)).
15
+ - **`Kiosk::Server::PaymentClaim`** — the PSP decorator that keeps §11.6's operator half: one capture per payable row, and a paid state anchored to the capture (see [Payments](#payments)).
16
+ - **Agent registration & login** — `AgentRegistration`, `AgentLogin`, `AuthToll`, and the pluggable agent-IdP resolve and mint per-agent identities.
17
+ - **PoW gate** — `PowGate` enforces the reputation policy's N×PoW challenge-response (soft dependency on `kiosk-reputation`; zero overhead when no policy is set).
18
+ - **`Kiosk::Server::WellKnown`** — pure-Ruby builder for `/.well-known/kiosk.json`.
19
+ - **`Kiosk::Server::Headers`** + **`HeadersMiddleware`** — Rack middleware that injects `Kiosk-Server-Version`, `Kiosk-API-Version`, `Kiosk-Min-Client` on `/kiosk/*` responses.
20
+ - **`IssuerMiddleware`** — answers `Kiosk.current_issuer` for each request: its origin when that is `c.issuer` or one of `c.additional_origins`, else `c.issuer`. Every origin served is its own operator.
21
+ - **The audit sink** — `c.audit_sink` receives one `Kiosk::Server::ActionEvent` per action invocation, success and failure alike. The default is no sink, so no `ActionEvent` goes anywhere. The engine keeps its own state in the `kiosk` schema (agents, mandates, settlements, refunds, events, visits), but it writes no audit record there. See [The audit sink](#the-audit-sink).
22
+ - **`Kiosk::Server::SchemaDefinitions`** — SQL generators for every table and helper that `bin/rails g kiosk:install` creates.
23
+ - **`Kiosk::Server::Engine`** — the Rails engine: `kiosk` in `config/routes.rb` mounts it, drawing the full mount-prefixed PROTOCOL surface (the reserved wire, auth, JWKS, KYC, account binding), then one route per verb its block names — installs the root discovery routes when mounted, and auto-injects the headers and issuer middleware (see [Draw the routes](#draw-the-routes)).
24
+ - **`Kiosk::Server::ConfigurationExtension`** — adds `mount_path`, `owner` (and the reputation/PoW slots) to `Kiosk::Configuration`.
25
+ - **`bin/rails g kiosk:install`** — the install generator lays down the initializer, the migrations and the `kiosk` block in `config/routes.rb` that mounts the engine.
26
+
27
+ kiosk-server is a Rails gem: it depends on railties, actionpack, actioncable, activerecord and activesupport (`~> 8.1`), and `require "kiosk/server"` loads them. Pieces such as `WellKnown` and `SchemaDefinitions` still work without a BOOTED Rails app — they just need the framework on the load path.
28
+
29
+ ## Install
30
+
31
+ > **Not on RubyGems yet** — so every `gem` line below carries `github: "kiosk-hq/kiosk"`, which is what makes it copy-pasteable today. Publication status and the canonical install are stated once, in the monorepo README's [Install](https://github.com/kiosk-hq/kiosk#install) section.
32
+
33
+ ### Preconditions
34
+
35
+ **Rails `~> 8.1`.** This gemspec pins `railties`, `actionpack`, `actioncable`,
36
+ `activerecord` and `activesupport` at `~> 8.1`. The only other Kiosk gemspec
37
+ that names a Rails gem is `kiosk-pay-stripe`, which pins `activerecord` at
38
+ `~> 8.1`. **Rails 7.x and 8.0.x are excluded** — bundler refuses to resolve
39
+ against an app on either, so for most "add Kiosk to an existing Rails app" this
40
+ is the first thing to check, not a footnote. Older Rails lines are untested
41
+ here, so they are not claimed; widening the floor means adding a CI leg first.
42
+
43
+ **Ruby `>= 4.0`**, which every Kiosk gemspec declares and CI runs.
44
+
45
+ **PostgreSQL.** The kiosk schema, the identity tables and the optional RLS
46
+ backstop are Postgres; no other database is supported.
47
+
48
+ ### The line
49
+
50
+ ```ruby
51
+ gem "kiosk-server", github: "kiosk-hq/kiosk"
52
+ ```
53
+
54
+ Or, via the meta-gem:
55
+
56
+ ```ruby
57
+ gem "kiosk-all", github: "kiosk-hq/kiosk"
58
+ ```
59
+
60
+ ## Configure
61
+
62
+ ```ruby
63
+ # config/initializers/kiosk.rb
64
+ Kiosk.configure do |c|
65
+ c.issuer = "https://api.acme.example"
66
+ # Further origins this deployment serves, each a separate operator with its
67
+ # own discovery document and assistant accounts. Redirect an alias of the
68
+ # same business to `c.issuer` instead.
69
+ # c.additional_origins = ["https://shop.acme.example"]
70
+ c.user_model = "User"
71
+ c.user_id_type = :uuid
72
+ c.roles = %i[customer master support]
73
+ # The default role. The engine refuses to boot when `c.roles` is set and
74
+ # this is not one of them. An assistant admitted with no role of its own (a
75
+ # self-registration, or a binding whose approving human resolves none) gets
76
+ # this one. Name your least-privileged role. An origin that assigns roles to
77
+ # nobody sets neither line, and then no token carries a `role` claim.
78
+ c.registration_role = :customer
79
+ c.owner = { name: "Acme Inc.", support: "support@acme.example" }
80
+ # c.mount_path = "/kiosk" # default
81
+ # Optional: one event per action invocation. See "The audit sink".
82
+ # c.audit_sink = ->(event) { AuditRow.create!(**event.to_h) }
83
+ end
84
+ ```
85
+
86
+ ### KYC
87
+
88
+ Set a KYC provider adapter (a `kiosk-kyc-*` gem) and the engine serves
89
+ `POST <mount>/request_kyc`, the provider's `POST <mount>/kyc/callback` and the
90
+ `kyc_verification` topic, and records the verified attributes against the
91
+ person. Declare the attributes your gated actions need; gate with
92
+ `Kiosk::Server::Kyc.require!`, which refuses `403 kyc_required` until the
93
+ calling principal holds them. An action that needs only some of them passes
94
+ their names, `require!(%w[age_over_18])`, and may pass its own `detail:`.
95
+
96
+ ```ruby
97
+ Kiosk.configure do |c|
98
+ c.kyc_provider = Kiosk::KycProviders::Prove.new(operator_id: "acme", intake_secret: ENV["PROVE_SECRET"])
99
+ c.kyc_claims = %w[age_over_18]
100
+ c.kyc_issuer = "https://kyc.example" # the `iss` the provider signs
101
+ c.kyc_public_key = ENV["KYC_PUBLIC_KEY_PEM"]
102
+ c.kyc_audience = "acme" # the `aud` it mints for you
103
+ end
104
+ ```
105
+
106
+ ### Payments
107
+
108
+ Set a PSP adapter (a `kiosk-pay-*` gem) and the engine serves `pay`. Wrap it in
109
+ `Kiosk::Server::PaymentClaim` and the engine keeps §11.6's operator half for you:
110
+ it claims your payable row (`unpaid → paying`) before the capture, so a second
111
+ `pay` for that row is refused before the processor is reached, and marks it
112
+ `paid` when the capture returns. Under the claim it checks the signed cart:
113
+ your currency, priced lines that sum to the total, and a total equal to your
114
+ own price for the row. That price is the one thing you write — a checker that
115
+ answers it from your catalog, given the row and the cart's item lines as
116
+ signed, or returns a String saying why the cart is refused.
117
+
118
+ ```ruby
119
+ module PriceChecker
120
+ def self.call(order_id, _lines) = Order.where(id: order_id).pick(:total_cents)
121
+ end
122
+
123
+ Kiosk.configure do |c|
124
+ c.payment_provider = Kiosk::Server::PaymentClaim.new(
125
+ psp, currency: "eur", model: "Order", reference: "order_id", query: "my_orders"
126
+ )
127
+ c.cart_price_checker = PriceChecker
128
+ c.after_payment = ->(order_id) { CourierDispatchJob.arm!(order_id) } # optional
129
+ end
130
+ ```
131
+
132
+ `model` names your payable model; it includes `Kiosk::Owned`, so the claim
133
+ takes only the payer's own row. The status lives in its `payment_status` column,
134
+ whose unpaid value is `unpaid`.
135
+
136
+ The model also includes `Kiosk::Payable`, which gives each row:
137
+
138
+ - `payment_state`: `unpaid`, `pending` while a capture is in flight, `paid`, or
139
+ `refunded`. Your per-user query publishes it, so a capture in flight never
140
+ reads as *not paid*. `Order.own.with_settlement(Kiosk::Settlement.own)` reads
141
+ it for many rows in one query.
142
+ - `cart(lines)`: the cart your action returns for the assistant to sign, a line
143
+ that names the row and then your priced lines.
144
+ - `settlement`: the receipt of the capture that paid the row. `c.after_payment`
145
+ runs once this receipt is recorded, so it can refund against it.
146
+
147
+ When the capture returns, the engine pushes `{"order_id": "…", "payment_state":
148
+ "paid"}` to the row's owner on the `payment` topic. Refund through the claim,
149
+ `refund(psp_reference:, amount_cents:, key:)`: `key` names this refund among
150
+ the refunds of the charge, so a retry with the same key refunds once. It
151
+ records the refund in `kiosk.refunds`, and recent spend leaves the refunded
152
+ cents out. The spending cap gets the refunded amount back. When the refund
153
+ returns the whole charge, set the row's status column to `refunded`, which is
154
+ where `payment_state` reads it. Then call `announce(id, "refunded")` on the
155
+ claim to push the same topic.
156
+
157
+ A repeat of a chain whose capture did not complete runs the capture again under
158
+ the same key, the cart mandate id, so your PSP adapter must key its charge by
159
+ it (`kiosk-pay-stripe` does). A charge refused with no money moved marks the
160
+ cart declined: it stops counting against `spending_cap` at once, and a repeat of
161
+ that chain answers `409 conflict`.
162
+
163
+ Set `c.spending_cap = Kiosk::Server::ColumnSpendingCap.new` to cap what each
164
+ assistant spends. The human sets the cap on `/kiosk/auth/assistants`: an amount
165
+ in cents and a currency. The engine refuses a `pay` that passes the cap, or a
166
+ `pay` in another currency, with `403 spending_cap_exceeded` before the capture.
167
+
168
+ ### Proof of work
169
+
170
+ Set a policy and the engine tolls the first call of each access token; the
171
+ token then calls freely until it expires (`access_token_ttl`, 3600 seconds by
172
+ default). With the ready-made policy, the more the customer spent here, the
173
+ smaller the chance that a session pays the toll:
174
+
175
+ ```ruby
176
+ Kiosk.configure do |c|
177
+ c.pow_secret = ENV.fetch("KIOSK_POW_SECRET")
178
+ c.reputation_policy = Kiosk::Reputation::Policies::RecentSpend.new(within: 1.week, cap: 100)
179
+ end
180
+ ```
181
+
182
+ The engine counts the customer's settlements, and the visits you record when
183
+ you honour a booking that takes no payment:
184
+
185
+ ```ruby
186
+ Kiosk::Visit.record!(user_id: booking.user_id, value_cents: 45_00)
187
+ ```
188
+
189
+
190
+ ## Upgrading
191
+
192
+ ### The schema
193
+
194
+ `rails generate kiosk:install` emits the **genesis** — the whole current shape,
195
+ as migrations. That is the fresh-install path. Migrations already in
196
+ `db/migrate/` are never re-emitted and never edited: a change to the Kiosk
197
+ schema arrives as a new file you copy in and migrate, so `db:migrate` on your
198
+ existing database transfers only what it has not run.
199
+
200
+ Each MAJOR publishes its own genesis and drops the previous major's chain, so
201
+ **an upgrade crossing a major stops at it.** Install that major, `bin/rails
202
+ db:migrate`, then move on — one major at a time. A fresh install at any major
203
+ gets that major's genesis and replays no history at all.
204
+
205
+ MINOR and PATCH releases only ever add files to the chain.
206
+
207
+ ### The configuration
208
+
209
+ `config/initializers/kiosk.rb` is **yours**. Nothing regenerates it, and no
210
+ upgrade edits it.
211
+
212
+ - **Every new setting ships a working default**, so an initializer you never
213
+ touch keeps working across an upgrade. The generated file is documentation of
214
+ what can be set, not the source of truth for what is set.
215
+ - **A setting that cannot have a safe default fails closed, naming itself**,
216
+ with a `Kiosk::Server::Errors::ConfigurationError`. `c.roles` with no
217
+ `c.registration_role` among them stops the boot. `Kiosk.configure` refuses a
218
+ `c.pow_secret` shorter than 32 bytes. With the toll enabled and no
219
+ `c.pow_secret`, the first tolled request is refused.
220
+ - **An unknown, removed or renamed setting is a `NoMethodError` on
221
+ `Kiosk::Configuration`**, which names the key you typed, so an initializer
222
+ carrying an old name stops the boot instead of being silently ignored.
223
+
224
+ The principle behind all three: generate as little as possible, derive at boot
225
+ whatever can be derived, default whatever can be defaulted, and fail closed on
226
+ what can be neither.
227
+
228
+
229
+ ## Multi-process deployments
230
+
231
+ The PoW gate keeps spent challenge ids in `c.pow_spent_store`, by default
232
+ `Kiosk::Server::PowSpentStores::ActiveRecord`: the `kiosk.pow_spent` table that
233
+ `bin/rails g kiosk:install` lays down. Every process and every deploy shares it,
234
+ so a proof is accepted once (kiosk.tech `protocol.md` §15.2).
235
+
236
+ Any other backend works: the contract is `claim(id, exp) → Boolean`,
237
+ `release(id)`, `spent?(id)`, `mark_spent(id, exp)`, and `claim` **MUST** be one
238
+ atomic operation (Redis `SET … NX EX`, or SQL `INSERT … ON CONFLICT`). A
239
+ read-then-write reintroduces exactly the replay race the gate closes.
240
+
241
+ Token revocation lives in `c.revocation_store`, by default
242
+ `Kiosk::Server::RevocationStores::ActiveRecord`: the `kiosk.revocations` table,
243
+ which `bin/rails g kiosk:install` lays down too. A revoke, a rebind or an unlink
244
+ stops the agent's older tokens in every process and after a restart, and a
245
+ token of an unlinked agent stops at once. The contract is
246
+ `revoke_all(agent_id, at:)`, `watermark_for(agent_id)` and
247
+ `revoked?(agent_id:, iat:)`.
248
+
249
+ Outstanding auth challenges live in `c.auth_challenge_store`, by default
250
+ `Kiosk::Server::AuthChallengeStores::ActiveRecord`: the `kiosk.auth_challenges`
251
+ table, which `bin/rails g kiosk:install` lays down too. A challenge issued by one
252
+ process is redeemed by any other. Any other backend works: the contract is
253
+ `put(public_key_pem, nonce, exp)` / `take(public_key_pem, nonce) → Boolean`, and
254
+ `take` **MUST** be one atomic operation (SQL `DELETE … RETURNING`, Redis
255
+ `GETDEL`) so the store, not the application, decides which of two concurrent
256
+ presentations wins.
257
+
258
+ Events live in `c.event_store`, by default `Kiosk::Server::EventStores::ActiveRecord`:
259
+ the `kiosk.events` table from `bin/rails g kiosk:install`. The event stream runs
260
+ on the Action Cable adapter that `config/cable.yml` names for the environment. In
261
+ production that adapter must be one every process shares, such as `solid_cable`
262
+ in the host's Gemfile;
263
+ a production origin that serves events with no adapter there does not boot.
264
+
265
+ ## The audit sink
266
+
267
+ **Kiosk does not keep an audit trail. It hands you one and gets out of the way.**
268
+
269
+ Set a callable and it receives one `Kiosk::Server::ActionEvent` for **every
270
+ action invocation — successful and failed alike**:
271
+
272
+ ```ruby
273
+ # config/initializers/kiosk.rb
274
+ Kiosk.configure do |c|
275
+ c.audit_sink = ->(event) { AuditRow.create!(**event.to_h) }
276
+ end
277
+ ```
278
+
279
+ **The default is no sink**: nothing is emitted, nothing is built, and Kiosk
280
+ writes no audit record to any table of its own. It keeps no audit table, and
281
+ will not grow one: an audit trail the framework keeps is a retention policy the
282
+ framework decided for your customers' data.
283
+
284
+ ### What the event carries
285
+
286
+ | field | |
287
+ |---|---|
288
+ | `action` | the action's wire name |
289
+ | `user_id` / `agent_id` | the principal, and the assistant credential that acted (nil when the caller is not an agent) |
290
+ | `role` / `actor` | the active role (may be nil), and `"agent"` \| `"human"` \| `"service"` |
291
+ | `args` | **the arguments exactly as the handler received them** |
292
+ | `status` | `"ok"` or `"error"` |
293
+ | `error_class` / `error_message` | the raised error, untruncated, on the `"error"` branch |
294
+ | `cause_class` / `cause_message` | the error's own `#cause`, when it has one distinct from itself — the handler's exception behind a wrapper |
295
+ | `invoked_at` | when the invocation started |
296
+
297
+ ### The arguments arrive unredacted, and the PII is yours
298
+
299
+ `event.args` is verbatim: the delivery address, the passenger name, the cart,
300
+ the booking reference — whatever your verb takes. **Kiosk does not redact them
301
+ for you, and the moment you write them somewhere you are the data controller
302
+ for whatever they contain** — retention, encryption at rest, access control and
303
+ deletion requests are all yours. That is the deliberate shape of this seam:
304
+ Kiosk gives you the capability and does not pretend to have made your privacy
305
+ decisions for you.
306
+
307
+ Withholding them is one call, if that is what you want:
308
+
309
+ ```ruby
310
+ # names and JSON types, never values: {"salon_id" => "integer", "slot" => "string"}
311
+ c.audit_sink = ->(e) { Rails.logger.info(e.with_arg_types.to_h.to_json) }
312
+
313
+ # nothing at all
314
+ c.audit_sink = ->(e) { Siem.record(e.without_args.to_h) }
315
+
316
+ # per-field, because only you know which of your fields are hot
317
+ c.audit_sink = ->(e) { Siem.record(e.to_h.merge(args: e.args.except(:card_token))) }
318
+ ```
319
+
320
+ ### What is emitted, and what is not
321
+
322
+ Actions only. A **query** is not emitted (it changes nothing, and every read
323
+ would drown the trail). **`pay`** is not emitted — it already writes the far
324
+ richer AP2 mandate trail (`intent_mandates`, `cart_mandates`,
325
+ `payment_mandates`, `settlements`). A **refusal that never reached an action**
326
+ is not emitted: a 401 with no identity, a 404 for an unregistered name, a 405,
327
+ a 400 from argument validation, a 402 from the toll. Nothing was invoked, so
328
+ there is nothing to report — this is an action trail, not a request log. Put a
329
+ Rack middleware in front of the engine if a request log is what you want.
330
+
331
+ ### A sink that raises does not fail the action
332
+
333
+ The sink is your code and it is called after the action has already succeeded
334
+ or failed. If it raises a `StandardError` the error is logged (`Rails.logger`,
335
+ or `warn` outside Rails) and the invocation stands: a booking that succeeded
336
+ must not come back as a 500 because a broker was down. Emission also happens
337
+ **after** the action's transaction closes, so a slow sink cannot hold a
338
+ database transaction open and a failed action's `ROLLBACK` cannot erase the
339
+ record of it.
340
+
341
+ Any `#call`-able works — a lambda, or an instance of a class of yours when the
342
+ sink has state. A non-callable is rejected when you configure it, so a typo is
343
+ a boot failure rather than an audit trail that silently was never there.
344
+
345
+
346
+ ## Draw the routes
347
+
348
+ Bundling the gem draws no route at all, and an origin with no mount answers
349
+ nothing. `bin/rails g kiosk:install` writes the `kiosk` block into
350
+ `config/routes.rb`; you name your verbs in it:
351
+
352
+ ```ruby
353
+ # config/routes.rb
354
+ Rails.application.routes.draw do
355
+ kiosk do
356
+ queries :catalog
357
+ actions :place_order
358
+ end
359
+ end
360
+ ```
361
+
362
+ `kiosk` mounts the engine at `Kiosk.configuration.mount_path`, then draws
363
+ `GET <mount_path>/<name>` for each query and `POST <mount_path>/<name>` for each
364
+ action. Each route reaches the controller action that declares the verb, and
365
+ `bin/rails routes` prints that action. A block-less `kiosk` only mounts.
366
+
367
+ **The mount draws the PROTOCOL PLANE** — the paths whose shape is the spec's and
368
+ not yours. Under the mount: the reserved `schema`, `openapi.json` and `pay`,
369
+ `payment_setup` and the `payment_setup/return` page a payment provider sends the
370
+ human back to (both answer `501 module_not_served` without a `payment_provider`), the
371
+ kiosk-pop auth plane (`auth/challenge`, `auth/register`, `auth/login`,
372
+ `auth/revoke`), JWKS (`.well-known/jwks.json`), KYC (`agents/kyc`, and
373
+ `request_kyc` and `kyc/callback`, which answer `501 module_not_served` without a
374
+ `kyc_provider`) and the whole account-binding ceremony (the RFC 8628 claim wire,
375
+ `auth/link`/`claim`/`unlink`, the verify and «Link an assistant» pages). The
376
+ engine also installs the ROOT-relative discovery documents — `/agents.txt`,
377
+ `/agents.json`, `/auth.md`,
378
+ `/.well-known/{agent-configuration,kiosk.json,api-catalog}` — into the host app
379
+ via `routes.append`, because the agents.txt standard and RFC 8615 place them at
380
+ the origin root, outside any mount prefix. That install happens ONLY when the
381
+ engine is mounted: merely bundling the gem adds no routes.
382
+
383
+ **You name your OWN VERBS, and the METHOD FOLLOWS THE KIND** — GET for a
384
+ `kind :query`, POST for a `kind :action`. That is what the protocol says a verb
385
+ IS, so the routes state it instead of hiding it, and `bin/rails routes` prints
386
+ your actual wire.
387
+
388
+ **The mount comes FIRST.** Rails dispatches the first matching route, so
389
+ everything the engine draws wins over every verb route and no verb of yours can
390
+ shadow `schema`, `pay` or the auth plane. (You could not declare such a verb
391
+ anyway — `Kiosk::Handler` refuses a reserved name at boot.)
392
+
393
+ A path under the mount that names no route — including a verb called with the
394
+ other method — matches nothing, so it is the ordinary 404 Rails answers at any
395
+ unrouted path: no `code`, no `hint`. The mount-path middleware still stamps the
396
+ version headers on it. If you want the wire's own `404 verb_not_found` there,
397
+ draw a catch-all action of your own at the end of your routes; the engine does not
398
+ draw one, because an assistant reads `GET <mount>/schema` before it dials.
399
+ Declared-but-unrouted is the bug class this trade opens: every verb you declare
400
+ needs its name in the block.
401
+
402
+ **The protocol plane comes from the mount and from nowhere else.** Copying those
403
+ paths into your routes by hand is not a supported second way to draw them:
404
+ the mount above the copy already answers, so the line is dead, and a protocol
405
+ this gem keeps in step with the spec becomes a table you now own. Write your own
406
+ verbs; mount the rest.
407
+
408
+
409
+ ## Declaring queries and actions
410
+
411
+ An assistant reaches a provider at ONE ENDPOINT PER VERB: a query is
412
+ `GET <mount>/<query-name>` with its arguments in the query string, an action is
413
+ `POST <mount>/<action-name>` with its arguments in a JSON body. The operator
414
+ decides what those names are and what they mean. `Kiosk::Handler` is the module
415
+ that lets a controller answer them, and you name each one in the `kiosk` block — see
416
+ [Draw the routes](#draw-the-routes).
417
+
418
+ Kiosk ships a **mixin, not a base class**. Which superclass a handler controller
419
+ has is your decision; the `include` is the whole contract.
420
+
421
+ Which verb reaches a handler is a property of the **declaration**, not of the
422
+ class: `kind :query` puts it on `GET`, `kind :action` on `POST`, and one
423
+ controller may declare both — a resource you think of as one thing is one
424
+ controller. Split when your domain splits.
425
+
426
+ ```ruby
427
+ # app/controllers/kiosk/catalog_controller.rb
428
+ class Kiosk::CatalogController < ActionController::API
429
+ include Kiosk::Handler
430
+
431
+ kind :query
432
+ description "Lists what the shop has in stock right now, so the assistant " \
433
+ "can decide what to put in a basket."
434
+ input_schema type: "object", additionalProperties: false,
435
+ properties: { q: { type: "string" } }
436
+ output_schema type: "array",
437
+ items: { type: "object",
438
+ properties: { sku: { type: "string" },
439
+ price_cents: { type: "integer" } } }
440
+ example_params({ q: "milk" })
441
+ def catalog
442
+ render json: Product.in_stock.search(params[:q]).as_json
443
+ end
444
+ end
445
+ ```
446
+
447
+ ```ruby
448
+ # app/controllers/kiosk/orders_controller.rb
449
+ class Kiosk::OrdersController < ActionController::API
450
+ include Kiosk::Handler
451
+
452
+ kind :action
453
+ description "Places an order for the assistant's human and reserves the " \
454
+ "chosen delivery window. Nothing is charged until `pay`."
455
+ input_schema type: "object",
456
+ properties: { items: { type: "array" }, slot_at: { type: "string", format: "date-time" } },
457
+ required: %w[items slot_at]
458
+ output_schema type: "object",
459
+ properties: { order_id: { type: "string" },
460
+ total_cents: { type: "integer" } },
461
+ required: %w[order_id total_cents]
462
+ def create_order
463
+ order = Orders::Place.call(user_id: kiosk_identity.user_id, params: params)
464
+ render json: { order_id: order.id, total_cents: order.total_cents }
465
+ rescue Orders::SlotTaken => e
466
+ render json: { error: e.message, hint: "call delivery_slots again" }, status: :conflict
467
+ end
468
+ end
469
+ ```
470
+
471
+ Put handler controllers in `app/controllers/kiosk/`. The engine loads that
472
+ directory at boot and after every development reload, and each class that
473
+ includes `Kiosk::Handler` registers its verbs, so an edited, added or removed
474
+ verb lands without restarting the server.
475
+
476
+
477
+ ### What the macros do
478
+
479
+ Each macro records a declaration; the **next `def`** claims all pending ones and
480
+ becomes a wire verb. A method with no declarations above it is not a verb, so
481
+ helper methods stay invisible to the wire.
482
+
483
+ | macro | what it declares |
484
+ | --- | --- |
485
+ | `kind` | **Required.** `:query` (reached by `GET <mount>/<name>`) or `:action` (`POST <mount>/<name>`). A property of the declaration, so one controller may carry both. There is no default: either one would silently pick an HTTP method for you. `:action` is the DECLARATION name and nothing else: the wire command an action travels under is **`:run`**, so a `kind :action` verb reaches a reputation policy as `verb: :run` (`Kiosk::Handler` serves an action as `:run`; the reserved pay endpoint arrives as `:pay`). Branch on `:run` there, not on `:action` — see `kiosk-reputation`'s README. |
486
+ | `reach` | Optional, and the default is the strong one. Whose rows this verb may touch (spec §7.2): `:principal` (default — only the caller's own, or rows that belong to no principal), `:published`, `:consented` or `:role`. Declare nothing and the verb is held to per-principal scoping absolutely; a departure costs one line and is published on the catalog, so an assistant can tell an open board from a scoping bug. |
487
+ | `description` | Semantics **only**: what the verb does, how, and what it returns *in meaning*. Never a field list, a type, a required marker or a param name — those live in the schemas. |
488
+ | `input_schema` | **Required.** JSON Schema for the params. The input contract: every name, type, enum and range — and the wire coerces and validates every call against it. A verb that takes nothing still declares the empty closed object. |
489
+ | `output_schema` | **Required.** JSON Schema for what comes back, so an assistant knows the result shape without a call-and-observe probe — the only machine-readable statement of the answer, now that there is no response envelope. |
490
+ | `example_params` | A params object an assistant can copy verbatim. |
491
+ | `example_row` | A worked example of the result. |
492
+ | `wire_name` | Optional. The name agents call the verb by, when it cannot be the method name. |
493
+
494
+ The **Required** rows are enforced at declaration time, not at request time: a
495
+ verb that omits any of them raises an `ArgumentError` as its class body is read,
496
+ so the app fails to boot instead of publishing an incomplete contract.
497
+
498
+ All of them surface in `GET <mount>/schema`, which is how an assistant discovers
499
+ the surface.
500
+
501
+ **A schema slot may be a proc, when the constraint is a fact about your data.**
502
+ Any part of `input_schema`, `output_schema`, `example_params` or `example_row`
503
+ may be a zero-arity proc:
504
+
505
+ ```ruby
506
+ input_schema type: "object", additionalProperties: false,
507
+ properties: {
508
+ category_slug: { type: "string", enum: -> { Category.pluck(:slug) } },
509
+ }
510
+ ```
511
+
512
+ Write the proc, not the plain call. `enum: Category.pluck(:slug)` runs while the
513
+ class body is read — which is `db:create`, `db:migrate` and
514
+ `assets:precompile` as well as a serving boot, where there is no table to read —
515
+ and it captures a list that then goes stale for the life of the process. The
516
+ proc is called when the descriptor is SERVED, resolved once and reused for a
517
+ short window, and re-resolved after it: so adding a category publishes itself,
518
+ with no restart and no deploy. The catalog's `?v=` version moves with it, and
519
+ the discovery document republishes the new link within its own minute.
520
+ `description`, `kind`, `reach` and `wire_name` are not resolvable — the first is
521
+ prose semantics, two are routing facts fixed when the route is drawn, and
522
+ `reach` is a security claim about the verb: one computed from your rows could
523
+ change under a caller between the catalog it read and the call it made.
524
+
525
+
526
+ ### Two things to know
527
+
528
+ **Draw verb routes with `kiosk` only.** A route you draw at a handler action
529
+ yourself answers 404: the `kiosk` block names the verb that the action answers.
530
+ `Kiosk::Handler` runs authentication, the argument check, the proof-of-work gate
531
+ and the transaction around the action, before your own `before_action` filters.
532
+
533
+ ### What you get inside a handler
534
+
535
+ It is an ordinary Rails action. `before_action` filters run, `rescue_from`
536
+ applies, `params` is `ActionController::Parameters`, and the answer is whatever
537
+ you `render` — or the action's JSON template (a Jbuilder view, say), which is
538
+ the one rendered whatever `Accept` the caller sent. On top of that:
539
+
540
+ - `kiosk_identity` — the `Kiosk::Identity` the wire resolved (`user_id`,
541
+ `agent_id`, `role`, `actor`). The four transaction-local GUCs are already applied to
542
+ the connection, so SQL-side and RLS scoping work whether or not you read it.
543
+ - `render_kiosk_page(rows, next_cursor:, total:)` — answer one page of a large
544
+ query; leave out `rows` and the action's template renders them. The body stays the bare array every query answers: the cursor leaves as
545
+ an RFC 8288 `Link: <…?cursor=…>; rel="next"` response header and the
546
+ matching-row count as `X-Total-Count`. `Kiosk::Server::Cursor` has an offset
547
+ helper; pass `total:` only when you know it.
548
+ - `include Kiosk::Owned` — gives a model with a `user_id` column the scope
549
+ `own`, the current principal's rows. `Kiosk.current_user_id` is the same id,
550
+ for a column with another name.
551
+ - `Kiosk.reader_zone(default = nil)` — the zone the caller declared in
552
+ `Kiosk-Timezone`, else `default`.
553
+ - `Kiosk.clock_gap_note(name, zone, caller_zone)` — when the place's `zone`
554
+ and `caller_zone` are on different clocks now, one sentence:
555
+ `Pera House runs on Europe/Istanbul time, 8 hours ahead of your America/New_York.`
556
+ Else `nil`. Pass `Kiosk.reader_zone` as `caller_zone`. Add your own sentence
557
+ that every time in the answer is the place's.
558
+ - `include Kiosk::SkillLink` — in a page controller: every answer carries
559
+ `Link: <skill_url>; rel="kiosk"`, the discovery signal for a client that
560
+ does not read HTML.
561
+ - `Kiosk::Settlement` and `Kiosk::CartMandate` — read models over the receipts
562
+ `pay` records. `Kiosk::Settlement.own` is the caller's own;
563
+ `Kiosk::Settlement.joins(:cart_mandate).merge(Kiosk::CartMandate.referencing(order_id: id))`
564
+ is the settlement whose cart names your row by the key your line items carry.
565
+ - The handler runs inside the wire's GUC-scoped transaction, so raising rolls
566
+ back — and so does rendering a non-2xx, which the seam converts into a raise.
567
+
568
+ Errors are Rails' idiom, end to end. The error-**code** vocabulary is the wire
569
+ contract — a closed table, not a class hierarchy. Mind which side of the seam
570
+ you are on: a handler names a code the Rails way, under `error.code` in the body
571
+ it renders, and the wire re-renders that as an RFC 9457
572
+ `application/problem+json` document whose `code` is a FLAT top-level member —
573
+ `error.code` is the handler-side spelling, `code` is what travels and what an
574
+ assistant branches on. Three Rails-native moves cover all of it:
575
+
576
+ - `render json: {...}, status: :bad_request` — the status becomes its wire
577
+ code (400 `bad_request`, 401 `unauthenticated`, 403 `forbidden`,
578
+ 404 `not_found`, 409 `conflict`, 422 `bad_request`, 429 `quota_exceeded`).
579
+ - Raise what you would raise anyway. Any exception Rails knows a status for —
580
+ `params.require`, `ActiveRecord::RecordNotFound`, anything your app
581
+ registered in `config.action_dispatch.rescue_responses` (the same registry
582
+ policy libraries use) — is mapped to that status' wire code by one
583
+ `rescue_from` the include installs. Your own `rescue_from` declarations win
584
+ over it. Anything unregistered stays a 500 `action_failed`.
585
+ **The CODE travels; the exception's own SENTENCE does not.** That branch is
586
+ for exceptions you did not author, so the message would be some library's
587
+ wording — actionpack's «param is missing or the value is empty or invalid:
588
+ sku» is what `params.require` puts on a 400 — and it moves when a
589
+ dependency moves. The caller gets a Kiosk sentence and a `hint` for that
590
+ code; the exception's class, message and backtrace go to your `Rails.logger`.
591
+ When you mean to say something to the assistant, say it: render the envelope,
592
+ or raise a `Kiosk::Server::Errors` class with your own `message:` and
593
+ `hint:`. Both carry your words verbatim.
594
+ - For a code a bare status cannot name — `rls_denied`, or a *specific* 402
595
+ (`payment_setup_required` vs `payment_failed` vs `pow_required`) — render
596
+ the code explicitly:
597
+ `render json: { error: { code: "rls_denied", message: "…" } },
598
+ status: :forbidden`. It travels verbatim — the code becomes the problem
599
+ document's top-level `code`, the message its `detail` — while a bare 402/500
600
+ is never guessed at. (The gate-style `Kiosk::Server::Errors` classes remain
601
+ raisable too.)
602
+
603
+
604
+ ### The initializer holds configuration, not verbs
605
+
606
+ There is no second way to declare a verb — no block you register from an
607
+ initializer. A block there cannot be reloaded, cannot be reached by your
608
+ filters, `rescue_from` or strong parameters, and would teach — in the very file
609
+ an adopter copies — that Rails does not apply to the surface you expose to
610
+ assistants. Write a controller in `app/controllers/kiosk/`, and the initializer
611
+ keeps what an initializer is for: the identity providers, the payment provider,
612
+ the PoW gates.
613
+
614
+
615
+ ## Well-known endpoint (no booted Rails app required)
616
+
617
+ ```ruby
618
+ require "kiosk/server"
619
+
620
+ # The issuer is the AP2 mandate `iss` anchor; the builder refuses to answer
621
+ # without it, booted app or not.
622
+ Kiosk.configure { |c| c.issuer = "https://api.acme.example" }
623
+
624
+ doc = Kiosk::Server::WellKnown.build_json(base_url: "https://api.acme.example")
625
+ # => '{"kiosk":{"version":"1.0","endpoint":"https://api.acme.example/kiosk",...}}'
626
+ ```
627
+
628
+ ## License
629
+
630
+ Apache-2.0 — see `LICENSE.txt`.
631
+
632
+ ## Links
633
+
634
+ - [kiosk.tech](https://kiosk.tech)
635
+ - [Issue tracker](https://github.com/kiosk-hq/kiosk/issues)