kiosk-server 0.6.1 → 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 +138 -0
- data/README.md +134 -102
- 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 +3 -3
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +2 -7
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +8 -7
- data/app/controllers/kiosk/server/oauth_token_controller.rb +4 -1
- 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 +0 -2
- data/app/models/kiosk/settlement.rb +0 -2
- 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 -64
- 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 -14
- 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 +1 -1
- data/lib/kiosk/server/account_binding.rb +14 -5
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +1 -1
- data/lib/kiosk/server/agent_rotation.rb +1 -1
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/auth_toll.rb +5 -5
- data/lib/kiosk/server/client.rb +1 -1
- data/lib/kiosk/server/client_assertion.rb +3 -2
- data/lib/kiosk/server/client_registration.rb +6 -2
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +15 -38
- 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 +2 -2
- data/lib/kiosk/server/dpop.rb +6 -1
- data/lib/kiosk/server/engine.rb +2 -2
- data/lib/kiosk/server/errors.rb +21 -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 +23 -5
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +1 -1
- data/lib/kiosk/server/executor/spending_cap.rb +8 -66
- data/lib/kiosk/server/executor.rb +10 -10
- 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 +1 -1
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +26 -27
- data/lib/kiosk/server/linked_assistants.rb +21 -16
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +1 -0
- data/lib/kiosk/server/oauth_error.rb +3 -0
- data/lib/kiosk/server/open_api.rb +9 -9
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +3 -3
- 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 +4 -3
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +2 -1
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +1 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +2 -2
- data/lib/kiosk/server/schemas/binding.schema.json +3 -3
- data/lib/kiosk/server/schemas/kyc.schema.json +22 -3
- data/lib/kiosk/server/schemas/mandates.schema.json +5 -5
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +3 -3
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +14 -11
- data/lib/kiosk/server/wire_rendering.rb +1 -1
- data/lib/kiosk/server.rb +6 -2
- metadata +9 -8
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/schema_definitions.rb +0 -320
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
|
@@ -6,10 +6,10 @@ require 'json'
|
|
|
6
6
|
module Kiosk
|
|
7
7
|
module Server
|
|
8
8
|
# The proof-of-work toll, in front of every verb that reaches the executor.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# The policy decides on each call; a valid proof sets the account's count of
|
|
10
|
+
# tolled calls back to zero. With no `reputation_policy` it returns
|
|
11
|
+
# `:proceed` and touches nothing; `kiosk-reputation` is required only when a
|
|
12
|
+
# policy is set.
|
|
13
13
|
module PowGate
|
|
14
14
|
module_function
|
|
15
15
|
|
|
@@ -21,46 +21,25 @@ module Kiosk
|
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# `command` is the policy verb ({Executor::VERBS}); `verb` is the
|
|
24
|
+
# `command` is the policy verb ({Executor::VERBS}); `verb` is the published name
|
|
25
25
|
# the fingerprint binds.
|
|
26
|
-
def gate(identity:, command:, body:, pow:, method: 'POST', verb: nil) # rubocop:disable Metrics/ParameterLists --
|
|
26
|
+
def gate(identity:, command:, body:, pow:, method: 'POST', verb: nil) # rubocop:disable Metrics/ParameterLists -- WireController and the specs pass these six by name
|
|
27
27
|
config = Kiosk.configuration
|
|
28
28
|
policy = config.reputation_policy
|
|
29
|
-
return :proceed if policy.nil?
|
|
29
|
+
return :proceed if policy.nil?
|
|
30
30
|
|
|
31
|
-
secret
|
|
32
|
-
|
|
31
|
+
secret = pow_secret!(config)
|
|
32
|
+
factors = ReputationFactors.for(identity:, policy:)
|
|
33
|
+
spec = policy.challenge_for(identity: identity, verb: command.to_sym, factors: factors)
|
|
33
34
|
return :proceed if spec.nil?
|
|
34
35
|
|
|
35
36
|
fp = request_fingerprint(method: method, verb: verb || command, body: body)
|
|
36
37
|
PowToll.new(spec: spec, fingerprint: fp, secret: secret, config: config)
|
|
37
|
-
.collect!(pow).tap {
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def session_paid?(config, identity)
|
|
41
|
-
session = session_id(identity)
|
|
42
|
-
!session.nil? && config.pow_spent_store.spent?(session)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Records the token's session as paid.
|
|
46
|
-
def paid(config, identity)
|
|
47
|
-
session = session_id(identity)
|
|
48
|
-
config.pow_spent_store.mark_spent(session, identity.claims[:exp]) if session
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# The access token a toll pays for, by its `jti`, or nil for a caller without one.
|
|
52
|
-
def session_id(identity)
|
|
53
|
-
claims = identity&.claims || {}
|
|
54
|
-
"session:#{claims[:jti]}" if claims[:jti] && claims[:exp]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# The policy's toll for this caller and verb, or nil for none.
|
|
58
|
-
def toll_for(policy, identity, verb)
|
|
59
|
-
policy.challenge_for(identity: identity, verb: verb, factors: ReputationFactors.for(identity:, policy:))
|
|
38
|
+
.collect!(pow).tap { TolledCalls.reset!(identity.user_id) if factors.calls_since_proof }
|
|
60
39
|
end
|
|
61
40
|
|
|
62
41
|
def pow_secret!(config)
|
|
63
|
-
unless defined?(::Kiosk::Reputation)
|
|
42
|
+
unless defined?(::Kiosk::Reputation::Challenge)
|
|
64
43
|
raise Errors::ConfigurationError,
|
|
65
44
|
'Kiosk::Server: reputation_policy is set but kiosk-reputation is not loaded. ' \
|
|
66
45
|
"Add `require 'kiosk/reputation'` (and `gem 'kiosk-reputation'`) to your app."
|
|
@@ -6,6 +6,8 @@ module Kiosk
|
|
|
6
6
|
# operator signs as it signs a checkout, each naming its mandate by reference.
|
|
7
7
|
module Receipts
|
|
8
8
|
SUCCESS = 'Success'
|
|
9
|
+
# AP2 requires a network id; this text stands in when the PSP reports none.
|
|
10
|
+
NO_NETWORK_ID = 'not reported by the payment provider'
|
|
9
11
|
|
|
10
12
|
module_function
|
|
11
13
|
|
|
@@ -13,16 +15,15 @@ module Kiosk
|
|
|
13
15
|
def issue(checkout:, payment:, settlement_id:, psp_reference:, network_confirmation_id:)
|
|
14
16
|
{ checkout_receipt: sign(checkout, order_id: order_id(checkout)),
|
|
15
17
|
payment_receipt: sign(payment, payment_id: settlement_id, psp_confirmation_id: psp_reference,
|
|
16
|
-
network_confirmation_id: network_confirmation_id) }
|
|
18
|
+
network_confirmation_id: network_confirmation_id || NO_NETWORK_ID) }
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# The id in the checkout's first line item, which names the item paid for (§11.7).
|
|
20
22
|
def order_id(checkout) = checkout.line_items.first.values.first.to_s
|
|
21
23
|
|
|
22
|
-
# A network id the PSP did not report is left out.
|
|
23
24
|
def sign(mandate, **claims)
|
|
24
25
|
Checkout.signed({ status: SUCCESS, iss: Kiosk.current_issuer, iat: Time.now.to_i,
|
|
25
|
-
reference: mandate.reference, **claims }
|
|
26
|
+
reference: mandate.reference, **claims })
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
end
|
|
@@ -3,36 +3,43 @@
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
5
|
# The reputation inputs the engine knows itself: what the customer paid
|
|
6
|
-
# here and kept paid, and the visits the operator recorded, within a policy's
|
|
6
|
+
# here and kept paid, and the visits the operator recorded, within a policy's
|
|
7
|
+
# window; and the account's tolled calls since its last proof.
|
|
7
8
|
module ReputationFactors
|
|
8
9
|
module_function
|
|
9
10
|
|
|
10
11
|
# Empty factors unless the policy names a window (`#within`, in seconds).
|
|
12
|
+
# Reading the factors counts an assistant's call as one more tolled call of its account.
|
|
11
13
|
def for(identity:, policy:)
|
|
12
14
|
return ::Kiosk::Reputation::Factors.empty unless policy.respond_to?(:within) && identity&.user_id
|
|
13
15
|
|
|
14
16
|
count, cents = recent_spend(identity.user_id, Time.current - policy.within.to_i)
|
|
15
|
-
::Kiosk::Reputation::Factors.
|
|
17
|
+
::Kiosk::Reputation::Factors.new(settled_purchases_count: count, settled_purchases_cents: cents,
|
|
18
|
+
calls_since_proof: identity.agent_id && TolledCalls.count!(identity.user_id))
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
# Settlements and visits of this customer since `since`: their count and their cents.
|
|
19
22
|
# A settlement counts less what was refunded of it; one refunded in full does not count.
|
|
23
|
+
# The settlements and visits of each assistant account merged into this account count too.
|
|
20
24
|
def recent_spend(user_id, since)
|
|
21
25
|
sql = format(RECENT_SPEND_SQL, schema: Kiosk.configuration.schema)
|
|
22
26
|
row = ::ActiveRecord::Base.lease_connection.exec_query(sql, 'Kiosk recent spend', [user_id, since]).to_a.first
|
|
23
27
|
[row.fetch('count').to_i, row.fetch('cents').to_i]
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
MERGED_SQL = 'SELECT user_id FROM "%<schema>s".kiosk_assistant_accounts WHERE merged_into = $1'
|
|
26
31
|
RECENT_SPEND_SQL = <<~SQL.squish
|
|
27
32
|
SELECT count(*) AS count, COALESCE(SUM(cents), 0) AS cents FROM (
|
|
28
33
|
SELECT s.settled_amount_cents - COALESCE(SUM(r.refunded_amount_cents), 0) AS cents
|
|
29
|
-
FROM "%<schema>s".
|
|
30
|
-
WHERE s.user_id = $1 AND s.settled_at >= $2 GROUP BY s.id
|
|
34
|
+
FROM "%<schema>s".kiosk_settlements s LEFT JOIN "%<schema>s".kiosk_refunds r ON r.settlement_id = s.id
|
|
35
|
+
WHERE (s.user_id = $1 OR s.user_id IN (#{MERGED_SQL})) AND s.settled_at >= $2 GROUP BY s.id
|
|
31
36
|
HAVING s.settled_amount_cents > COALESCE(SUM(r.refunded_amount_cents), 0)
|
|
32
37
|
UNION ALL
|
|
33
|
-
SELECT value_cents FROM "%<schema>s".
|
|
38
|
+
SELECT value_cents FROM "%<schema>s".kiosk_visits
|
|
39
|
+
WHERE (user_id = $1 OR user_id IN (#{MERGED_SQL})) AND visited_at >= $2
|
|
34
40
|
) spend
|
|
35
41
|
SQL
|
|
42
|
+
private_constant :MERGED_SQL
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
end
|
|
@@ -19,6 +19,7 @@ module Kiosk
|
|
|
19
19
|
'POST <endpoint>/auth/claim' => 'binding.schema.json#/$defs/claimRequest',
|
|
20
20
|
'POST <endpoint>/auth/unlink' => 'binding.schema.json#/$defs/unlinkRequest',
|
|
21
21
|
'POST <endpoint>/agents/kyc' => 'kyc.schema.json#/$defs/request',
|
|
22
|
+
'POST <endpoint>/kyc/callback' => 'kyc.schema.json#/$defs/callback',
|
|
22
23
|
'POST <endpoint>/pay' => 'mandates.schema.json#/$defs/payRequest'
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
@@ -34,7 +35,7 @@ module Kiosk
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
# A missing member keeps the
|
|
38
|
+
# A missing member keeps the protocol's own `missing field:` sentence.
|
|
38
39
|
def validate_body!(body, exchange:)
|
|
39
40
|
return unless Kiosk.configuration.validate_requests
|
|
40
41
|
|
data/lib/kiosk/server/result.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
5
|
# The executor's internal carrier for a successful call; only {#to_payload}
|
|
6
|
-
# reaches the
|
|
6
|
+
# reaches the protocol, as the body.
|
|
7
7
|
class Result < Data.define(:kind, :payload, :next_cursor, :total)
|
|
8
8
|
KINDS = %i[rows value].freeze
|
|
9
9
|
|
data/lib/kiosk/server/routing.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Kiosk
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# The endpoint of one verb route. The controller action that declares the
|
|
41
|
-
# verb answers the
|
|
41
|
+
# verb answers the protocol request; the name is looked up per request, so a
|
|
42
42
|
# development reload is served. A name that no action answers goes to
|
|
43
43
|
# {VerbController}, which refuses it.
|
|
44
44
|
class VerbRoute
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kiosk.tech/spec/schemas/auth.schema.json",
|
|
4
|
-
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/auth.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE
|
|
4
|
+
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/auth.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE HTTP bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
|
|
5
5
|
"title": "Kiosk client registration and tokens (OAuth)",
|
|
6
|
-
"description": "The
|
|
6
|
+
"description": "The protocol objects of Section 5: RFC 7591 open client registration of the AI assistant's public key, RFC 7523 client assertions signed with that key (the private_key_jwt method), and DPoP-bound access tokens from the client_credentials grant (RFC 9449). The roots are open: they state what MUST be present and the shape of each member.",
|
|
7
7
|
"$defs": {
|
|
8
8
|
"publicJwk": {
|
|
9
9
|
"type": "object",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kiosk.tech/spec/schemas/binding.schema.json",
|
|
4
|
-
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/binding.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE
|
|
4
|
+
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/binding.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE HTTP bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
|
|
5
5
|
"title": "Kiosk account binding -- claim and link",
|
|
6
|
-
"description": "The
|
|
6
|
+
"description": "The protocol objects of Section 6, the one-time ceremonies that bind a registered AI assistant to an existing operator account: claim, an RFC 8628 device grant, and link. Both need the human's approval and a client assertion by the registered key. The two /oauth/* requests are form-encoded and have no schema here; their answers are in auth.schema.json.",
|
|
7
7
|
"$defs": {
|
|
8
8
|
"deviceAuthorization": {
|
|
9
9
|
"type": "object",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"oauthError": {
|
|
53
53
|
"$ref": "https://kiosk.tech/spec/schemas/auth.schema.json#/$defs/oauthError",
|
|
54
|
-
"description": "Section 6.1: the error answer of both /oauth/* endpoints, the one error shape
|
|
54
|
+
"description": "Section 6.1: the error answer of both /oauth/* endpoints, the one error shape in this protocol that is not an RFC 9457 problem document."
|
|
55
55
|
},
|
|
56
56
|
"linkCode": {
|
|
57
57
|
"type": "object",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kiosk.tech/spec/schemas/kyc.schema.json",
|
|
4
|
-
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/kyc.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE
|
|
4
|
+
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/kyc.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE HTTP bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
|
|
5
5
|
"title": "Kiosk KYC attestation",
|
|
6
|
-
"description": "The decoded JWS payload of a KYC attestation, the POST /kiosk/agents/kyc request body, the request_kyc answer, and the data of the kyc_verification event that delivers the attestation.",
|
|
6
|
+
"description": "The decoded JWS payload of a KYC attestation, the POST /kiosk/agents/kyc request body, the request_kyc answer, the body that the KYC provider posts to the callback and its answer, and the data of the kyc_verification event that delivers the attestation.",
|
|
7
7
|
"$defs": {
|
|
8
8
|
"attestation": {
|
|
9
9
|
"type": "object",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"sub": { "type": "string", "description": "MUST equal the authenticated user_id." },
|
|
14
14
|
"level": { "const": "verified", "description": "Binary; anything other than \"verified\" is rejected." },
|
|
15
15
|
"iss": { "type": "string", "description": "MUST equal the provider-configured KYC issuer." },
|
|
16
|
-
"aud": { "type": "string", "description": "OPERATOR-BINDING. The operator the attestation was minted for. MUST equal the operator's
|
|
16
|
+
"aud": { "type": "string", "description": "OPERATOR-BINDING. The operator the attestation was minted for. MUST equal the audience the operator's KYC provider mints for it (its origin, or a stable handle it registered); the operator rejects any other aud. This stops a claim minted for operator A being replayed to operator B." },
|
|
17
17
|
"iat": { "type": "integer", "description": "Issued-at timestamp. Not enforced by the verifier." },
|
|
18
18
|
"exp": { "type": "integer", "description": "One year after iat. An expired attestation is rejected." },
|
|
19
19
|
"attributes": { "$ref": "#/$defs/attributes" }
|
|
@@ -42,6 +42,25 @@
|
|
|
42
42
|
"status": { "const": "pending" }
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
+
"callback": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"description": "The body that the KYC provider posts to POST <endpoint>/kyc/callback when the human approves (Section 12.4.1). The operator answers 403 unless nonce equals the one that it kept.",
|
|
48
|
+
"required": ["request_id", "kyc_jws"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"request_id": { "type": "string", "description": "The provider's id for the verification that request_kyc opened." },
|
|
51
|
+
"nonce": { "type": "string", "description": "The one-time nonce that the provider gave the operator when it opened the verification." },
|
|
52
|
+
"kyc_jws": { "type": "string", "description": "The attestation as a compact EdDSA JWS." }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"callbackAnswer": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "The operator's 200 answer to a callback that it accepted.",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["ok"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"ok": { "const": true }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
45
64
|
"verificationEvent": {
|
|
46
65
|
"type": "object",
|
|
47
66
|
"description": "The data of a kyc_verification event: the human approved, and the event carries the provider-signed attestation to submit to POST /kiosk/agents/kyc.",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kiosk.tech/spec/schemas/mandates.schema.json",
|
|
4
|
-
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/mandates.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE
|
|
4
|
+
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/mandates.schema.json. It is here so RequestValidation can hold a reserved-plane request body to it at runtime, and so e2e/schema_conformance.rb can validate this origin's LIVE HTTP bytes against the PUBLISHED schema -- both with no second checkout and no network fetch. Identical to the upstream copy as PARSED JSON once this $comment is removed.",
|
|
5
5
|
"title": "Kiosk payment: the checkout and the AP2 v0.2 mandates",
|
|
6
|
-
"description": "The checkout an operator signs, the two closed AP2 v0.2 mandates an AI assistant signs to pay it, the POST <endpoint>/pay request body, the settlement it answers and the two receipts in it.
|
|
6
|
+
"description": "The checkout an operator signs, the two closed AP2 v0.2 mandates an AI assistant signs to pay it, the POST <endpoint>/pay request body, the settlement it answers and the two receipts in it. Over HTTP the checkout is a compact EdDSA JWT, and each mandate is a compact SD-JWT whose one disclosure is the mandate; these schemas describe the decoded objects.",
|
|
7
7
|
"$defs": {
|
|
8
8
|
"currency": {
|
|
9
9
|
"type": "string",
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
},
|
|
144
144
|
"paymentReceipt": {
|
|
145
145
|
"type": "object",
|
|
146
|
-
"description": "The claims of the payment receipt a successful pay answers (AP2 v0.2 payment_receipt.json, status Success).
|
|
147
|
-
"required": ["status", "iss", "iat", "reference", "payment_id", "psp_confirmation_id"],
|
|
146
|
+
"description": "The claims of the payment receipt a successful pay answers (AP2 v0.2 payment_receipt.json, status Success).",
|
|
147
|
+
"required": ["status", "iss", "iat", "reference", "payment_id", "psp_confirmation_id", "network_confirmation_id"],
|
|
148
148
|
"properties": {
|
|
149
149
|
"status": { "const": "Success" },
|
|
150
150
|
"iss": { "type": "string", "description": "The operator's issuer." },
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"reference": { "type": "string", "description": "The reference of the payment mandate: the base64url SHA-256 of its compact SD-JWT." },
|
|
153
153
|
"payment_id": { "type": "string", "description": "The settlement_id." },
|
|
154
154
|
"psp_confirmation_id": { "type": "string", "description": "The psp_reference." },
|
|
155
|
-
"network_confirmation_id": { "type": "string", "description": "The card network's id of the transaction." }
|
|
155
|
+
"network_confirmation_id": { "type": "string", "minLength": 1, "description": "The card network's id of the transaction, or `not reported by the payment provider` when the payment provider reports none (protocol.md 11.3)." }
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kiosk.tech/spec/schemas/pow.schema.json",
|
|
4
|
-
"title": "Kiosk proof-of-work",
|
|
5
4
|
"$comment": "VENDORED copy of the normative kiosk.tech schema at spec/schemas/pow.schema.json. This file is self-contained (no cross-file $ref) and must be kept in sync with the upstream normative copy.",
|
|
5
|
+
"title": "Kiosk proof-of-work",
|
|
6
6
|
"description": "The PoW challenge object (issued in a pow_required 402) and the proof(s) an agent returns in the Kiosk-PoW request header (raw JSON) on retry.",
|
|
7
7
|
"$ref": "#/$defs/powHeader",
|
|
8
8
|
"$defs": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"header_nonce": {
|
|
74
|
-
"$comment": "The bound is stated as an INCLUSIVE `maximum` of 2**32-1, matching how `indices` states its own: 4294967295 is exactly representable as an IEEE-754 double, so it survives any JSON parser, and an inclusive bound never refuses the largest legal value. Without a bound the field is unbounded
|
|
74
|
+
"$comment": "The bound is stated as an INCLUSIVE `maximum` of 2**32-1, matching how `indices` states its own: 4294967295 is exactly representable as an IEEE-754 double, so it survives any JSON parser, and an inclusive bound never refuses the largest legal value. Without a bound the field is unbounded in the JSON while both specs call it a u32 -- and the reference verifier folds it in with `pack(\"V\")`, which truncates mod 2**32 instead of raising, so 0, 2**32 and -(2**32) seed identically and one proof has infinitely many spellings. The verifier rejects out-of-range values for the same reason; schema and verifier state one bound, not two.",
|
|
75
75
|
"type": "integer",
|
|
76
76
|
"minimum": 0,
|
|
77
77
|
"maximum": 4294967295,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
|
-
# The identity a
|
|
5
|
+
# The identity a protocol request runs as. {#open} wraps the request in a
|
|
6
6
|
# transaction and sets the principal's GUCs transaction-locally, for
|
|
7
7
|
# operators who opt into RLS.
|
|
8
8
|
class SessionContext
|
|
@@ -19,7 +19,7 @@ module Kiosk
|
|
|
19
19
|
|
|
20
20
|
def self.open? = !Thread.current[KEY].nil?
|
|
21
21
|
|
|
22
|
-
# The identity the
|
|
22
|
+
# The identity the protocol resolved. Raises outside a session, where a scope
|
|
23
23
|
# built on it would otherwise match nothing.
|
|
24
24
|
def self.identity
|
|
25
25
|
return current.identity if open?
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# Each account's tolled calls since its last proof, in
|
|
6
|
+
# `tolled_calls.calls_since_proof`. The count belongs to the account: the
|
|
7
|
+
# assistants bound to one account share it, and a new access token or a
|
|
8
|
+
# new key does not set it back.
|
|
9
|
+
module TolledCalls
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Adds this call and answers the account's count, this call included.
|
|
13
|
+
# One statement, so calls made at the same time each get their own number.
|
|
14
|
+
def count!(user_id)
|
|
15
|
+
sql = <<~SQL.squish
|
|
16
|
+
INSERT INTO "#{schema}".kiosk_tolled_calls AS t (user_id, calls_since_proof) VALUES ($1, 1)
|
|
17
|
+
ON CONFLICT (user_id) DO UPDATE SET calls_since_proof = t.calls_since_proof + 1
|
|
18
|
+
RETURNING calls_since_proof
|
|
19
|
+
SQL
|
|
20
|
+
connection.exec_query(sql, 'Kiosk tolled call', [user_id]).rows.dig(0, 0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A valid proof sets the count back to zero.
|
|
24
|
+
def reset!(user_id)
|
|
25
|
+
connection.exec_query(%(UPDATE "#{schema}".kiosk_tolled_calls SET calls_since_proof = 0 WHERE user_id = $1),
|
|
26
|
+
'Kiosk tolled calls reset', [user_id])
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A binding adds the previous account's count to the new account's, so a
|
|
31
|
+
# binding never lowers the count of the calls an assistant made.
|
|
32
|
+
def carry!(from:, to:)
|
|
33
|
+
sql = <<~SQL.squish
|
|
34
|
+
INSERT INTO "#{schema}".kiosk_tolled_calls AS t (user_id, calls_since_proof)
|
|
35
|
+
SELECT $2, calls_since_proof FROM "#{schema}".kiosk_tolled_calls WHERE user_id = $1
|
|
36
|
+
ON CONFLICT (user_id) DO UPDATE SET calls_since_proof = t.calls_since_proof + excluded.calls_since_proof
|
|
37
|
+
SQL
|
|
38
|
+
connection.exec_query(sql, 'Kiosk tolled calls carry', [from, to])
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def connection = ::ActiveRecord::Base.lease_connection
|
|
43
|
+
def schema = Kiosk.configuration.schema
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -4,10 +4,10 @@ require 'json'
|
|
|
4
4
|
|
|
5
5
|
module Kiosk
|
|
6
6
|
module Server
|
|
7
|
-
# The gates of a call
|
|
7
|
+
# The gates of a call in the protocol, in order: identity (401), the arguments
|
|
8
8
|
# (400), the caller's timezone (400) and the toll (402). Then the call runs
|
|
9
9
|
# inside a session. {WireController} and every controller that includes
|
|
10
|
-
# {Kiosk::Handler} serve the
|
|
10
|
+
# {Kiosk::Handler} serve the protocol with them.
|
|
11
11
|
module VerbWire
|
|
12
12
|
include WireRendering
|
|
13
13
|
|
|
@@ -51,7 +51,7 @@ module Kiosk
|
|
|
51
51
|
raise Errors.malformed_json(hint: BODY_HINT)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# A {Result}. `command` is the policy verb ({Executor::VERBS}); `name` the
|
|
54
|
+
# A {Result}. `command` is the policy verb ({Executor::VERBS}); `name` the published name;
|
|
55
55
|
# `handler` answers in place of the registered one.
|
|
56
56
|
def call_wire(command:, args:, identity:, name:, handler: nil)
|
|
57
57
|
# Refused with the arguments, before the toll.
|
data/lib/kiosk/server/version.rb
CHANGED
|
@@ -33,6 +33,11 @@ module Kiosk
|
|
|
33
33
|
'browse, order, book, apply, pay -- and subscribe to the operator\'s event ' \
|
|
34
34
|
'stream for anything that you would otherwise wait for.'
|
|
35
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
|
+
|
|
36
41
|
ARD_API = 'The Kiosk discovery document of this origin: the endpoint, the sign-in and the catalogue. ' \
|
|
37
42
|
'Read it with the Kiosk skill.'
|
|
38
43
|
|
|
@@ -84,20 +89,18 @@ module Kiosk
|
|
|
84
89
|
doc = { issuer: Kiosk.current_issuer, registration_endpoint: "#{endpoint}/oauth/register",
|
|
85
90
|
token_endpoint: "#{endpoint}/oauth/token" }
|
|
86
91
|
doc[:device_authorization_endpoint] = "#{endpoint}/oauth/device_authorization" if config.serve_account_binding
|
|
87
|
-
# No authorization endpoint, so no response type.
|
|
88
|
-
#
|
|
89
|
-
doc.merge(jwks_uri: "#{endpoint}/.well-known/jwks.json", response_types_supported: [], scopes_supported: [],
|
|
92
|
+
# No authorization endpoint, so no response type.
|
|
93
|
+
doc.merge(jwks_uri: "#{endpoint}/.well-known/jwks.json", response_types_supported: [], scopes_supported: SCOPES,
|
|
90
94
|
grant_types_supported: Client.grant_types(config), **Shared.client_authentication,
|
|
91
95
|
service_documentation: "#{base}/auth.md", dpop_signing_alg_values_supported: AssistantKey.algorithms)
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
# `/.well-known/oauth-protected-resource` (RFC 9728). RFC 9728 §3.3 binds a
|
|
95
|
-
# document at this path to the origin, so `resource` is the origin.
|
|
96
|
-
# AI assistant requests no scope, so `scopes_supported` is empty.
|
|
99
|
+
# document at this path to the origin, so `resource` is the origin.
|
|
97
100
|
def self.oauth_protected_resource(base_url:, config: Kiosk.configuration)
|
|
98
101
|
Shared.validate_issuer!(config)
|
|
99
102
|
base = base_url.to_s.chomp('/')
|
|
100
|
-
{ resource: base, authorization_servers: [Kiosk.current_issuer], scopes_supported:
|
|
103
|
+
{ resource: base, authorization_servers: [Kiosk.current_issuer], scopes_supported: SCOPES,
|
|
101
104
|
bearer_methods_supported: ['header'],
|
|
102
105
|
dpop_signing_alg_values_supported: AssistantKey.algorithms, dpop_bound_access_tokens_required: true,
|
|
103
106
|
resource_documentation: "#{base}/auth.md" }
|
|
@@ -213,7 +216,7 @@ module Kiosk
|
|
|
213
216
|
return '' unless skill?(config)
|
|
214
217
|
|
|
215
218
|
sha = config.skill_sha256
|
|
216
|
-
line = "-
|
|
219
|
+
line = "- Skill for AI assistants: #{config.skill_url}"
|
|
217
220
|
line += " (sha256 `#{sha}`)" if sha && !sha.to_s.empty?
|
|
218
221
|
line
|
|
219
222
|
end
|
|
@@ -277,7 +280,7 @@ module Kiosk
|
|
|
277
280
|
# auth.md — #{site}
|
|
278
281
|
|
|
279
282
|
How AI assistants authenticate against this provider's Kiosk
|
|
280
|
-
endpoint (`#{endpoint}`).
|
|
283
|
+
endpoint (`#{endpoint}`). Protocol: the Kiosk specification
|
|
281
284
|
(https://kiosk.tech/specification.html). This file follows the
|
|
282
285
|
auth.md section order. Registration and tokens use OAuth 2.0
|
|
283
286
|
standards: RFC 7591, RFC 7523, RFC 9449 and RFC 8628.
|
|
@@ -338,7 +341,7 @@ module Kiosk
|
|
|
338
341
|
`Kiosk-PoW` header line for each proof. A proof is never in the
|
|
339
342
|
request body: the operator ignores a body `pow` field, and a
|
|
340
343
|
changed body makes the proofs incorrect. The same header answers a
|
|
341
|
-
`402 pow_required` on the
|
|
344
|
+
`402 pow_required` on the verbs too. Most of them are GETs,
|
|
342
345
|
which have no body. The ONE endpoint under `#{endpoint}` that has
|
|
343
346
|
no toll is `GET #{endpoint}/schema`: it is public, the operator
|
|
344
347
|
serves it from memory, and a toll needs an identity to charge.
|
|
@@ -349,7 +352,7 @@ module Kiosk
|
|
|
349
352
|
check failed. Get the reference solver from
|
|
350
353
|
`#{PowGate::POW_SOLVER_URL}`. The 403 `hint` names the same file.
|
|
351
354
|
That URL has no version and always serves the CURRENT solver.
|
|
352
|
-
Before you run it, compare its SHA-256 with the pin that the
|
|
355
|
+
Before you run it, compare its SHA-256 with the pin that the
|
|
353
356
|
skill above publishes.
|
|
354
357
|
|
|
355
358
|
## Claim ceremony
|
|
@@ -408,7 +411,7 @@ module Kiosk
|
|
|
408
411
|
|
|
409
412
|
## Use the access_token
|
|
410
413
|
|
|
411
|
-
Send `Authorization: DPoP <access_token>` to the
|
|
414
|
+
Send `Authorization: DPoP <access_token>` to the verbs
|
|
412
415
|
under `#{endpoint}` (every query, every action, and `pay`) with a
|
|
413
416
|
`DPoP` header: a proof (RFC 9449) that your registered key signs
|
|
414
417
|
for that method and URL, with `ath`, the hash of the token. A
|
|
@@ -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
|
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'
|
|
@@ -17,7 +17,11 @@ module Kiosk
|
|
|
17
17
|
module Server
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
#
|
|
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.
|
|
21
25
|
def self.current_user_id = Server::SessionContext.identity.user_id
|
|
22
26
|
def self.current_role = Server::SessionContext.identity.role
|
|
23
27
|
|