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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +196 -0
- data/README.md +192 -138
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +21 -65
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +24 -7
- data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
- data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +17 -34
- data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
- data/app/controllers/kiosk/server/oauth_token_controller.rb +36 -32
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +20 -0
- data/app/models/kiosk/settlement.rb +1 -3
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -69
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -15
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +19 -25
- data/lib/kiosk/server/account_binding.rb +32 -62
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
- data/lib/kiosk/server/agent_rotation.rb +62 -0
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/assistant_key.rb +86 -0
- data/lib/kiosk/server/auth_toll.rb +6 -6
- data/lib/kiosk/server/binding_module_gate.rb +1 -1
- data/lib/kiosk/server/checkout.rb +57 -0
- data/lib/kiosk/server/client.rb +61 -0
- data/lib/kiosk/server/client_assertion.rb +154 -0
- data/lib/kiosk/server/client_registration.rb +98 -0
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +64 -75
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +38 -69
- data/lib/kiosk/server/dpop.rb +126 -0
- data/lib/kiosk/server/eddsa.rb +38 -0
- data/lib/kiosk/server/engine.rb +16 -13
- data/lib/kiosk/server/errors.rb +28 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +38 -10
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +13 -12
- data/lib/kiosk/server/executor/spending_cap.rb +17 -76
- data/lib/kiosk/server/executor.rb +86 -105
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +12 -3
- data/lib/kiosk/server/jwk.rb +83 -0
- data/lib/kiosk/server/jwks.rb +6 -0
- data/lib/kiosk/server/jwt_issuer.rb +7 -13
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +28 -29
- data/lib/kiosk/server/link_code.rb +9 -18
- data/lib/kiosk/server/linked_assistants.rb +22 -17
- data/lib/kiosk/server/mandate_verifier.rb +97 -139
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +31 -0
- data/lib/kiosk/server/oauth_error.rb +33 -0
- data/lib/kiosk/server/open_api.rb +31 -29
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +47 -78
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +30 -0
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +3 -3
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +9 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +369 -58
- data/lib/kiosk/server/schemas/binding.schema.json +24 -59
- data/lib/kiosk/server/schemas/kyc.schema.json +40 -78
- data/lib/kiosk/server/schemas/mandates.schema.json +123 -39
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/sd_jwt.rb +104 -0
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/signing_key.rb +18 -52
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +4 -4
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +272 -205
- data/lib/kiosk/server/wire_rendering.rb +2 -2
- data/lib/kiosk/server.rb +8 -2
- data/lib/kiosk/skill_link.rb +6 -2
- data/listen.py +38 -8
- metadata +34 -24
- data/app/models/kiosk/cart_mandate.rb +0 -21
- data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/agent_login.rb +0 -49
- data/lib/kiosk/server/agent_registration.rb +0 -88
- data/lib/kiosk/server/auth_challenge.rb +0 -33
- data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
- data/lib/kiosk/server/pop_verifier.rb +0 -87
- data/lib/kiosk/server/schema_definitions.rb +0 -351
- data/lib/kiosk/server/settled_spend.rb +0 -21
- 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
|
-
- **
|
|
12
|
-
- **Account binding** — the claim/link ceremonies bind
|
|
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
|
|
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
|
-
- **
|
|
17
|
-
- **
|
|
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
|
|
22
|
-
- **`Kiosk::Server::
|
|
23
|
-
- **`Kiosk::Server::
|
|
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.
|
|
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
|
|
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.**
|
|
46
|
-
|
|
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"
|
|
48
|
+
gem "kiosk-server"
|
|
52
49
|
```
|
|
53
50
|
|
|
54
51
|
Or, via the meta-gem:
|
|
55
52
|
|
|
56
53
|
```ruby
|
|
57
|
-
gem "kiosk-all"
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
99
|
-
c.
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
113
|
-
|
|
114
|
-
own price for the row. That price is the one thing you write — a
|
|
115
|
-
answers it from your catalog, given the row and the
|
|
116
|
-
|
|
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.
|
|
128
|
-
c.after_payment
|
|
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
|
|
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
|
-
- `
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 `
|
|
152
|
-
cents out.
|
|
153
|
-
|
|
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
|
|
158
|
-
the same key, the
|
|
159
|
-
it (`kiosk-pay-stripe` does).
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
178
|
-
c.reputation_policy
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
232
|
-
`Kiosk::Server::PowSpentStores::ActiveRecord`: the `
|
|
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 `
|
|
243
|
-
which `bin/rails g kiosk:install` lays down too. A
|
|
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 `
|
|
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
|
|
289
|
-
| `user_id` / `agent_id` | the
|
|
290
|
-
| `role` / `actor` | the active role (may be nil), and `"agent"` \| `"human"`
|
|
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
|
|
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
|
|
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 (`
|
|
325
|
-
`
|
|
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
|
|
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 :
|
|
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
|
|
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
|
-
|
|
372
|
-
`auth/
|
|
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
|
|
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/{
|
|
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
|
|
384
|
-
`kind :query`, POST for a `kind :action`. That is what the protocol says
|
|
385
|
-
IS, so the routes state it instead of hiding it, and `bin/rails routes` prints
|
|
386
|
-
your actual
|
|
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
|
|
390
|
-
shadow `schema`, `pay` or the auth plane. (You could not
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
459
|
+
operations; mount the rest.
|
|
407
460
|
|
|
408
461
|
|
|
409
462
|
## Declaring queries and actions
|
|
410
463
|
|
|
411
|
-
An assistant reaches
|
|
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
|
|
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
|
|
474
|
-
|
|
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
|
|
481
|
-
helper methods stay invisible to the
|
|
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
|
|
486
|
-
| `reach` | Optional, and the default is the strong one. Whose rows this
|
|
487
|
-
| `description` | Semantics **only**: what the
|
|
488
|
-
| `input_schema` | **Required.** JSON Schema for the params. The input contract: every name, type, enum and range — and the
|
|
489
|
-
| `output_schema` | **Required.** JSON Schema for what comes back, so an assistant knows the result shape
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
529
|
-
yourself answers 404: the `kiosk` block names the
|
|
530
|
-
|
|
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
|
|
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
|
|
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::
|
|
562
|
-
`pay`
|
|
563
|
-
`Kiosk::Settlement.joins(:
|
|
564
|
-
is the settlement whose
|
|
565
|
-
- The handler runs inside the
|
|
566
|
-
back — and so does rendering a non-2xx, which
|
|
567
|
-
|
|
568
|
-
Errors are Rails' idiom, end to end. The error-**code** vocabulary is the
|
|
569
|
-
contract — a closed table, not a class hierarchy. Mind which side of
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
658
|
+
### The initializer holds configuration, not operations
|
|
605
659
|
|
|
606
|
-
There is no second way to declare
|
|
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
|
|
666
|
+
the toll.
|
|
613
667
|
|
|
614
668
|
|
|
615
|
-
##
|
|
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
|
|
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
|
|