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