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
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'uri'
|
|
4
5
|
|
|
5
6
|
module Kiosk
|
|
6
7
|
module Server
|
|
7
|
-
# The discovery documents — kiosk.json, agents.txt, agents.json,
|
|
8
|
-
#
|
|
9
|
-
# of one configuration and the request's base URL, so they
|
|
8
|
+
# The discovery documents — kiosk.json, agents.txt, agents.json, the OAuth
|
|
9
|
+
# metadata (RFC 8414, RFC 9728), the skills index, ard.json, api-catalog and auth.md — rendered
|
|
10
|
+
# as pure functions of one configuration and the request's base URL, so they
|
|
11
|
+
# cannot drift.
|
|
10
12
|
module WellKnown
|
|
11
13
|
DOCUMENT_VERSION = '1.0'
|
|
12
14
|
|
|
13
15
|
AGENTS_VERSION = '1.0'
|
|
14
|
-
AGENTS_STANDARD = 'https://agents-txt.com
|
|
16
|
+
AGENTS_STANDARD = 'https://agents-txt.com'
|
|
17
|
+
AGENTS_SCHEMA = 'https://agents-txt.com/schema/agents-json/v1.0.json'
|
|
15
18
|
|
|
16
|
-
#
|
|
17
|
-
|
|
19
|
+
# agents.txt v1.0 names OAuth 2.0 `oauth2`. Its discovery document is the RFC 8414 metadata.
|
|
20
|
+
AUTHORIZATION = { protocols: %w[oauth2], discovery: '/.well-known/oauth-authorization-server' }.freeze
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
ap2: { description: 'Mandate-trust layer with VC presentations' },
|
|
21
|
-
required: true
|
|
22
|
-
}.freeze
|
|
22
|
+
SKILLS_INDEX_SCHEMA = 'https://schemas.agentskills.io/discovery/0.2.0/schema.json'
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
# AI Catalog requires `specVersion`; ARD v0.91 ignores it.
|
|
25
|
+
AI_CATALOG_VERSION = '1.0'
|
|
26
|
+
|
|
27
|
+
# The `name` and `description` of the pinned skill's frontmatter.
|
|
28
|
+
# kiosk-test-support's skill_pin_spec holds them to the published cut.
|
|
29
|
+
SKILL_NAME = 'kiosk-agent-protocol'
|
|
30
|
+
SKILL_DESCRIPTION = 'A protocol that lets an AI assistant use the service of any Kiosk operator -- ' \
|
|
31
|
+
'a shop, a booking desk, a job board, a classifieds board, or data, not only ' \
|
|
32
|
+
'commerce. Register, read the catalogue to learn the operations, then act: ' \
|
|
33
|
+
'browse, order, book, apply, pay -- and subscribe to the operator\'s event ' \
|
|
34
|
+
'stream for anything that you would otherwise wait for.'
|
|
35
|
+
|
|
36
|
+
# The one scope value. An AI assistant does not choose its role: the
|
|
37
|
+
# token request grants the role of its assistant account, whatever scope
|
|
38
|
+
# it names (RFC 6749 Section 3.3).
|
|
39
|
+
SCOPES = %w[default].freeze
|
|
40
|
+
|
|
41
|
+
ARD_API = 'The Kiosk discovery document of this origin: the endpoint, the sign-in and the catalogue. ' \
|
|
42
|
+
'Read it with the Kiosk skill.'
|
|
29
43
|
|
|
30
44
|
# `/.well-known/kiosk.json`.
|
|
31
45
|
def self.build(base_url:, config: Kiosk.configuration)
|
|
@@ -50,64 +64,98 @@ module Kiosk
|
|
|
50
64
|
base = base_url.to_s.chomp('/')
|
|
51
65
|
|
|
52
66
|
lines = ['# agents.txt — https://agents-txt.com', "# JSON: #{base}/agents.json"]
|
|
53
|
-
lines.push('',
|
|
54
|
-
lines.push('',
|
|
55
|
-
lines.push('', "Skills: #{config.skill_url}") if config.skill_url && !config.skill_url.to_s.empty?
|
|
67
|
+
lines.push('', "Authorization: #{AUTHORIZATION[:protocols].join(', ')}")
|
|
68
|
+
lines.push('', "Skills: #{config.skill_url}") if Shared.skill?(config)
|
|
56
69
|
|
|
57
70
|
"#{lines.join("\n")}\n"
|
|
58
71
|
end
|
|
59
72
|
|
|
60
|
-
# agents.json v1.0
|
|
73
|
+
# agents.json v1.0.
|
|
61
74
|
def self.agents_json(base_url:, config: Kiosk.configuration)
|
|
62
75
|
Shared.validate_issuer!(config)
|
|
63
|
-
doc = { version: AGENTS_VERSION, standard: AGENTS_STANDARD,
|
|
64
|
-
site: { name: site_name(config), url: base_url.to_s.chomp('/') }
|
|
65
|
-
|
|
66
|
-
doc[:
|
|
67
|
-
doc[:skills] = Shared.skills_list(config)
|
|
68
|
-
doc[:'x-kiosk'] = Shared.x_kiosk(config)
|
|
76
|
+
doc = { '$schema': AGENTS_SCHEMA, version: AGENTS_VERSION, standard: AGENTS_STANDARD,
|
|
77
|
+
site: { name: site_name(config), url: base_url.to_s.chomp('/') },
|
|
78
|
+
authorization: AUTHORIZATION }
|
|
79
|
+
doc[:skills] = [{ url: config.skill_url, description: SKILL_DESCRIPTION }] if Shared.skill?(config)
|
|
69
80
|
doc
|
|
70
81
|
end
|
|
71
82
|
|
|
72
|
-
# `/.well-known/
|
|
73
|
-
|
|
83
|
+
# `/.well-known/oauth-authorization-server` (RFC 8414): registration,
|
|
84
|
+
# tokens, and the device-code grant where the origin serves binding.
|
|
85
|
+
def self.oauth_authorization_server(base_url:, config: Kiosk.configuration)
|
|
74
86
|
Shared.validate_issuer!(config)
|
|
75
87
|
base = base_url.to_s.chomp('/')
|
|
76
88
|
endpoint = base + config.mount_path
|
|
89
|
+
doc = { issuer: Kiosk.current_issuer, registration_endpoint: "#{endpoint}/oauth/register",
|
|
90
|
+
token_endpoint: "#{endpoint}/oauth/token" }
|
|
91
|
+
doc[:device_authorization_endpoint] = "#{endpoint}/oauth/device_authorization" if config.serve_account_binding
|
|
92
|
+
# No authorization endpoint, so no response type.
|
|
93
|
+
doc.merge(jwks_uri: "#{endpoint}/.well-known/jwks.json", response_types_supported: [], scopes_supported: SCOPES,
|
|
94
|
+
grant_types_supported: Client.grant_types(config), **Shared.client_authentication,
|
|
95
|
+
service_documentation: "#{base}/auth.md", dpop_signing_alg_values_supported: AssistantKey.algorithms)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `/.well-known/oauth-protected-resource` (RFC 9728). RFC 9728 §3.3 binds a
|
|
99
|
+
# document at this path to the origin, so `resource` is the origin.
|
|
100
|
+
def self.oauth_protected_resource(base_url:, config: Kiosk.configuration)
|
|
101
|
+
Shared.validate_issuer!(config)
|
|
102
|
+
base = base_url.to_s.chomp('/')
|
|
103
|
+
{ resource: base, authorization_servers: [Kiosk.current_issuer], scopes_supported: SCOPES,
|
|
104
|
+
bearer_methods_supported: ['header'],
|
|
105
|
+
dpop_signing_alg_values_supported: AssistantKey.algorithms, dpop_bound_access_tokens_required: true,
|
|
106
|
+
resource_documentation: "#{base}/auth.md" }
|
|
107
|
+
end
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
# `/.well-known/agent-skills/index.json` (Agent Skills Discovery v0.2.0):
|
|
110
|
+
# the pinned skill and its digest. nil without a `skill_sha256`.
|
|
111
|
+
def self.agent_skills_index(config: Kiosk.configuration)
|
|
112
|
+
return unless Shared.skill?(config) && config.skill_sha256
|
|
113
|
+
|
|
114
|
+
{ '$schema': SKILLS_INDEX_SCHEMA,
|
|
115
|
+
skills: [{ name: SKILL_NAME, type: 'skill-md', description: SKILL_DESCRIPTION,
|
|
116
|
+
url: config.skill_url, digest: "sha256:#{config.skill_sha256}" }] }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `/.well-known/ard.json` (Agentic Resource Discovery v0.91), also served
|
|
120
|
+
# at `/.well-known/ai-catalog.json` (AI Catalog 1.0): this origin's
|
|
121
|
+
# kiosk.json and the skill that reads it.
|
|
122
|
+
def self.ard(base_url:, config: Kiosk.configuration)
|
|
123
|
+
Shared.validate_issuer!(config)
|
|
124
|
+
base = base_url.to_s.chomp('/')
|
|
125
|
+
urn = "urn:air:#{URI.parse(base).host}:kiosk"
|
|
126
|
+
entries = [{ identifier: "#{urn}:api", displayName: "#{site_name(config)} Kiosk endpoint",
|
|
127
|
+
type: 'application/json', url: "#{base}/.well-known/kiosk.json", description: ARD_API }]
|
|
128
|
+
if Shared.skill?(config)
|
|
129
|
+
entries << { identifier: "#{urn}:skill", displayName: SKILL_NAME, type: 'application/agent-skills+md',
|
|
130
|
+
url: config.skill_url, description: SKILL_DESCRIPTION }
|
|
131
|
+
end
|
|
132
|
+
{ specVersion: AI_CATALOG_VERSION, entries: entries }
|
|
85
133
|
end
|
|
86
134
|
|
|
87
|
-
# `/.well-known/api-catalog` (RFC 9727):
|
|
88
|
-
#
|
|
89
|
-
#
|
|
135
|
+
# `/.well-known/api-catalog` (RFC 9727): every verb as an `item` of the
|
|
136
|
+
# catalog, its method in the `kiosk-method` extension attribute, and the
|
|
137
|
+
# two service descriptions on the endpoint. Composed from configuration
|
|
138
|
+
# and the registries only, never a query.
|
|
90
139
|
def self.api_catalog(base_url:, config: Kiosk.configuration)
|
|
91
140
|
Shared.validate_issuer!(config)
|
|
92
141
|
base = base_url.to_s.chomp('/')
|
|
93
142
|
endpoint = base + config.mount_path
|
|
94
143
|
modules = Array(config.capabilities)
|
|
95
144
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{ linkset:
|
|
145
|
+
linkset = [{ anchor: "#{base}/.well-known/api-catalog",
|
|
146
|
+
item: Catalog.verb_items(endpoint) + Catalog.module_items(endpoint, modules) }]
|
|
147
|
+
descs = Catalog.service_descs(endpoint, modules, config)
|
|
148
|
+
linkset << { anchor: endpoint, 'service-desc': descs } if descs.any?
|
|
149
|
+
{ linkset: linkset }
|
|
101
150
|
end
|
|
102
151
|
|
|
103
|
-
# `/auth.md`: the auth methods in auth.md's
|
|
152
|
+
# `/auth.md`: the auth methods in auth.md's section order.
|
|
104
153
|
def self.auth_md(base_url:, config: Kiosk.configuration)
|
|
105
154
|
Shared.validate_issuer!(config)
|
|
106
155
|
base = base_url.to_s.chomp('/')
|
|
107
156
|
endpoint = base + config.mount_path
|
|
108
157
|
|
|
109
|
-
AuthMd.render(site: site_name(config), base: base, endpoint: endpoint,
|
|
110
|
-
urls: Shared.auth_urls(endpoint), skill_line: Shared.skill_discover_line(config))
|
|
158
|
+
AuthMd.render(site: site_name(config), base: base, endpoint: endpoint, urls: Shared.auth_urls(endpoint), config: config)
|
|
111
159
|
end
|
|
112
160
|
|
|
113
161
|
# The owner's name, else the issuer's host. Also titles the OpenAPI document.
|
|
@@ -137,60 +185,44 @@ module Kiosk
|
|
|
137
185
|
}
|
|
138
186
|
end
|
|
139
187
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Additive: a client that ignores binding still has register/login.
|
|
145
|
-
device_authorization_url: "#{endpoint}/oauth/device_authorization",
|
|
146
|
-
claim_url: "#{endpoint}/auth/claim"
|
|
147
|
-
}
|
|
188
|
+
# RFC 8414: how a client authenticates at the token endpoint.
|
|
189
|
+
def client_authentication
|
|
190
|
+
{ token_endpoint_auth_methods_supported: [Client::AUTH_METHOD],
|
|
191
|
+
token_endpoint_auth_signing_alg_values_supported: AssistantKey.algorithms }
|
|
148
192
|
end
|
|
149
193
|
|
|
150
|
-
#
|
|
151
|
-
def
|
|
152
|
-
{
|
|
153
|
-
schema: "#{config.mount_path}/schema?v=#{SchemaDocument.digest(config: config)}",
|
|
154
|
-
api_catalog: '/.well-known/api-catalog',
|
|
155
|
-
mount_path: config.mount_path,
|
|
156
|
-
api_version: Kiosk::Protocol::API_VERSION
|
|
157
|
-
}
|
|
194
|
+
# Every URL, also the binding ones: where binding is not served they answer 501.
|
|
195
|
+
def auth_section(endpoint)
|
|
196
|
+
auth_urls(endpoint).transform_keys { |step| :"#{step}_url" }
|
|
158
197
|
end
|
|
159
198
|
|
|
160
199
|
def auth_urls(endpoint)
|
|
161
200
|
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
registration: "#{endpoint}/oauth/register",
|
|
202
|
+
token: "#{endpoint}/oauth/token",
|
|
203
|
+
rotate: "#{endpoint}/auth/rotate",
|
|
204
|
+
device_authorization: "#{endpoint}/oauth/device_authorization",
|
|
205
|
+
claim: "#{endpoint}/auth/claim"
|
|
166
206
|
}
|
|
167
207
|
end
|
|
168
208
|
|
|
169
|
-
def pay_served?(config)
|
|
170
|
-
Array(config.capabilities).map(&:to_s).include?('pay')
|
|
171
|
-
end
|
|
172
|
-
|
|
173
209
|
def host_of(issuer)
|
|
174
|
-
require 'uri'
|
|
175
210
|
URI.parse(issuer.to_s).host || issuer.to_s
|
|
176
211
|
rescue URI::InvalidURIError
|
|
177
212
|
issuer.to_s
|
|
178
213
|
end
|
|
179
214
|
|
|
180
215
|
def skill_discover_line(config)
|
|
181
|
-
|
|
182
|
-
return '' if url.nil? || url.to_s.empty?
|
|
216
|
+
return '' unless skill?(config)
|
|
183
217
|
|
|
184
218
|
sha = config.skill_sha256
|
|
185
|
-
line = "-
|
|
219
|
+
line = "- Skill for AI assistants: #{config.skill_url}"
|
|
186
220
|
line += " (sha256 `#{sha}`)" if sha && !sha.to_s.empty?
|
|
187
221
|
line
|
|
188
222
|
end
|
|
189
223
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
[{ url: config.skill_url, description: 'Kiosk wire skill' }]
|
|
224
|
+
def skill?(config)
|
|
225
|
+
!config.skill_url.to_s.empty?
|
|
194
226
|
end
|
|
195
227
|
|
|
196
228
|
def validate_issuer!(config)
|
|
@@ -199,11 +231,11 @@ module Kiosk
|
|
|
199
231
|
raise ArgumentError,
|
|
200
232
|
'Kiosk.configuration.issuer must be set before serving ' \
|
|
201
233
|
'/.well-known/kiosk.json — the issuer ' \
|
|
202
|
-
'is the
|
|
234
|
+
'is the origin a token or a checkout names'
|
|
203
235
|
end
|
|
204
236
|
end
|
|
205
237
|
|
|
206
|
-
# The api-catalog's
|
|
238
|
+
# The api-catalog's links.
|
|
207
239
|
module Catalog
|
|
208
240
|
module_function
|
|
209
241
|
|
|
@@ -213,7 +245,8 @@ module Kiosk
|
|
|
213
245
|
return [] unless modules.include?('schema')
|
|
214
246
|
|
|
215
247
|
version = SchemaDocument.digest(config: config)
|
|
216
|
-
|
|
248
|
+
[{ href: "#{endpoint}/schema?v=#{version}", type: 'application/json' },
|
|
249
|
+
{ href: "#{endpoint}/openapi.json?v=#{version}", type: OpenApi::CONTENT_TYPE }]
|
|
217
250
|
end
|
|
218
251
|
|
|
219
252
|
# Sorted, so the document is byte-stable across boots.
|
|
@@ -223,139 +256,169 @@ module Kiosk
|
|
|
223
256
|
end
|
|
224
257
|
|
|
225
258
|
def module_items(endpoint, modules)
|
|
226
|
-
|
|
227
|
-
{ href: "#{endpoint}/#{path}", rel: 'item' } if modules.include?(mod)
|
|
228
|
-
end
|
|
259
|
+
modules.include?('pay') ? [verb_item(endpoint, 'pay', 'POST')] : []
|
|
229
260
|
end
|
|
230
261
|
|
|
231
262
|
# RFC 9264 §4.2.4.3: an extension attribute is an array of strings.
|
|
232
263
|
def verb_item(endpoint, name, method)
|
|
233
|
-
{ href: "#{endpoint}/#{name}",
|
|
264
|
+
{ href: "#{endpoint}/#{name}", 'kiosk-method': [method] }
|
|
234
265
|
end
|
|
235
266
|
end
|
|
236
267
|
|
|
237
268
|
# The `/auth.md` document.
|
|
238
269
|
# rubocop:disable-next Metrics/ModuleLength -- one Markdown document; its lines are prose
|
|
239
270
|
module AuthMd
|
|
240
|
-
|
|
271
|
+
# This operator also accepts a P-256 key, or says nothing.
|
|
272
|
+
def self.es256_line(config)
|
|
273
|
+
return '' unless config.assistant_algorithms.include?('ES256')
|
|
274
|
+
|
|
275
|
+
' This provider also accepts a P-256 key (ES256).'
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.render(site:, base:, endpoint:, urls:, config:)
|
|
241
279
|
<<~MARKDOWN
|
|
242
|
-
# #{site}
|
|
280
|
+
# auth.md — #{site}
|
|
243
281
|
|
|
244
282
|
How AI assistants authenticate against this provider's Kiosk
|
|
245
|
-
endpoint (`#{endpoint}`).
|
|
246
|
-
(https://kiosk.tech/specification.html).
|
|
283
|
+
endpoint (`#{endpoint}`). Protocol: the Kiosk specification
|
|
284
|
+
(https://kiosk.tech/specification.html). This file follows the
|
|
285
|
+
auth.md section order. Registration and tokens use OAuth 2.0
|
|
286
|
+
standards: RFC 7591, RFC 7523, RFC 9449 and RFC 8628.
|
|
247
287
|
|
|
248
288
|
## Discover
|
|
249
289
|
|
|
250
|
-
- This file:
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
#{
|
|
290
|
+
- This file: #{base}/auth.md
|
|
291
|
+
- Kiosk discovery document: #{base}/.well-known/kiosk.json
|
|
292
|
+
- OAuth metadata of this authorization server (RFC 8414): #{base}/.well-known/oauth-authorization-server
|
|
293
|
+
- OAuth metadata of this protected resource (RFC 9728): #{base}/.well-known/oauth-protected-resource
|
|
294
|
+
- Verb catalogue (PUBLIC, no token): #{endpoint}/schema
|
|
295
|
+
- Token-verification keys (JWKS): #{endpoint}/.well-known/jwks.json
|
|
296
|
+
#{Shared.skill_discover_line(config)}
|
|
256
297
|
|
|
257
298
|
## Pick a method
|
|
258
299
|
|
|
259
|
-
- **
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
- **
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
- **Link code** — supported
|
|
267
|
-
|
|
268
|
-
single-use code on the provider's site and hands it to the
|
|
269
|
-
assistant.
|
|
300
|
+
- **Open registration** — supported. You register your own
|
|
301
|
+
public key (RFC 7591). No human account is necessary. You
|
|
302
|
+
authenticate each token request with a client assertion that
|
|
303
|
+
your key signs (`private_key_jwt`, RFC 7523).
|
|
304
|
+
- **Claim ceremony** — supported. It binds your registered client
|
|
305
|
+
to an EXISTING account after its holder approves in their own
|
|
306
|
+
browser session (RFC 8628).
|
|
307
|
+
- **Link code** — supported. The account holder makes a
|
|
308
|
+
single-use code on the provider's site and gives it to you.
|
|
270
309
|
- **Identity assertion (ID-JAG)** — not supported (planned).
|
|
271
310
|
|
|
272
311
|
## Register
|
|
273
312
|
|
|
274
|
-
|
|
313
|
+
1. Make one Ed25519 key for this provider. Keep the private
|
|
314
|
+
key.#{es256_line(config)}
|
|
315
|
+
2. `POST #{urls[:registration]}` with `Content-Type: application/json`:
|
|
316
|
+
|
|
317
|
+
{"jwks":{"keys":[{"kty":"OKP","crv":"Ed25519","x":"…"}]},
|
|
318
|
+
"token_endpoint_auth_method":"private_key_jwt"}
|
|
275
319
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
challenge).
|
|
280
|
-
3. `POST #{urls[:register]}` `{ public_key, signed }`
|
|
281
|
-
→ `201 { agent_id, user_id, access_token }`
|
|
320
|
+
→ `201 { client_id, client_id_issued_at, jwks,
|
|
321
|
+
token_endpoint_auth_method, grant_types, dpop_bound_access_tokens }`
|
|
322
|
+
3. Keep `client_id`. Registration gives no token.
|
|
282
323
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
324
|
+
If you register a key that has a live client here, you get that
|
|
325
|
+
client again, with the same `client_id`. If the `client_id` is
|
|
326
|
+
new, the account holder unlinked your old client: ask them before
|
|
327
|
+
you continue. A body that is not correct gets
|
|
328
|
+
`400 { "error": "invalid_client_metadata", "error_description" }`.
|
|
329
|
+
|
|
330
|
+
Registration can have an Equihash proof-of-work toll. Then the
|
|
331
|
+
request gets `402 pow_required` with
|
|
332
|
+
`WWW-Authenticate: Kiosk-PoW realm="<issuer>"` and a `challenges`
|
|
333
|
+
array in the body. The toll is bound to the key that you
|
|
334
|
+
register. Solve EVERY challenge. Then send the SAME body again,
|
|
335
|
+
with the proofs in a `Kiosk-PoW` REQUEST HEADER as raw minified
|
|
336
|
+
JSON (no base64):
|
|
291
337
|
|
|
292
338
|
Kiosk-PoW: {"challenge":{…},"nonce":{"indices":[…],"header_nonce":0}}
|
|
293
339
|
|
|
294
|
-
Send N proofs as a JSON array in that one header, or as one
|
|
295
|
-
`Kiosk-PoW` header line
|
|
296
|
-
request body: a body `pow` field
|
|
297
|
-
|
|
298
|
-
on the
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
memory, and a toll needs an identity to charge.
|
|
340
|
+
Send N proofs as a JSON array in that one header, or as one
|
|
341
|
+
`Kiosk-PoW` header line for each proof. A proof is never in the
|
|
342
|
+
request body: the operator ignores a body `pow` field, and a
|
|
343
|
+
changed body makes the proofs incorrect. The same header answers a
|
|
344
|
+
`402 pow_required` on the verbs too. Most of them are GETs,
|
|
345
|
+
which have no body. The ONE endpoint under `#{endpoint}` that has
|
|
346
|
+
no toll is `GET #{endpoint}/schema`: it is public, the operator
|
|
347
|
+
serves it from memory, and a toll needs an identity to charge.
|
|
302
348
|
|
|
303
349
|
Do not write your own Equihash solver. The verifier is exact about
|
|
304
|
-
the seed construction, the index width and the tree
|
|
305
|
-
mismatch
|
|
306
|
-
failed.
|
|
307
|
-
`#{PowGate::POW_SOLVER_URL}
|
|
308
|
-
That URL
|
|
309
|
-
|
|
310
|
-
above publishes
|
|
350
|
+
the seed construction, the index width and the tree order. A
|
|
351
|
+
mismatch gets `403 forbidden`, and the answer does not say which
|
|
352
|
+
check failed. Get the reference solver from
|
|
353
|
+
`#{PowGate::POW_SOLVER_URL}`. The 403 `hint` names the same file.
|
|
354
|
+
That URL has no version and always serves the CURRENT solver.
|
|
355
|
+
Before you run it, compare its SHA-256 with the pin that the
|
|
356
|
+
skill above publishes.
|
|
311
357
|
|
|
312
358
|
## Claim ceremony
|
|
313
359
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
A client assertion is a compact JWT that your registered key
|
|
361
|
+
signs: `alg` EdDSA for an Ed25519 key, ES256 for a P-256 key. Its payload is `{iss, sub, aud, exp, jti}`: `iss` and `sub`
|
|
362
|
+
are your `client_id`, `aud` is the origin you connected to (take it
|
|
363
|
+
from your own request URL), `exp` is at most 300 seconds from now,
|
|
364
|
+
and `jti` is a new unique id. Send it as `client_assertion`, with
|
|
365
|
+
`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`.
|
|
366
|
+
The operator accepts each `jti` one time.
|
|
367
|
+
|
|
368
|
+
Claim binding (RFC 8628) binds YOUR client to the account holder's
|
|
369
|
+
existing account. The codes are single-use and short-lived:
|
|
370
|
+
|
|
371
|
+
1. `POST #{urls[:device_authorization]}` (form-encoded:
|
|
372
|
+
`client_assertion_type`, `client_assertion`) →
|
|
373
|
+
`{ device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }`
|
|
374
|
+
2. Tell the holder: "open <verification_uri>, enter <user_code>".
|
|
321
375
|
They approve in their own signed-in browser session.
|
|
322
|
-
3. Poll `POST #{
|
|
376
|
+
3. Poll `POST #{urls[:token]}` (form-encoded:
|
|
323
377
|
`grant_type=urn:ietf:params:oauth:grant-type:device_code`,
|
|
324
|
-
`device_code`,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
No binding happens without a valid possession proof.
|
|
378
|
+
`device_code`, `client_assertion_type`, `client_assertion`),
|
|
379
|
+
with a `DPoP` header as in Exchange. When the holder approves,
|
|
380
|
+
the answer is a token under the holder's account.
|
|
328
381
|
|
|
329
|
-
Link flow (
|
|
330
|
-
|
|
331
|
-
`POST #{
|
|
332
|
-
→ `
|
|
382
|
+
Link flow (the opposite direction): the holder makes a code on the
|
|
383
|
+
provider's site and gives it to you. Send
|
|
384
|
+
`POST #{urls[:claim]}` `{ code, client_assertion_type, client_assertion }`
|
|
385
|
+
→ `204 No Content`. Then get a token as in Exchange.
|
|
333
386
|
|
|
334
387
|
Where registration has a toll, the link-code claim and the FIRST
|
|
335
388
|
token poll of each `device_code` have the same toll. It comes
|
|
336
|
-
before the
|
|
337
|
-
SAME request again with the `Kiosk-PoW` header. Later
|
|
338
|
-
same `device_code`
|
|
389
|
+
before the operator reads the code. Solve it as for registration
|
|
390
|
+
and send the SAME request again with the `Kiosk-PoW` header. Later
|
|
391
|
+
polls of the same `device_code` have no toll.
|
|
339
392
|
|
|
340
|
-
A
|
|
341
|
-
|
|
342
|
-
carries over — claiming never resets an identity).
|
|
393
|
+
A binding keeps your `client_id` and your reputation. Your client
|
|
394
|
+
moves to the holder's account and gets the holder's role.
|
|
343
395
|
|
|
344
396
|
## Exchange
|
|
345
397
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
`
|
|
350
|
-
|
|
398
|
+
`POST #{urls[:token]}`, form-encoded:
|
|
399
|
+
`grant_type=client_credentials`, `client_assertion_type`,
|
|
400
|
+
`client_assertion`. Add a `DPoP` header (RFC 9449): a JWS with
|
|
401
|
+
`typ` `dpop+jwt`, the `alg` of your key and the public `jwk` of your
|
|
402
|
+
registered key, that the key signs, with the payload
|
|
403
|
+
`{jti, htm: "POST", htu: "#{urls[:token]}", iat}`. A token
|
|
404
|
+
request has no `ath`.
|
|
405
|
+
→ `200 { access_token, token_type: "DPoP", expires_in, scope }`.
|
|
406
|
+
`scope` is your role, and it is there only when you have a role.
|
|
407
|
+
|
|
408
|
+
To get a new token, do this request again: no ceremony repeats.
|
|
409
|
+
If you get `invalid_client`, sign a new assertion one time. If
|
|
410
|
+
that also fails, register your key again.
|
|
351
411
|
|
|
352
412
|
## Use the access_token
|
|
353
413
|
|
|
354
|
-
Send `Authorization:
|
|
355
|
-
under `#{endpoint}`
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
414
|
+
Send `Authorization: DPoP <access_token>` to the verbs
|
|
415
|
+
under `#{endpoint}` (every query, every action, and `pay`) with a
|
|
416
|
+
`DPoP` header: a proof (RFC 9449) that your registered key signs
|
|
417
|
+
for that method and URL, with `ath`, the hash of the token. A
|
|
418
|
+
token is bound to that key and works with no other key. Tokens are
|
|
419
|
+
EdDSA JWTs that the JWKS above verifies.
|
|
420
|
+
`GET #{endpoint}/schema` is the exception: it is PUBLIC, so you
|
|
421
|
+
can read the catalogue before you register.
|
|
359
422
|
|
|
360
423
|
## Errors
|
|
361
424
|
|
|
@@ -367,47 +430,51 @@ module Kiosk
|
|
|
367
430
|
"title":"Proof-of-work required","status":402,
|
|
368
431
|
"detail":"proof-of-work required","code":"pow_required"}
|
|
369
432
|
|
|
370
|
-
|
|
371
|
-
the closed vocabulary
|
|
372
|
-
`pow_required`, …);
|
|
373
|
-
and names the same
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
`501 module_not_served`
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
`
|
|
394
|
-
`
|
|
395
|
-
`
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
grant but the device-code one. Everything answers `400` except
|
|
399
|
-
`invalid_client`, which answers `401`.
|
|
433
|
+
Use `code` to decide what to do. It is a FLAT member of the
|
|
434
|
+
document, not nested, from the closed vocabulary
|
|
435
|
+
(`unauthenticated`, `not_found`, `conflict`, `pow_required`, …);
|
|
436
|
+
`type` is `https://kiosk.tech/problems/<code>` and names the same
|
|
437
|
+
fact. Some codes add members of their own, such as the
|
|
438
|
+
`challenges` array on a `402 pow_required`.
|
|
439
|
+
- Three codes say "it is not here", and each has a different
|
|
440
|
+
meaning. `404 verb_not_found`: no verb with that NAME is
|
|
441
|
+
registered here; `hint` lists the verbs that are, so read the
|
|
442
|
+
catalogue again and call a verb that exists. `404 not_found`:
|
|
443
|
+
the verb is real and an ARGUMENT names something that is not
|
|
444
|
+
there; the answer is final, so stop. `501 module_not_served`:
|
|
445
|
+
this origin does not serve that optional module (binding,
|
|
446
|
+
payment, KYC, the event stream; `detail` names which); do not
|
|
447
|
+
retry, and do what you would do at an operator that never
|
|
448
|
+
offered it.
|
|
449
|
+
- The OAuth endpoints (`/oauth/*`) use the OAuth error shape
|
|
450
|
+
`{ error, error_description }`. Two answers there are problem
|
|
451
|
+
documents: `501 module_not_served` where the origin does not
|
|
452
|
+
serve binding, and the toll (`402 pow_required` and its
|
|
453
|
+
refusals of a proof). Registration has one code,
|
|
454
|
+
`invalid_client_metadata`. The token and device-authorization
|
|
455
|
+
codes are a closed set of nine: `invalid_request`,
|
|
456
|
+
`invalid_client`, `invalid_grant`, `unsupported_grant_type`,
|
|
457
|
+
`invalid_dpop_proof` (RFC 9449), and the ceremony codes
|
|
458
|
+
`authorization_pending`, `slow_down`, `expired_token`,
|
|
459
|
+
`access_denied` (RFC 8628 §3.5). Every code answers `400`,
|
|
460
|
+
except `invalid_client`, which answers `401`.
|
|
400
461
|
|
|
401
462
|
## Revocation
|
|
402
463
|
|
|
403
|
-
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
464
|
+
- Key rotation: `POST #{urls[:rotate]}`
|
|
465
|
+
`{ client_assertion_type, client_assertion, new_client_assertion, jwks }`
|
|
466
|
+
puts the new key in `jwks` on your client. Your registered key
|
|
467
|
+
signs `client_assertion`, with `new_key`, the RFC 7638
|
|
468
|
+
thumbprint of the new key, in its payload. The new key signs
|
|
469
|
+
`new_client_assertion` for the same `client_id`. The answer is
|
|
470
|
+
your client information with the new key. Your `client_id` and
|
|
471
|
+
your reputation stay. Every earlier token stops. Get a new token
|
|
472
|
+
with the new key.
|
|
473
|
+
- Unlink: the account holder, or the provider, deactivates your
|
|
474
|
+
client (`POST #{endpoint}/auth/unlink`, authenticated by the
|
|
475
|
+
holder's session, or the provider's linked-assistants page).
|
|
476
|
+
Its tokens stop, and the token endpoint answers it
|
|
477
|
+
`invalid_client`. It does NOT become a standalone account.
|
|
411
478
|
MARKDOWN
|
|
412
479
|
end
|
|
413
480
|
end
|