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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -0
  3. data/README.md +134 -102
  4. data/app/channels/kiosk_events.rb +30 -17
  5. data/app/controllers/kiosk/server/application_controller.rb +0 -1
  6. data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
  7. data/app/controllers/kiosk/server/auth_controller.rb +3 -3
  8. data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
  9. data/app/controllers/kiosk/server/discovery_controller.rb +2 -7
  10. data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
  11. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +8 -7
  12. data/app/controllers/kiosk/server/oauth_token_controller.rb +4 -1
  13. data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
  14. data/app/controllers/kiosk/server/wire_controller.rb +2 -2
  15. data/app/models/kiosk/checkout_mandate.rb +0 -2
  16. data/app/models/kiosk/settlement.rb +0 -2
  17. data/app/models/kiosk/visit.rb +0 -2
  18. data/app/views/kiosk/server/assistants/show.html.erb +20 -24
  19. data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
  20. data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
  21. data/app/views/layouts/kiosk/server/application.html.erb +14 -0
  22. data/lib/generators/kiosk/install/install_generator.rb +25 -64
  23. data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
  24. data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
  25. data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
  26. data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
  27. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -14
  28. data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
  29. data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
  30. data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
  31. data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
  32. data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
  33. data/lib/kiosk/payable.rb +1 -1
  34. data/lib/kiosk/server/account_binding.rb +14 -5
  35. data/lib/kiosk/server/account_holder_gate.rb +3 -1
  36. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +1 -1
  37. data/lib/kiosk/server/agent_rotation.rb +1 -1
  38. data/lib/kiosk/server/argument_decoder.rb +2 -2
  39. data/lib/kiosk/server/assistant_accounts.rb +38 -0
  40. data/lib/kiosk/server/auth_toll.rb +5 -5
  41. data/lib/kiosk/server/client.rb +1 -1
  42. data/lib/kiosk/server/client_assertion.rb +3 -2
  43. data/lib/kiosk/server/client_registration.rb +6 -2
  44. data/lib/kiosk/server/column_spending_cap.rb +1 -1
  45. data/lib/kiosk/server/configuration_extension.rb +15 -38
  46. data/lib/kiosk/server/current_request.rb +1 -1
  47. data/lib/kiosk/server/device_authorization.rb +1 -1
  48. data/lib/kiosk/server/device_authorization_stores.rb +1 -1
  49. data/lib/kiosk/server/device_code_grant.rb +2 -2
  50. data/lib/kiosk/server/dpop.rb +6 -1
  51. data/lib/kiosk/server/engine.rb +2 -2
  52. data/lib/kiosk/server/errors.rb +21 -7
  53. data/lib/kiosk/server/event_stores.rb +2 -2
  54. data/lib/kiosk/server/events.rb +4 -3
  55. data/lib/kiosk/server/events_connection.rb +23 -5
  56. data/lib/kiosk/server/events_replay.rb +79 -0
  57. data/lib/kiosk/server/executor/repeat.rb +1 -1
  58. data/lib/kiosk/server/executor/spending_cap.rb +8 -66
  59. data/lib/kiosk/server/executor.rb +10 -10
  60. data/lib/kiosk/server/failure_log.rb +3 -3
  61. data/lib/kiosk/server/handler_mixin.rb +9 -9
  62. data/lib/kiosk/server/handler_registrations.rb +1 -1
  63. data/lib/kiosk/server/handler_serving.rb +11 -11
  64. data/lib/kiosk/server/headers.rb +3 -3
  65. data/lib/kiosk/server/identity_resolution.rb +1 -1
  66. data/lib/kiosk/server/kyc.rb +5 -7
  67. data/lib/kiosk/server/kyc_verifier.rb +26 -27
  68. data/lib/kiosk/server/linked_assistants.rb +21 -16
  69. data/lib/kiosk/server/money.rb +41 -0
  70. data/lib/kiosk/server/oauth_answers.rb +1 -0
  71. data/lib/kiosk/server/oauth_error.rb +3 -0
  72. data/lib/kiosk/server/open_api.rb +9 -9
  73. data/lib/kiosk/server/page.rb +1 -1
  74. data/lib/kiosk/server/payment_claim.rb +3 -3
  75. data/lib/kiosk/server/pow_gate.rb +12 -33
  76. data/lib/kiosk/server/pow_spent_stores.rb +1 -1
  77. data/lib/kiosk/server/receipts.rb +4 -3
  78. data/lib/kiosk/server/reputation_factors.rb +12 -5
  79. data/lib/kiosk/server/request_validation.rb +2 -1
  80. data/lib/kiosk/server/result.rb +1 -1
  81. data/lib/kiosk/server/revocation_stores.rb +1 -1
  82. data/lib/kiosk/server/routing.rb +1 -1
  83. data/lib/kiosk/server/schemas/auth.schema.json +2 -2
  84. data/lib/kiosk/server/schemas/binding.schema.json +3 -3
  85. data/lib/kiosk/server/schemas/kyc.schema.json +22 -3
  86. data/lib/kiosk/server/schemas/mandates.schema.json +5 -5
  87. data/lib/kiosk/server/schemas/pow.schema.json +2 -2
  88. data/lib/kiosk/server/session_context.rb +2 -2
  89. data/lib/kiosk/server/tolled_calls.rb +46 -0
  90. data/lib/kiosk/server/verb_wire.rb +3 -3
  91. data/lib/kiosk/server/version.rb +1 -1
  92. data/lib/kiosk/server/well_known.rb +14 -11
  93. data/lib/kiosk/server/wire_rendering.rb +1 -1
  94. data/lib/kiosk/server.rb +6 -2
  95. metadata +9 -8
  96. data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
  97. data/lib/kiosk/server/schema_definitions.rb +0 -320
  98. data/lib/kiosk/server/settled_spend.rb +0 -21
  99. 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
- # It is paid once per access token: after one paid toll, every call made
10
- # with that token proceeds until the token expires. With no
11
- # `reputation_policy` it returns `:proceed` and touches nothing;
12
- # `kiosk-reputation` is required only when a policy is set.
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 wire name
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 -- the wire controller and the specs pass these six by name
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? || session_paid?(config, identity)
29
+ return :proceed if policy.nil?
30
30
 
31
- secret = pow_secret!(config)
32
- spec = toll_for(policy, identity, command.to_sym)
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 { paid(config, identity) }
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."
@@ -89,7 +89,7 @@ module Kiosk
89
89
  end
90
90
 
91
91
  def connection = ::ActiveRecord::Base.lease_connection
92
- def table = %("#{Kiosk.configuration.schema}".pow_spent)
92
+ def table = %("#{Kiosk.configuration.schema}".kiosk_pow_spent)
93
93
  end
94
94
  end
95
95
  end
@@ -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 }.compact)
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 window.
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.empty.with(settled_purchases_count: count, settled_purchases_cents: cents)
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".settlements s LEFT JOIN "%<schema>s".refunds r ON r.settlement_id = s.id
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".visits WHERE user_id = $1 AND visited_at >= $2
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 wire's own `missing field:` sentence.
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
 
@@ -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 wire, as the body.
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
 
@@ -58,7 +58,7 @@ module Kiosk
58
58
  private
59
59
 
60
60
  def connection = ::ActiveRecord::Base.lease_connection
61
- def table(name) = %("#{Kiosk.configuration.schema}".#{name})
61
+ def table(name) = %("#{Kiosk.configuration.schema}".kiosk_#{name})
62
62
  end
63
63
  end
64
64
  end
@@ -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 wire request; the name is looked up per request, so a
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 wire 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.",
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 wire 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.",
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 wire 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.",
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 wire 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.",
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 on this wire that is not an RFC 9457 problem document."
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 wire 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.",
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 configured kyc_audience (its origin, or a stable handle it declares); the operator rejects any other aud. This stops a claim minted for operator A being replayed to operator B." },
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 wire 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.",
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. On the wire 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.",
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). `network_confirmation_id` is present only when the payment provider reports it; AP2 requires it, so a receipt without it does not satisfy AP2's schema (protocol.md 11.3).",
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 on the wire 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.",
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 wire request runs as. {#open} wraps the request in 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 wire resolved. Raises outside a session, where a scope
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 on the wire, in order: identity (401), the arguments
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 wire with them.
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 wire name;
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kiosk
4
4
  module Server
5
- VERSION = '0.6.1'
5
+ VERSION = '0.6.28'
6
6
  end
7
7
  end
@@ -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. An AI assistant
88
- # requests no scope, so no scope value.
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. An
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 = "- Wire skill for AI assistants: #{config.skill_url}"
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}`). Wire contract: the Kiosk specification
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 wire verbs too. Most of them are GETs,
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 wire
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 wire verbs
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 wire answer is written: headers, cache policy, body. Success is the
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 wire, auth and discovery controllers, and
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
- # The user id and role of the principal the wire resolved.
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