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
data/README.md CHANGED
@@ -8,28 +8,25 @@ The Kiosk Rails engine — host-side surface for [Kiosk](https://kiosk.tech).
8
8
  The full host-side surface is shipped and covered by the gem's own suite
9
9
  (`bundle exec rspec` in this directory runs it and prints how many examples that is):
10
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.
11
+ - **Protocol controllers** — each operation's route (`GET <mount>/<query-name>`, `POST <mount>/<action-name>`) reaches the operator's controller action that declares the operation, and `Kiosk::Handler` runs the protocol's checks around it; `VerbController` refuses an operation's route that no action answers; `WireController` serves the two reserved paths `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`; `OauthRegistrationController` registers an assistant's public key (RFC 7591) and `OauthTokenController` gives a registered client an access token for a client assertion (RFC 7523) and a DPoP proof (the `client_credentials` grant); `AuthController` puts a new key on a client; every access token is bound to the client's registered key, and a call that carries one also carries a DPoP proof (RFC 9449) that `IdentityResolution` checks; JWKS backs stateless token verification.
12
+ - **Account binding** — the claim/link ceremonies bind a registered client to an existing assistant-account holder's account: the RFC 8628 device authorization and token poll, each authenticated by a client assertion, a session-authenticated verify page and «Link an assistant» page (minimal overridable engine views with no inline script or style, so a Content-Security-Policy of `default-src 'self'` holds), link-code mint/redeem, and unlink. The token endpoint gives the tokens; the durable `DeviceAuthorizationStores::ActiveRecord` store (the `create_kiosk_device_authorizations` migration) is the default.
13
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)).
14
+ - **`Kiosk::Handler`** — the mixin an operator includes into a controller of their own to declare operations 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
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).
16
+ - **Client registration and authentication** — `ClientRegistration`, `ClientAssertion`, `AssistantKey` (the one list of signing algorithms an assistant can use), `AuthToll`, and the pluggable agent-IdP resolve and mint per-agent identities.
17
+ - **Proof-of-work toll** — `PowGate` asks the toll that the reputation policy sets, as N proof-of-work challenges (soft dependency on `kiosk-reputation`; zero overhead when no policy is set).
18
18
  - **`Kiosk::Server::WellKnown`** — pure-Ruby builder for `/.well-known/kiosk.json`.
19
19
  - **`Kiosk::Server::Headers`** + **`HeadersMiddleware`** — Rack middleware that injects `Kiosk-Server-Version`, `Kiosk-API-Version`, `Kiosk-Min-Client` on `/kiosk/*` responses.
20
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`.
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 its `kiosk_*` tables (agents, mandates, settlements, refunds, events, visits), but it writes no audit record there. See [The audit sink](#the-audit-sink).
22
+ - **`Kiosk::Server::Engine`** — the Rails engine: `kiosk` in `config/routes.rb` mounts it, drawing the full mount-prefixed PROTOCOL surface (the reserved paths, auth, JWKS, KYC, account binding), then one route per operation 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)).
23
+ - **`Kiosk::Server::ConfigurationExtension`** — adds `mount_path`, `owner` (and the reputation and proof-of-work settings) to `Kiosk::Configuration`.
25
24
  - **`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
25
 
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.
26
+ kiosk-server is a Rails gem: it depends on railties, actionpack, actioncable, activerecord and activesupport (`~> 8.1`), and `require "kiosk/server"` loads them. `WellKnown` works without a BOOTED Rails app: it needs only the framework on the load path (see [The discovery document](#the-discovery-document-no-booted-rails-app-required)).
28
27
 
29
28
  ## Install
30
29
 
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
30
  ### Preconditions
34
31
 
35
32
  **Rails `~> 8.1`.** This gemspec pins `railties`, `actionpack`, `actioncable`,
@@ -38,23 +35,23 @@ that names a Rails gem is `kiosk-pay-stripe`, which pins `activerecord` at
38
35
  `~> 8.1`. **Rails 7.x and 8.0.x are excluded** — bundler refuses to resolve
39
36
  against an app on either, so for most "add Kiosk to an existing Rails app" this
40
37
  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.
38
+ here, so they are not claimed; widening the floor means adding a CI job first.
42
39
 
43
40
  **Ruby `>= 4.0`**, which every Kiosk gemspec declares and CI runs.
44
41
 
45
- **PostgreSQL.** The kiosk schema, the identity tables and the optional RLS
46
- backstop are Postgres; no other database is supported.
42
+ **PostgreSQL.** Kiosk's `kiosk_*` tables and the optional RLS backstop are
43
+ Postgres; no other database is supported.
47
44
 
48
45
  ### The line
49
46
 
50
47
  ```ruby
51
- gem "kiosk-server", github: "kiosk-hq/kiosk"
48
+ gem "kiosk-server"
52
49
  ```
53
50
 
54
51
  Or, via the meta-gem:
55
52
 
56
53
  ```ruby
57
- gem "kiosk-all", github: "kiosk-hq/kiosk"
54
+ gem "kiosk-all"
58
55
  ```
59
56
 
60
57
  ## Configure
@@ -72,11 +69,20 @@ Kiosk.configure do |c|
72
69
  c.roles = %i[customer master support]
73
70
  # The default role. The engine refuses to boot when `c.roles` is set and
74
71
  # 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
72
+ # registration, or a binding whose approving human resolves none) gets
76
73
  # this one. Name your least-privileged role. An origin that assigns roles to
77
74
  # nobody sets neither line, and then no token carries a `role` claim.
78
75
  c.registration_role = :customer
79
76
  c.owner = { name: "Acme Inc.", support: "support@acme.example" }
77
+ # The SHA-256 of the skill file that kiosk-server gives (Kiosk.configuration.skill_url):
78
+ # curl -fsS "$(bin/rails runner 'puts Kiosk.configuration.skill_url')" | shasum -a 256
79
+ # Without it, discovery advertises no skill pin. See "The skill pin" below.
80
+ c.skill_sha256 = "<the 64 hexadecimal characters that the command prints>"
81
+ # The Ed25519 key that signs access tokens, checkouts and receipts (EdDSA):
82
+ # openssl genpkey -algorithm ed25519 | base64
83
+ c.signing_key = Base64.decode64(ENV.fetch("KIOSK_SIGNING_KEY_B64"))
84
+ # An assistant key is an Ed25519 key. Add ES256 to also accept a P-256 key.
85
+ # c.assistant_algorithms = %w[EdDSA ES256]
80
86
  # c.mount_path = "/kiosk" # default
81
87
  # Optional: one event per action invocation. See "The audit sink".
82
88
  # c.audit_sink = ->(event) { AuditRow.create!(**event.to_h) }
@@ -90,30 +96,34 @@ Set a KYC provider adapter (a `kiosk-kyc-*` gem) and the engine serves
90
96
  `kyc_verification` topic, and records the verified attributes against the
91
97
  person. Declare the attributes your gated actions need; gate with
92
98
  `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
99
+ calling customer holds them. An action that needs only some of them passes
94
100
  their names, `require!(%w[age_over_18])`, and may pass its own `detail:`.
95
101
 
96
102
  ```ruby
97
103
  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
104
+ c.kyc_claims = %w[age_over_18]
105
+ c.kyc_provider = Kiosk::KycProviders::Proveli.new(operator_id: "acme",
106
+ intake_secret: ENV.fetch("KIOSK_PROVELI_INTAKE_SECRET"),
107
+ public_key: ENV.fetch("KIOSK_PROVELI_PUBLIC_KEY_PEM"))
103
108
  end
104
109
  ```
105
110
 
111
+ The provider names the `iss` it signs, its Ed25519 public key and the `aud` it
112
+ mints for you, and the engine checks every attestation against them. To accept
113
+ attestations at `POST <mount>/agents/kyc` only, with no `request_kyc`, set
114
+ `c.kyc_provider = Kiosk::KycProviders::Base.new(issuer:, public_key:, audience:)`.
115
+
106
116
  ### Payments
107
117
 
108
118
  Set a PSP adapter (a `kiosk-pay-*` gem) and the engine serves `pay`. Wrap it in
109
119
  `Kiosk::Server::PaymentClaim` and the engine keeps §11.6's operator half for you:
110
120
  it claims your payable row (`unpaid → paying`) before the capture, so a second
111
121
  `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.
122
+ `paid` when the capture returns. The checkout the assistant pays is one your
123
+ origin signed, so under the claim the engine checks one thing: that its total
124
+ is still your own price for the row. That price is the one thing you write — a
125
+ checker that answers it from your catalog, given the row and the checkout's
126
+ item lines, or returns a String saying why the checkout is refused.
117
127
 
118
128
  ```ruby
119
129
  module PriceChecker
@@ -124,8 +134,8 @@ Kiosk.configure do |c|
124
134
  c.payment_provider = Kiosk::Server::PaymentClaim.new(
125
135
  psp, currency: "eur", model: "Order", reference: "order_id", query: "my_orders"
126
136
  )
127
- c.cart_price_checker = PriceChecker
128
- c.after_payment = ->(order_id) { CourierDispatchJob.arm!(order_id) } # optional
137
+ c.checkout_price_checker = PriceChecker
138
+ c.after_payment = ->(order_id) { CourierDispatchJob.arm!(order_id) } # optional
129
139
  end
130
140
  ```
131
141
 
@@ -136,46 +146,69 @@ whose unpaid value is `unpaid`.
136
146
  The model also includes `Kiosk::Payable`, which gives each row:
137
147
 
138
148
  - `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
149
+ `refunded`. Your query of the customer's own rows publishes it, so a capture in flight never
140
150
  reads as *not paid*. `Order.own.with_settlement(Kiosk::Settlement.own)` reads
141
151
  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.
152
+ - `checkout(lines)`: the `checkout_jwt` your action returns, which the
153
+ assistant's AP2 mandates bind. Your origin signs it EdDSA with
154
+ `c.signing_key`, for the row's owner, with a random `jti`. Its first line names the row; then your
155
+ priced lines.
156
+ - `settlement`: the record of the capture that paid the row. `c.after_payment`
157
+ runs once this record is written, so it can refund against it.
146
158
 
147
159
  When the capture returns, the engine pushes `{"order_id": "…", "payment_state":
148
160
  "paid"}` to the row's owner on the `payment` topic. Refund through the claim,
149
161
  `refund(psp_reference:, amount_cents:, key:)`: `key` names this refund among
150
162
  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
163
+ records the refund in `kiosk_refunds`, and recent spend leaves the refunded
164
+ cents out. When the refund returns the whole charge, set the row's status
165
+ column to `refunded`, which is where `payment_state` reads it. Then call `announce(id, "refunded")` on the
155
166
  claim to push the same topic.
156
167
 
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.
168
+ A repeat of mandates whose capture did not complete runs the capture again
169
+ under the same key, the checkout mandate's `reference`, so your PSP adapter must
170
+ key its charge by it (`kiosk-pay-stripe` does). One checkout holds one live
171
+ checkout mandate: a second pair for it answers `409 conflict`. A charge refused
172
+ with no money moved marks the checkout mandate declined: a repeat of that pair
173
+ answers `409 conflict`, and a new pair for the same checkout is accepted.
174
+
175
+ A settled `pay` answers the settlement and the two AP2 receipts,
176
+ `checkout_receipt` and `payment_receipt`, which your origin signs EdDSA with
177
+ `c.signing_key`. The payment receipt carries the card network's id of the charge
178
+ that your PSP adapter's `capture` answers as `network_confirmation_id`. When the
179
+ adapter answers none, the receipt carries `not reported by the payment provider`.
180
+
181
+ Set `c.spending_cap = Kiosk::Server::ColumnSpendingCap.new` to cap what one
182
+ payment of each assistant may charge. The human sets the cap on
183
+ `/kiosk/auth/assistants`: an amount and a currency. The engine refuses a `pay`
184
+ whose checkout total is above the cap, or whose checkout is in another
185
+ currency, with `403 spending_cap_exceeded` before the capture. A cap of 0
186
+ stops the payments of that assistant.
167
187
 
168
188
  ### Proof of work
169
189
 
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:
190
+ The toll needs two gems beside kiosk-server: `kiosk-reputation`, which holds
191
+ the policies and the challenges, and `kiosk-pow-equihash`, the proof-of-work
192
+ backend. Add both to your Gemfile and `require` them. Both tolls sign their
193
+ challenges with `c.pow_secret`.
194
+
195
+ **The call toll.** Set a policy and the engine asks it on each query, action
196
+ and `pay`. The engine counts each account's tolled calls since its last proof.
197
+ The assistants bound to one account share that count, and a new access token or
198
+ a key rotation keeps it. A binding adds the count of the assistant's previous
199
+ account to the count of its new account. With the ready-made policy, an account
200
+ solves a proof on every 10th call, and the more its customer spent here, the
201
+ more calls pass between two proofs.
202
+
203
+ **The registration toll.** `c.registration_pow_count` is the number of proofs
204
+ that a client registration, a link-code claim and the first token poll of each
205
+ device code cost. The default is 0, which asks for none.
174
206
 
175
207
  ```ruby
176
208
  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)
209
+ c.pow_secret = ENV.fetch("KIOSK_POW_SECRET")
210
+ c.reputation_policy = Kiosk::Reputation::Policies::RecentSpend.new(within: 1.week, cap: 100)
211
+ c.registration_pow_count = 1
179
212
  end
180
213
  ```
181
214
 
@@ -186,23 +219,40 @@ you honour a booking that takes no payment:
186
219
  Kiosk::Visit.record!(user_id: booking.user_id, value_cents: 45_00)
187
220
  ```
188
221
 
222
+ ### Change the signing key
223
+
224
+ `c.signing_key` signs every access token, checkout and receipt. To change it with no
225
+ break, set the new key as `c.signing_key` and the old one as
226
+ `c.previous_signing_key` (a public PEM is enough). The JWKS then publishes
227
+ both, and a token or checkout that either key signed verifies. Remove
228
+ `c.previous_signing_key` after one access-token lifetime, when no checkout that
229
+ the old key signed is still to be paid.
230
+
231
+ ```ruby
232
+ c.signing_key = Base64.decode64(ENV.fetch("KIOSK_SIGNING_KEY_B64"))
233
+ c.previous_signing_key = Base64.decode64(ENV.fetch("KIOSK_PREVIOUS_SIGNING_KEY_B64"))
234
+ ```
235
+
189
236
 
190
237
  ## Upgrading
191
238
 
192
- ### The schema
239
+ ### The tables
193
240
 
194
241
  `rails generate kiosk:install` emits the **genesis** — the whole current shape,
195
- as migrations. That is the fresh-install path. Migrations already in
242
+ as migrations. That is the fresh-install path. The migrations create Kiosk's
243
+ tables as `kiosk_*` tables in the schema `--schema` names, `public` by default;
244
+ `c.schema` names the same schema at run time. From 1.0, migrations already in
196
245
  `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.
246
+ tables arrives as a new file you copy in and migrate, so `db:migrate` on your
247
+ existing database transfers only what it has not run. Before 1.0 a release may
248
+ replace the genesis; its changelog entry says so.
199
249
 
200
250
  Each MAJOR publishes its own genesis and drops the previous major's chain, so
201
251
  **an upgrade crossing a major stops at it.** Install that major, `bin/rails
202
252
  db:migrate`, then move on — one major at a time. A fresh install at any major
203
253
  gets that major's genesis and replays no history at all.
204
254
 
205
- MINOR and PATCH releases only ever add files to the chain.
255
+ From 1.0, MINOR and PATCH releases only ever add files to the chain.
206
256
 
207
257
  ### The configuration
208
258
 
@@ -225,13 +275,24 @@ The principle behind all three: generate as little as possible, derive at boot
225
275
  whatever can be derived, default whatever can be defaulted, and fail closed on
226
276
  what can be neither.
227
277
 
278
+ ### The skill pin
279
+
280
+ Each kiosk-server release gives the skill of its own version as
281
+ `skill_url`. Do not set `skill_url`. After an upgrade to a release with a
282
+ different skill, your `skill_sha256` is the digest of the old file, and an AI
283
+ assistant that checks the digest refuses the skill. The skill pin test of
284
+ `kiosk-test-support` (`Kiosk::TestHelpers::SkillPin`) fails in this case, and
285
+ its message tells you how to make the new digest.
286
+
228
287
 
229
288
  ## Multi-process deployments
230
289
 
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
290
+ The toll keeps spent challenge ids in `c.pow_spent_store`, by default
291
+ `Kiosk::Server::PowSpentStores::ActiveRecord`: the `kiosk_pow_spent` table that
233
292
  `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).
293
+ so a proof is accepted once (kiosk.tech `protocol.md` §15.2). The token
294
+ endpoint keeps the `jti` of each client assertion in the same store, so an
295
+ assertion is accepted once too.
235
296
 
236
297
  Any other backend works: the contract is `claim(id, exp) → Boolean`,
237
298
  `release(id)`, `spent?(id)`, `mark_spent(id, exp)`, and `claim` **MUST** be one
@@ -239,24 +300,15 @@ atomic operation (Redis `SET … NX EX`, or SQL `INSERT … ON CONFLICT`). A
239
300
  read-then-write reintroduces exactly the replay race the gate closes.
240
301
 
241
302
  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
303
+ `Kiosk::Server::RevocationStores::ActiveRecord`: the `kiosk_revocations` table,
304
+ which `bin/rails g kiosk:install` lays down too. A key rotation, a rebind or an unlink
244
305
  stops the agent's older tokens in every process and after a restart, and a
245
306
  token of an unlinked agent stops at once. The contract is
246
307
  `revoke_all(agent_id, at:)`, `watermark_for(agent_id)` and
247
308
  `revoked?(agent_id:, iat:)`.
248
309
 
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
310
  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
311
+ the `kiosk_events` table from `bin/rails g kiosk:install`. The event stream runs
260
312
  on the Action Cable adapter that `config/cable.yml` names for the environment. In
261
313
  production that adapter must be one every process shares, such as `solid_cable`
262
314
  in the host's Gemfile;
@@ -285,9 +337,9 @@ framework decided for your customers' data.
285
337
 
286
338
  | field | |
287
339
  |---|---|
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"` |
340
+ | `action` | the action's published name |
341
+ | `user_id` / `agent_id` | the customer, and the assistant credential that acted (nil when the caller is not an assistant) |
342
+ | `role` / `actor` | the active role (may be nil), and `"agent"` \| `"human"` |
291
343
  | `args` | **the arguments exactly as the handler received them** |
292
344
  | `status` | `"ok"` or `"error"` |
293
345
  | `error_class` / `error_message` | the raised error, untruncated, on the `"error"` branch |
@@ -297,10 +349,10 @@ framework decided for your customers' data.
297
349
  ### The arguments arrive unredacted, and the PII is yours
298
350
 
299
351
  `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
352
+ the booking reference — whatever your action takes. **Kiosk does not redact them
301
353
  for you, and the moment you write them somewhere you are the data controller
302
354
  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:
355
+ deletion requests are all yours. That is deliberate:
304
356
  Kiosk gives you the capability and does not pretend to have made your privacy
305
357
  decisions for you.
306
358
 
@@ -321,8 +373,8 @@ c.audit_sink = ->(e) { Siem.record(e.to_h.merge(args: e.args.except(:card_token)
321
373
 
322
374
  Actions only. A **query** is not emitted (it changes nothing, and every read
323
375
  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**
376
+ richer AP2 mandate trail (`checkout_mandates`, `payment_mandates`,
377
+ `settlements`). A **refusal that never reached an action**
326
378
  is not emitted: a 401 with no identity, a 404 for an unregistered name, a 405,
327
379
  a 400 from argument validation, a 402 from the toll. Nothing was invoked, so
328
380
  there is nothing to report — this is an action trail, not a request log. Put a
@@ -347,68 +399,69 @@ a boot failure rather than an audit trail that silently was never there.
347
399
 
348
400
  Bundling the gem draws no route at all, and an origin with no mount answers
349
401
  nothing. `bin/rails g kiosk:install` writes the `kiosk` block into
350
- `config/routes.rb`; you name your verbs in it:
402
+ `config/routes.rb`; you name your operations in it:
351
403
 
352
404
  ```ruby
353
405
  # config/routes.rb
354
406
  Rails.application.routes.draw do
355
407
  kiosk do
356
408
  queries :catalog
357
- actions :place_order
409
+ actions :create_order
358
410
  end
359
411
  end
360
412
  ```
361
413
 
362
414
  `kiosk` mounts the engine at `Kiosk.configuration.mount_path`, then draws
363
415
  `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
416
+ action. Each route reaches the controller action that declares the operation, and
365
417
  `bin/rails routes` prints that action. A block-less `kiosk` only mounts.
366
418
 
367
419
  **The mount draws the PROTOCOL PLANE** — the paths whose shape is the spec's and
368
420
  not yours. Under the mount: the reserved `schema`, `openapi.json` and `pay`,
369
421
  `payment_setup` and the `payment_setup/return` page a payment provider sends the
370
422
  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
423
+ OAuth registration and token endpoints (`oauth/register`, `oauth/token`), the key
424
+ rotation (`auth/rotate`), JWKS (`.well-known/jwks.json`), KYC (`agents/kyc`, and
373
425
  `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,
426
+ `kyc_provider`) and the whole account-binding ceremony (the RFC 8628 claim paths,
375
427
  `auth/link`/`claim`/`unlink`, the verify and «Link an assistant» pages). The
376
428
  engine also installs the ROOT-relative discovery documents — `/agents.txt`,
377
429
  `/agents.json`, `/auth.md`,
378
- `/.well-known/{agent-configuration,kiosk.json,api-catalog}` — into the host app
430
+ `/.well-known/{kiosk.json,api-catalog,oauth-authorization-server,oauth-protected-resource,ard.json,ai-catalog.json}` and
431
+ `/.well-known/agent-skills/index.json` — into the host app
379
432
  via `routes.append`, because the agents.txt standard and RFC 8615 place them at
380
433
  the origin root, outside any mount prefix. That install happens ONLY when the
381
434
  engine is mounted: merely bundling the gem adds no routes.
382
435
 
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.
436
+ **You name your OWN OPERATIONS, and the METHOD FOLLOWS THE KIND** — GET for a
437
+ `kind :query`, POST for a `kind :action`. That is what the protocol says an
438
+ operation IS, so the routes state it instead of hiding it, and `bin/rails routes` prints
439
+ your actual protocol.
387
440
 
388
441
  **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.)
442
+ everything the engine draws wins over every route of an operation, and no
443
+ operation of yours can shadow `schema`, `pay` or the auth plane. (You could not
444
+ declare such an operation anyway — `Kiosk::Handler` refuses a reserved name at boot.)
392
445
 
393
- A path under the mount that names no route — including a verb called with the
446
+ A path under the mount that names no route — including an operation called with the
394
447
  other method — matches nothing, so it is the ordinary 404 Rails answers at any
395
448
  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,
449
+ version headers on it. If you want the protocol's own `404 verb_not_found` there,
397
450
  draw a catch-all action of your own at the end of your routes; the engine does not
398
451
  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.
452
+ Declared-but-unrouted is the bug class this trade opens: every operation you
453
+ declare needs its name in the block.
401
454
 
402
455
  **The protocol plane comes from the mount and from nowhere else.** Copying those
403
456
  paths into your routes by hand is not a supported second way to draw them:
404
457
  the mount above the copy already answers, so the line is dead, and a protocol
405
458
  this gem keeps in step with the spec becomes a table you now own. Write your own
406
- verbs; mount the rest.
459
+ operations; mount the rest.
407
460
 
408
461
 
409
462
  ## Declaring queries and actions
410
463
 
411
- An assistant reaches a provider at ONE ENDPOINT PER VERB: a query is
464
+ An assistant reaches an operator at ONE PATH PER OPERATION: a query is
412
465
  `GET <mount>/<query-name>` with its arguments in the query string, an action is
413
466
  `POST <mount>/<action-name>` with its arguments in a JSON body. The operator
414
467
  decides what those names are and what they mean. `Kiosk::Handler` is the module
@@ -418,7 +471,7 @@ that lets a controller answer them, and you name each one in the `kiosk` block
418
471
  Kiosk ships a **mixin, not a base class**. Which superclass a handler controller
419
472
  has is your decision; the `include` is the whole contract.
420
473
 
421
- Which verb reaches a handler is a property of the **declaration**, not of the
474
+ Which HTTP method reaches a handler is a property of the **declaration**, not of the
422
475
  class: `kind :query` puts it on `GET`, `kind :action` on `POST`, and one
423
476
  controller may declare both — a resource you think of as one thing is one
424
477
  controller. Split when your domain splits.
@@ -470,29 +523,29 @@ end
470
523
 
471
524
  Put handler controllers in `app/controllers/kiosk/`. The engine loads that
472
525
  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.
526
+ includes `Kiosk::Handler` registers its operations, so an edited, added or
527
+ removed operation lands without restarting the server.
475
528
 
476
529
 
477
530
  ### What the macros do
478
531
 
479
532
  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.
533
+ becomes an operation. A method with no declarations above it is not an operation, so
534
+ helper methods stay invisible to the protocol.
482
535
 
483
536
  | macro | what it declares |
484
537
  | --- | --- |
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. |
538
+ | `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 protocol command an action travels under is **`:run`**, so a `kind :action` operation reaches a reputation policy as `verb: :run` (`Kiosk::Handler` serves an action as `:run`; the reserved `pay` path arrives as `:pay`). Branch on `:run` there, not on `:action` — see `kiosk-reputation`'s README. |
539
+ | `reach` | Optional, and the default is the strong one. Whose rows this operation may touch (spec §7.2): `:principal` (default — only the caller's own, or rows that belong to no customer), `:published`, `:consented` or `:role`. Declare nothing and the operation is held to per-customer 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. |
540
+ | `description` | Semantics **only**: what the operation 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. |
541
+ | `input_schema` | **Required.** JSON Schema for the params. The input contract: every name, type, enum and range — and the protocol coerces and validates every call against it. An operation that takes nothing still declares the empty closed object. |
542
+ | `output_schema` | **Required.** JSON Schema for what comes back, so an assistant knows the result shape before it calls. The response has no envelope, so this is the only machine-readable statement of the answer. |
490
543
  | `example_params` | A params object an assistant can copy verbatim. |
491
544
  | `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. |
545
+ | `wire_name` | Optional. The published name that assistants call the operation by, when it cannot be the method name. |
493
546
 
494
547
  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,
548
+ operation that omits any of them raises an `ArgumentError` as its class body is read,
496
549
  so the app fails to boot instead of publishing an incomplete contract.
497
550
 
498
551
  All of them surface in `GET <mount>/schema`, which is how an assistant discovers
@@ -519,15 +572,16 @@ with no restart and no deploy. The catalog's `?v=` version moves with it, and
519
572
  the discovery document republishes the new link within its own minute.
520
573
  `description`, `kind`, `reach` and `wire_name` are not resolvable — the first is
521
574
  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
575
+ `reach` is a security claim about the operation: one computed from your rows could
523
576
  change under a caller between the catalog it read and the call it made.
524
577
 
525
578
 
526
579
  ### Two things to know
527
580
 
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
581
+ **Draw the routes of operations with `kiosk` only.** A route you draw at a
582
+ handler action yourself answers 404: the `kiosk` block names the operation that
583
+ the action answers.
584
+ `Kiosk::Handler` runs authentication, the argument check, the proof-of-work toll
531
585
  and the transaction around the action, before your own `before_action` filters.
532
586
 
533
587
  ### What you get inside a handler
@@ -537,7 +591,7 @@ applies, `params` is `ActionController::Parameters`, and the answer is whatever
537
591
  you `render` — or the action's JSON template (a Jbuilder view, say), which is
538
592
  the one rendered whatever `Accept` the caller sent. On top of that:
539
593
 
540
- - `kiosk_identity` — the `Kiosk::Identity` the wire resolved (`user_id`,
594
+ - `kiosk_identity` — the `Kiosk::Identity` the protocol resolved (`user_id`,
541
595
  `agent_id`, `role`, `actor`). The four transaction-local GUCs are already applied to
542
596
  the connection, so SQL-side and RLS scoping work whether or not you read it.
543
597
  - `render_kiosk_page(rows, next_cursor:, total:)` — answer one page of a large
@@ -546,7 +600,7 @@ the one rendered whatever `Accept` the caller sent. On top of that:
546
600
  matching-row count as `X-Total-Count`. `Kiosk::Server::Cursor` has an offset
547
601
  helper; pass `total:` only when you know it.
548
602
  - `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,
603
+ `own`, the signed-in customer's rows. `Kiosk.current_user_id` is the same id,
550
604
  for a column with another name.
551
605
  - `Kiosk.reader_zone(default = nil)` — the zone the caller declared in
552
606
  `Kiosk-Timezone`, else `default`.
@@ -558,28 +612,28 @@ the one rendered whatever `Accept` the caller sent. On top of that:
558
612
  - `include Kiosk::SkillLink` — in a page controller: every answer carries
559
613
  `Link: <skill_url>; rel="kiosk"`, the discovery signal for a client that
560
614
  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
615
+ - `Kiosk::Settlement` and `Kiosk::CheckoutMandate` — read models over the
616
+ records `pay` writes. `Kiosk::Settlement.own` is the caller's own;
617
+ `Kiosk::Settlement.joins(:checkout_mandate).merge(Kiosk::CheckoutMandate.referencing(order_id: id))`
618
+ is the settlement whose checkout names your row by the key your line items carry.
619
+ - The handler runs inside the protocol's GUC-scoped transaction, so raising rolls
620
+ back — and so does rendering a non-2xx, which `Kiosk::Handler` converts into a raise.
621
+
622
+ Errors are Rails' idiom, end to end. The error-**code** vocabulary is the protocol
623
+ contract — a closed table, not a class hierarchy. Mind which side of `Kiosk::Handler`
570
624
  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
625
+ it renders, and the protocol re-renders that as an RFC 9457
572
626
  `application/problem+json` document whose `code` is a FLAT top-level member —
573
627
  `error.code` is the handler-side spelling, `code` is what travels and what an
574
628
  assistant branches on. Three Rails-native moves cover all of it:
575
629
 
576
- - `render json: {...}, status: :bad_request` — the status becomes its wire
630
+ - `render json: {...}, status: :bad_request` — the status becomes its protocol
577
631
  code (400 `bad_request`, 401 `unauthenticated`, 403 `forbidden`,
578
632
  404 `not_found`, 409 `conflict`, 422 `bad_request`, 429 `quota_exceeded`).
579
633
  - Raise what you would raise anyway. Any exception Rails knows a status for —
580
634
  `params.require`, `ActiveRecord::RecordNotFound`, anything your app
581
635
  registered in `config.action_dispatch.rescue_responses` (the same registry
582
- policy libraries use) — is mapped to that status' wire code by one
636
+ policy libraries use) — is mapped to that status' error code by one
583
637
  `rescue_from` the include installs. Your own `rescue_from` declarations win
584
638
  over it. Anything unregistered stays a 500 `action_failed`.
585
639
  **The CODE travels; the exception's own SENTENCE does not.** That branch is
@@ -601,23 +655,23 @@ assistant branches on. Three Rails-native moves cover all of it:
601
655
  raisable too.)
602
656
 
603
657
 
604
- ### The initializer holds configuration, not verbs
658
+ ### The initializer holds configuration, not operations
605
659
 
606
- There is no second way to declare a verb — no block you register from an
660
+ There is no second way to declare an operation — no block you register from an
607
661
  initializer. A block there cannot be reloaded, cannot be reached by your
608
662
  filters, `rescue_from` or strong parameters, and would teach — in the very file
609
663
  an adopter copies — that Rails does not apply to the surface you expose to
610
664
  assistants. Write a controller in `app/controllers/kiosk/`, and the initializer
611
665
  keeps what an initializer is for: the identity providers, the payment provider,
612
- the PoW gates.
666
+ the toll.
613
667
 
614
668
 
615
- ## Well-known endpoint (no booted Rails app required)
669
+ ## The discovery document (no booted Rails app required)
616
670
 
617
671
  ```ruby
618
672
  require "kiosk/server"
619
673
 
620
- # The issuer is the AP2 mandate `iss` anchor; the builder refuses to answer
674
+ # The issuer is the origin a token or a checkout names; the builder refuses to answer
621
675
  # without it, booted app or not.
622
676
  Kiosk.configure { |c| c.issuer = "https://api.acme.example" }
623
677