kiosk-server 0.5.20

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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +549 -0
  3. data/LICENSE.txt +202 -0
  4. data/README.md +635 -0
  5. data/app/channels/kiosk_events.rb +138 -0
  6. data/app/controllers/kiosk/server/application_controller.rb +17 -0
  7. data/app/controllers/kiosk/server/assistants_controller.rb +96 -0
  8. data/app/controllers/kiosk/server/auth_controller.rb +187 -0
  9. data/app/controllers/kiosk/server/device_verify_controller.rb +101 -0
  10. data/app/controllers/kiosk/server/discovery_controller.rb +66 -0
  11. data/app/controllers/kiosk/server/jwks_controller.rb +15 -0
  12. data/app/controllers/kiosk/server/kyc_attestation_controller.rb +69 -0
  13. data/app/controllers/kiosk/server/kyc_callback_controller.rb +15 -0
  14. data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +73 -0
  15. data/app/controllers/kiosk/server/oauth_token_controller.rb +59 -0
  16. data/app/controllers/kiosk/server/open_api_controller.rb +20 -0
  17. data/app/controllers/kiosk/server/payment_setup_controller.rb +31 -0
  18. data/app/controllers/kiosk/server/verb_controller.rb +61 -0
  19. data/app/controllers/kiosk/server/wire_controller.rb +43 -0
  20. data/app/models/kiosk/cart_mandate.rb +21 -0
  21. data/app/models/kiosk/server/application_record.rb +10 -0
  22. data/app/models/kiosk/settlement.rb +12 -0
  23. data/app/models/kiosk/visit.rb +13 -0
  24. data/app/views/kiosk/server/assistants/show.html.erb +94 -0
  25. data/app/views/kiosk/server/device_verify/decided.html.erb +14 -0
  26. data/app/views/kiosk/server/device_verify/show.html.erb +53 -0
  27. data/lib/generators/kiosk/install/install_generator.rb +125 -0
  28. data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +16 -0
  29. data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +17 -0
  30. data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +22 -0
  31. data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +16 -0
  32. data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +20 -0
  33. data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +21 -0
  34. data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +16 -0
  35. data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +16 -0
  36. data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +16 -0
  37. data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +17 -0
  38. data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +16 -0
  39. data/lib/generators/kiosk/install/templates/initializer.rb.tt +53 -0
  40. data/lib/kiosk/handler.rb +43 -0
  41. data/lib/kiosk/owned.rb +17 -0
  42. data/lib/kiosk/payable.rb +90 -0
  43. data/lib/kiosk/server/account_binding.rb +188 -0
  44. data/lib/kiosk/server/account_holder_gate.rb +58 -0
  45. data/lib/kiosk/server/action_event.rb +60 -0
  46. data/lib/kiosk/server/actions.rb +14 -0
  47. data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +89 -0
  48. data/lib/kiosk/server/agent_login.rb +49 -0
  49. data/lib/kiosk/server/agent_registration.rb +88 -0
  50. data/lib/kiosk/server/argument_decoder.rb +260 -0
  51. data/lib/kiosk/server/audit_sink.rb +42 -0
  52. data/lib/kiosk/server/auth_challenge.rb +33 -0
  53. data/lib/kiosk/server/auth_challenge_stores.rb +74 -0
  54. data/lib/kiosk/server/auth_toll.rb +44 -0
  55. data/lib/kiosk/server/binding_module_gate.rb +25 -0
  56. data/lib/kiosk/server/caller_timezone.rb +50 -0
  57. data/lib/kiosk/server/column_spending_cap.rb +26 -0
  58. data/lib/kiosk/server/configuration_extension.rb +328 -0
  59. data/lib/kiosk/server/controller_verb.rb +63 -0
  60. data/lib/kiosk/server/current_request.rb +31 -0
  61. data/lib/kiosk/server/cursor.rb +33 -0
  62. data/lib/kiosk/server/device_authorization.rb +136 -0
  63. data/lib/kiosk/server/device_authorization_stores.rb +184 -0
  64. data/lib/kiosk/server/device_code_grant.rb +145 -0
  65. data/lib/kiosk/server/device_verification.rb +46 -0
  66. data/lib/kiosk/server/engine.rb +203 -0
  67. data/lib/kiosk/server/errors.rb +339 -0
  68. data/lib/kiosk/server/event_stores.rb +124 -0
  69. data/lib/kiosk/server/events.rb +86 -0
  70. data/lib/kiosk/server/events_cable.rb +102 -0
  71. data/lib/kiosk/server/events_connection.rb +170 -0
  72. data/lib/kiosk/server/executor/repeat.rb +42 -0
  73. data/lib/kiosk/server/executor/spending_cap.rb +102 -0
  74. data/lib/kiosk/server/executor.rb +397 -0
  75. data/lib/kiosk/server/failure_log.rb +28 -0
  76. data/lib/kiosk/server/handler_mixin.rb +418 -0
  77. data/lib/kiosk/server/handler_registrations.rb +57 -0
  78. data/lib/kiosk/server/handler_serving.rb +197 -0
  79. data/lib/kiosk/server/headers.rb +82 -0
  80. data/lib/kiosk/server/headers_middleware.rb +31 -0
  81. data/lib/kiosk/server/identity_resolution.rb +20 -0
  82. data/lib/kiosk/server/issuer_middleware.rb +22 -0
  83. data/lib/kiosk/server/jwks.rb +14 -0
  84. data/lib/kiosk/server/jwt_issuer.rb +98 -0
  85. data/lib/kiosk/server/kyc.rb +261 -0
  86. data/lib/kiosk/server/kyc_verifier.rb +106 -0
  87. data/lib/kiosk/server/link_code.rb +90 -0
  88. data/lib/kiosk/server/linked_assistants.rb +116 -0
  89. data/lib/kiosk/server/mandate_verifier.rb +221 -0
  90. data/lib/kiosk/server/open_api.rb +543 -0
  91. data/lib/kiosk/server/page.rb +19 -0
  92. data/lib/kiosk/server/payment_claim.rb +293 -0
  93. data/lib/kiosk/server/payment_setup.rb +102 -0
  94. data/lib/kiosk/server/pop_verifier.rb +87 -0
  95. data/lib/kiosk/server/pow_gate.rb +121 -0
  96. data/lib/kiosk/server/pow_spent_stores.rb +96 -0
  97. data/lib/kiosk/server/pow_toll.rb +136 -0
  98. data/lib/kiosk/server/queries.rb +14 -0
  99. data/lib/kiosk/server/reputation_factors.rb +38 -0
  100. data/lib/kiosk/server/request_validation.rb +171 -0
  101. data/lib/kiosk/server/response_validation.rb +45 -0
  102. data/lib/kiosk/server/result.rb +25 -0
  103. data/lib/kiosk/server/revocation_stores.rb +57 -0
  104. data/lib/kiosk/server/routing.rb +76 -0
  105. data/lib/kiosk/server/schema_definitions.rb +351 -0
  106. data/lib/kiosk/server/schema_document.rb +116 -0
  107. data/lib/kiosk/server/schema_slots.rb +119 -0
  108. data/lib/kiosk/server/schemas/auth.schema.json +135 -0
  109. data/lib/kiosk/server/schemas/binding.schema.json +145 -0
  110. data/lib/kiosk/server/schemas/kyc.schema.json +114 -0
  111. data/lib/kiosk/server/schemas/mandates.schema.json +75 -0
  112. data/lib/kiosk/server/schemas/pow.schema.json +94 -0
  113. data/lib/kiosk/server/session_context.rb +88 -0
  114. data/lib/kiosk/server/settled_spend.rb +21 -0
  115. data/lib/kiosk/server/signing_key.rb +82 -0
  116. data/lib/kiosk/server/verb_registry.rb +77 -0
  117. data/lib/kiosk/server/verb_vocabulary.rb +163 -0
  118. data/lib/kiosk/server/verb_wire.rb +85 -0
  119. data/lib/kiosk/server/version.rb +7 -0
  120. data/lib/kiosk/server/well_known.rb +416 -0
  121. data/lib/kiosk/server/wire_rendering.rb +116 -0
  122. data/lib/kiosk/server/wrong_door.rb +42 -0
  123. data/lib/kiosk/server.rb +42 -0
  124. data/lib/kiosk/skill_link.rb +16 -0
  125. data/listen.py +278 -0
  126. data/requirements.txt +1 -0
  127. metadata +349 -0
@@ -0,0 +1,135 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
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.",
5
+ "title": "Kiosk registration and login (kiosk-pop)",
6
+ "description": "The wire objects of Section 5: kiosk-pop, a proof-of-possession challenge-response. The caller proves that it holds the private key of the public key it presents, and the operator answers with a short-lived access token. The roots are open: they state what MUST be present and the shape of each member.",
7
+ "$defs": {
8
+ "challenge": {
9
+ "type": "object",
10
+ "description": "Section 5.1 -- the answer to GET <endpoint>/auth/challenge?public_key=<url-encoded PEM>. Single-use and short-lived; the nonce is worthless to anyone without the matching private key, which is why the endpoint is unauthenticated.",
11
+ "required": ["challenge", "exp"],
12
+ "properties": {
13
+ "challenge": {
14
+ "type": "string",
15
+ "description": "The server-issued nonce to sign. It becomes the `nonce` claim of the possession proof.",
16
+ "minLength": 1
17
+ },
18
+ "exp": {
19
+ "type": "integer",
20
+ "description": "Unix expiry of the challenge."
21
+ }
22
+ }
23
+ },
24
+ "possessionProof": {
25
+ "type": "object",
26
+ "description": "Section 5.2 -- the decoded payload of `signed`, a compact RS256 JWS. This is the object the register, login, claim and device-grant token calls all carry. It is a PAYLOAD, not a request body: the wire carries the compact serialisation, and this schema describes what a verifier reads out of it.",
27
+ "required": ["aud", "nonce", "jti"],
28
+ "properties": {
29
+ "aud": {
30
+ "type": "string",
31
+ "description": "ORIGIN-BINDING, and the reason a relayed proof is worthless. MUST be the origin the AI assistant dialed (Section 15.1); the operator rejects any other `aud`."
32
+ },
33
+ "nonce": {
34
+ "type": "string",
35
+ "description": "The `challenge` from Section 5.1. Single-use and server-TTL-bounded."
36
+ },
37
+ "jti": {
38
+ "type": "string",
39
+ "description": "A unique id."
40
+ },
41
+ "pub": {
42
+ "type": "string",
43
+ "description": "RFC 7638 thumbprint of the public key; verified only when present."
44
+ },
45
+ "iat": {
46
+ "type": "integer",
47
+ "description": "Informational only -- the server-issued `nonce` is the authoritative freshness bound, so an operator MUST NOT rely on this claim for freshness."
48
+ }
49
+ }
50
+ },
51
+ "credentialRequest": {
52
+ "type": "object",
53
+ "description": "Section 5.3 -- the body of POST /auth/register and POST /auth/login. Register also accepts an optional proof-of-work proof, which rides in the `Kiosk-PoW` request HEADER (Sections 5.5 and 10.1) and never in this body, so that the signed bytes are identical on the retry.",
54
+ "required": ["public_key", "signed"],
55
+ "properties": {
56
+ "public_key": {
57
+ "type": "string",
58
+ "description": "The AI assistant's public key, PEM-encoded. An AI assistant SHOULD generate a fresh keypair per operator origin (Section 15.3)."
59
+ },
60
+ "signed": {
61
+ "type": "string",
62
+ "description": "The possession proof of Section 5.2, as a compact RS256 JWS. Its decoded payload is `#/$defs/possessionProof`."
63
+ }
64
+ }
65
+ },
66
+ "registration": {
67
+ "type": "object",
68
+ "description": "Section 5.3 -- the 201 answer to POST /auth/register. Registering an already-known key answers 409 conflict instead (use login). This is also the 201 answer to POST /auth/claim (Section 6.2), which is why `#/$defs/claimResponse` in binding.schema.json refers here rather than restating it.",
69
+ "required": ["agent_id", "user_id", "access_token"],
70
+ "properties": {
71
+ "agent_id": {
72
+ "type": "string",
73
+ "description": "The acting agent id. Stable across a rebind (Section 6.3)."
74
+ },
75
+ "user_id": {
76
+ "type": "string",
77
+ "description": "The identity the key maps to. An operator MUST map a known key to the same `user_id` so a saved payment card survives across sessions."
78
+ },
79
+ "access_token": {
80
+ "type": "string",
81
+ "description": "The access token, a compact RS256 JWT. Its claims are `#/$defs/accessTokenClaims`."
82
+ }
83
+ }
84
+ },
85
+ "token": {
86
+ "type": "object",
87
+ "description": "Section 5.3: the 200 answer to POST /auth/login; an unknown key answers 404 not_found. It is ALSO the 200 answer to POST /auth/revoke (Section 5.5), with a token issued after the new watermark.",
88
+ "required": ["access_token"],
89
+ "properties": {
90
+ "access_token": {
91
+ "type": "string",
92
+ "description": "The access token, a compact RS256 JWT. Its claims are `#/$defs/accessTokenClaims`."
93
+ }
94
+ }
95
+ },
96
+ "accessTokenClaims": {
97
+ "type": "object",
98
+ "description": "Section 5.4 -- the decoded claim set of `access_token`. A 3-part RS256 JWT signed by the operator and verifiable statelessly against the JWKS of Section 4.4, presented as `Authorization: Bearer`.",
99
+ "required": ["sub", "agent_id", "actor", "iss", "aud", "iat", "nbf", "exp", "jti"],
100
+ "properties": {
101
+ "sub": {
102
+ "type": "string",
103
+ "description": "The identity's `user_id`."
104
+ },
105
+ "agent_id": {
106
+ "type": "string",
107
+ "description": "The acting agent id."
108
+ },
109
+ "actor": {
110
+ "const": "agent",
111
+ "description": "Always the string \"agent\". A token minted for anything else is not a Kiosk access token."
112
+ },
113
+ "role": {
114
+ "type": "string",
115
+ "description": "The role the operator assigns (Section 5.4). It is omitted when absent, never null. No endpoint accepts a role that the client requests."
116
+ },
117
+ "iss": {
118
+ "type": "string",
119
+ "description": "The operator issuer."
120
+ },
121
+ "aud": {
122
+ "description": "The operator issuer. A string, or the array spelling RFC 7519 permits.",
123
+ "oneOf": [
124
+ { "type": "string" },
125
+ { "type": "array", "items": { "type": "string" }, "minItems": 1 }
126
+ ]
127
+ },
128
+ "iat": { "type": "integer", "description": "Issued-at. Also what the revoked-before watermark of Section 15.4 is compared against." },
129
+ "nbf": { "type": "integer", "description": "Not-before." },
130
+ "exp": { "type": "integer", "description": "Expiry. Default lifetime is 1 hour; the durable credential is the private key, not the token." },
131
+ "jti": { "type": "string", "description": "Unique token id." }
132
+ }
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,145 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
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.",
5
+ "title": "Kiosk account binding -- claim and link",
6
+ "description": "The wire objects of Section 6, the one-time ceremonies that bind an AI assistant to an existing operator account: claim, an RFC 8628 device grant, and link. Both need the human's approval and a valid possession proof. The two /oauth/* requests are form-encoded and have no schema here; their error answer has one.",
7
+ "$defs": {
8
+ "deviceAuthorization": {
9
+ "type": "object",
10
+ "description": "Section 6.1 step 1: the answer to POST <endpoint>/oauth/device_authorization. All six members are required, also the two that RFC 8628 makes optional.",
11
+ "required": [
12
+ "device_code",
13
+ "user_code",
14
+ "verification_uri",
15
+ "verification_uri_complete",
16
+ "expires_in",
17
+ "interval"
18
+ ],
19
+ "properties": {
20
+ "device_code": {
21
+ "type": "string",
22
+ "description": "The opaque code the AI assistant polls with. Never shown to the human."
23
+ },
24
+ "user_code": {
25
+ "type": "string",
26
+ "description": "The short code the human types at the verification page."
27
+ },
28
+ "verification_uri": {
29
+ "type": "string",
30
+ "format": "uri",
31
+ "description": "The operator's session-authenticated approval page (Section 15.8), which names the access the approval hands over."
32
+ },
33
+ "verification_uri_complete": {
34
+ "type": "string",
35
+ "format": "uri",
36
+ "description": "The same page with the `user_code` already carried, so the human does not have to type it."
37
+ },
38
+ "expires_in": {
39
+ "type": "integer",
40
+ "description": "Seconds until the authorization expires."
41
+ },
42
+ "interval": {
43
+ "type": "integer",
44
+ "description": "Seconds the AI assistant waits between polls. A poll that arrives sooner may be answered `slow_down`."
45
+ }
46
+ }
47
+ },
48
+ "deviceTokenResponse": {
49
+ "type": "object",
50
+ "description": "Section 6.1 step 3: the success answer to POST <endpoint>/oauth/token. The bound principal (`user_id`, `agent_id`) is in the access token's claims.",
51
+ "required": ["access_token", "token_type", "expires_in"],
52
+ "properties": {
53
+ "access_token": {
54
+ "type": "string",
55
+ "description": "The access token. Its claims are auth.schema.json#/$defs/accessTokenClaims."
56
+ },
57
+ "token_type": {
58
+ "const": "Bearer",
59
+ "description": "Always \"Bearer\"."
60
+ },
61
+ "expires_in": {
62
+ "type": "integer",
63
+ "description": "Seconds until the access token expires."
64
+ },
65
+ "scope": {
66
+ "type": "string",
67
+ "description": "The approving human's role, as the scope that RFC 6749 Section 5.1 grants. It is present only when the binding carries a role, and never null. An AI assistant MUST NOT treat its absence as an error."
68
+ }
69
+ }
70
+ },
71
+ "oauthError": {
72
+ "type": "object",
73
+ "description": "Section 6.1: the error answer of both /oauth/* endpoints, the one error on this wire that is not an RFC 9457 problem document. The status is 400 for every code except `invalid_client`, which is 401.",
74
+ "required": ["error"],
75
+ "properties": {
76
+ "error": {
77
+ "enum": [
78
+ "authorization_pending",
79
+ "slow_down",
80
+ "expired_token",
81
+ "access_denied",
82
+ "invalid_grant",
83
+ "invalid_client",
84
+ "invalid_request",
85
+ "unsupported_grant_type"
86
+ ],
87
+ "description": "The OAuth error code. A closed vocabulary: an operator MUST NOT emit anything outside it, or an AI assistant polling this ceremony has a branch it cannot take."
88
+ },
89
+ "error_description": {
90
+ "type": "string",
91
+ "description": "Human-readable detail. An AI assistant branches on `error` and never on this string."
92
+ }
93
+ }
94
+ },
95
+ "linkCode": {
96
+ "type": "object",
97
+ "description": "Section 6.2 -- the 201 answer to POST <endpoint>/auth/link, called on the operator's own session by the signed-in human. 201 rather than 200 because the call MINTS a redeemable single-use credential.",
98
+ "required": ["link_code", "expires_in"],
99
+ "properties": {
100
+ "link_code": {
101
+ "type": "string",
102
+ "description": "A long opaque token, paste-grade -- the human hands it to their AI assistant. Stored hashed, single-use, short-TTL and attempt-capped (Section 6.3)."
103
+ },
104
+ "expires_in": {
105
+ "type": "integer",
106
+ "description": "Seconds until the link code expires."
107
+ }
108
+ }
109
+ },
110
+ "claimRequest": {
111
+ "type": "object",
112
+ "description": "Section 6.2 -- the body of POST <endpoint>/auth/claim, which redeems a link code. The possession proof is REQUIRED, so a leaked code alone can never bind a key its holder does not control.",
113
+ "required": ["code", "public_key", "signed"],
114
+ "properties": {
115
+ "code": {
116
+ "type": "string",
117
+ "description": "The `link_code` the human handed over."
118
+ },
119
+ "public_key": {
120
+ "type": "string",
121
+ "description": "The AI assistant's public key, PEM-encoded."
122
+ },
123
+ "signed": {
124
+ "type": "string",
125
+ "description": "The possession proof of Section 5.2, as a compact RS256 JWS."
126
+ }
127
+ }
128
+ },
129
+ "claimResponse": {
130
+ "$ref": "https://kiosk.tech/spec/schemas/auth.schema.json#/$defs/registration",
131
+ "description": "Section 6.2: the 201 answer to POST <endpoint>/auth/claim, in the shape of the register answer. A new key, a rebound key and a repeated bind give answers that cannot be told apart (Section 6.3)."
132
+ },
133
+ "unlinkRequest": {
134
+ "type": "object",
135
+ "description": "Section 6.3 -- the body of POST <endpoint>/auth/unlink, called on the operator's own session. The response is 204 No Content and carries no body at all, which is why no response object appears here.",
136
+ "required": ["agent_id"],
137
+ "properties": {
138
+ "agent_id": {
139
+ "type": "string",
140
+ "description": "The bound AI assistant to deactivate. Its tokens stop verifying and /auth/login answers 404 (Section 15.4)."
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
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.",
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.",
7
+ "$defs": {
8
+ "attestation": {
9
+ "type": "object",
10
+ "description": "Signed by the KYC provider, verified with its configured public key.",
11
+ "required": [
12
+ "sub",
13
+ "level",
14
+ "iss",
15
+ "aud",
16
+ "exp"
17
+ ],
18
+ "properties": {
19
+ "sub": {
20
+ "type": "string",
21
+ "description": "MUST equal the authenticated user_id."
22
+ },
23
+ "level": {
24
+ "const": "verified",
25
+ "description": "Binary; anything other than \"verified\" is rejected."
26
+ },
27
+ "iss": {
28
+ "type": "string",
29
+ "description": "MUST equal the provider-configured KYC issuer."
30
+ },
31
+ "aud": {
32
+ "type": "string",
33
+ "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."
34
+ },
35
+ "iat": {
36
+ "type": "integer",
37
+ "description": "Issued-at timestamp. Not enforced by the verifier."
38
+ },
39
+ "exp": {
40
+ "type": "integer",
41
+ "description": "One year after iat. An expired attestation is rejected."
42
+ },
43
+ "attributes": {
44
+ "$ref": "#/$defs/attributes"
45
+ }
46
+ }
47
+ },
48
+ "attributes": {
49
+ "type": "object",
50
+ "description": "Optional named boolean attributes that the KYC provider vouches for, e.g. age_over_18. Only a value that is literally true counts; an action MAY require named attributes and refuse with kyc_required (403).",
51
+ "additionalProperties": {
52
+ "type": "boolean"
53
+ }
54
+ },
55
+ "request": {
56
+ "type": "object",
57
+ "required": [
58
+ "kyc_jws"
59
+ ],
60
+ "properties": {
61
+ "kyc_jws": {
62
+ "type": "string",
63
+ "description": "The attestation as a compact RS256 JWS."
64
+ }
65
+ }
66
+ },
67
+ "verification": {
68
+ "type": "object",
69
+ "description": "The answer to the request_kyc action: a verification opened at the KYC provider, whose page the AI assistant relays to its human.",
70
+ "additionalProperties": false,
71
+ "required": [
72
+ "request_id",
73
+ "verification_url",
74
+ "status"
75
+ ],
76
+ "properties": {
77
+ "request_id": {
78
+ "type": "string",
79
+ "description": "The provider's id for this verification; the kyc_verification event carries it back."
80
+ },
81
+ "verification_url": {
82
+ "type": "string",
83
+ "description": "The provider page the human completes."
84
+ },
85
+ "status": {
86
+ "const": "pending"
87
+ }
88
+ }
89
+ },
90
+ "verificationEvent": {
91
+ "type": "object",
92
+ "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.",
93
+ "additionalProperties": false,
94
+ "required": [
95
+ "request_id",
96
+ "status",
97
+ "kyc_jws"
98
+ ],
99
+ "properties": {
100
+ "request_id": {
101
+ "type": "string",
102
+ "description": "The request_id request_kyc returned."
103
+ },
104
+ "status": {
105
+ "const": "approved"
106
+ },
107
+ "kyc_jws": {
108
+ "type": "string",
109
+ "description": "The attestation as a compact RS256 JWS."
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
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.",
5
+ "title": "Kiosk AP2 payment mandates",
6
+ "description": "The decoded JWS payloads of the three AP2 mandates and the POST /kiosk/pay request body. On the wire each mandate is an RS256 compact JWS string; these schemas describe the decoded claim sets.",
7
+ "$defs": {
8
+ "baseClaims": {
9
+ "type": "object",
10
+ "description": "Claims every mandate MUST carry.",
11
+ "required": ["id", "user_id", "agent_id", "iss", "iat", "exp"],
12
+ "properties": {
13
+ "id": { "type": "string", "description": "Unique mandate id." },
14
+ "user_id": { "type": "string", "description": "MUST match the authenticated identity (string-compared)." },
15
+ "agent_id": { "type": "string", "description": "MUST match the authenticated identity (string-compared)." },
16
+ "iss": { "type": "string", "description": "MUST equal the provider issuer from /.well-known/kiosk.json, verbatim." },
17
+ "iat": { "type": "integer" },
18
+ "exp": { "type": "integer", "description": "An exp already in the past is rejected." }
19
+ }
20
+ },
21
+ "intent": {
22
+ "allOf": [{ "$ref": "#/$defs/baseClaims" }],
23
+ "type": "object",
24
+ "required": ["cap_amount_cents", "currency"],
25
+ "properties": {
26
+ "cap_amount_cents": { "type": "integer", "minimum": 1, "description": "Spending cap; there is no \"no cap\" spelling -- a nil cap is rejected. MUST be positive; a non-positive amount is rejected." },
27
+ "currency": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z]{3}$", "description": "An ISO 4217 ALPHA-3 code, and the `pattern` is the domain: three ASCII letters, either case. An operator MAY refuse more (Section 11.1)."},
28
+ "scope": { "type": "string", "description": "Provider-defined scope." }
29
+ }
30
+ },
31
+ "cart": {
32
+ "allOf": [{ "$ref": "#/$defs/baseClaims" }],
33
+ "type": "object",
34
+ "required": ["intent_mandate_id", "total_amount_cents", "currency", "line_items"],
35
+ "properties": {
36
+ "intent_mandate_id": { "type": "string", "description": "MUST equal the intent mandate id; cart total MUST NOT exceed the intent cap." },
37
+ "total_amount_cents": { "type": "integer", "minimum": 1, "description": "MUST be positive; a non-positive amount is rejected." },
38
+ "currency": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z]{3}$", "description": "MUST equal the intent currency, and MUST be an ISO 4217 alpha-3 code -- see the intent's note; equality to another empty string, or to another currency NAME, is not a check." },
39
+ "line_items": { "type": "array", "minItems": 1, "description": "Provider-defined line items, at least one. The operator matches a settlement to its own record through them." }
40
+ }
41
+ },
42
+ "payment": {
43
+ "allOf": [{ "$ref": "#/$defs/baseClaims" }],
44
+ "type": "object",
45
+ "required": ["cart_mandate_id", "amount_cents", "currency"],
46
+ "properties": {
47
+ "cart_mandate_id": { "type": "string", "description": "MUST equal the cart mandate id." },
48
+ "amount_cents": { "type": "integer", "minimum": 1, "description": "MUST equal the cart total, same currency. MUST be positive; a non-positive amount is rejected." },
49
+ "currency": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z]{3}$", "description": "MUST equal the cart currency, and MUST be an ISO 4217 alpha-3 code." },
50
+ "payment_method": { "type": "string", "description": "PSP method identifier (e.g. \"on_file\")." }
51
+ }
52
+ },
53
+ "payRequest": {
54
+ "type": "object",
55
+ "description": "The POST /kiosk/pay request body: the three mandates as compact JWS strings.",
56
+ "required": ["intent_mandate_jws", "cart_mandate_jws", "payment_mandate_jws"],
57
+ "properties": {
58
+ "intent_mandate_jws": { "type": "string" },
59
+ "cart_mandate_jws": { "type": "string" },
60
+ "payment_mandate_jws": { "type": "string" }
61
+ }
62
+ },
63
+ "settlement": {
64
+ "type": "object",
65
+ "description": "The pay response body, answered verbatim: there is no envelope, no ok flag and no kind discriminator (protocol.md 8.2, 11.3).",
66
+ "required": ["settlement_id", "psp_reference", "settled_amount_cents", "currency"],
67
+ "properties": {
68
+ "settlement_id": { "type": "string" },
69
+ "psp_reference": { "type": "string" },
70
+ "settled_amount_cents": { "type": "integer", "minimum": 1, "description": "The captured amount; equals the cart total. MUST be positive: a settlement exists only for a completed capture, and every mandate amount is already positive." },
71
+ "currency": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z]{3}$", "description": "The currency of the capture: the cart's currency in the OPERATOR's canonical spelling, which MAY differ byte for byte from the spelling the assistant signed (protocol.md 11.3, 11.5). Compare it as an ISO 4217 code, never as bytes." }
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kiosk.tech/spec/schemas/pow.schema.json",
4
+ "title": "Kiosk proof-of-work",
5
+ "$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.",
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
+ "$ref": "#/$defs/powHeader",
8
+ "$defs": {
9
+ "challenge": {
10
+ "type": "object",
11
+ "description": "A single stateless, request-bound PoW challenge.",
12
+ "required": ["id", "alg", "params", "salt", "exp", "sig"],
13
+ "properties": {
14
+ "id": { "type": "string", "description": "Opaque unique challenge id." },
15
+ "alg": { "type": "string", "description": "Algorithm name, e.g. \"equihash\"." },
16
+ "params": {
17
+ "type": "object",
18
+ "description": "Algorithm-specific parameters. For equihash: {n, k}.",
19
+ "properties": {
20
+ "n": { "type": "integer" },
21
+ "k": { "type": "integer" }
22
+ }
23
+ },
24
+ "salt": { "type": "string", "description": "Base64 random salt; no amortization across challenges." },
25
+ "exp": { "type": "integer", "description": "Unix expiry; scales with the number of requested proofs." },
26
+ "sig": { "type": "string", "description": "HMAC-SHA256 over the challenge fields plus a fingerprint of the exact request." }
27
+ }
28
+ },
29
+ "proof": {
30
+ "type": "object",
31
+ "description": "One solved challenge: the challenge echoed back verbatim plus its solution.",
32
+ "required": ["challenge", "nonce"],
33
+ "properties": {
34
+ "challenge": { "$ref": "#/$defs/challenge" },
35
+ "nonce": {
36
+ "$comment": "NO `type` HERE, AND THAT IS THE POINT. Typing `nonce` as the Equihash solution object outright would contradict the decision `alg` is open for: the challenge carries {alg, params} so that an operator may bring a backend of their own, and a backend brings its own solution encoding with it. A schema that admits one algorithm's `alg` string and only that algorithm's nonce shape refuses every other backend's valid proof before any verifier sees it. So the shape is stated CONDITIONALLY, in the `allOf` beside this property, keyed on `challenge.alg`.",
37
+ "description": "The solution to the challenge, in the shape of the algorithm that `challenge.alg` names. A verifier refuses a proof whose `alg` it does not know. For `equihash` the shape is #/$defs/equihashNonce."
38
+ }
39
+ },
40
+ "allOf": [
41
+ {
42
+ "$comment": "ALGORITHM-RELATIVE NONCE. `if` matches only when the echoed challenge names equihash; `then` applies the Equihash solution shape. Any other `alg` reaches no `then` and so leaves `nonce` unconstrained here -- which is the honest statement of what this specification knows about a backend it does not define.",
43
+ "if": {
44
+ "required": ["challenge"],
45
+ "properties": {
46
+ "challenge": {
47
+ "required": ["alg"],
48
+ "properties": { "alg": { "const": "equihash" } }
49
+ }
50
+ }
51
+ },
52
+ "then": {
53
+ "properties": { "nonce": { "$ref": "#/$defs/equihashNonce" } }
54
+ }
55
+ }
56
+ ]
57
+ },
58
+ "equihashNonce": {
59
+ "type": "object",
60
+ "description": "The Equihash solution -- the shape a proof's `nonce` takes when, and only when, its challenge's `alg` is \"equihash\".",
61
+ "required": ["indices"],
62
+ "properties": {
63
+ "indices": {
64
+ "type": "array",
65
+ "description": "u64 indices in Zcash canonical (subtree/tree) order; NOT a global ascending sort.",
66
+ "items": {
67
+ "$comment": "The upper bound is stated as an INCLUSIVE `maximum` of 2**64-1, not as `exclusiveMaximum: 2**64`. A JSON parser that reads numbers as IEEE-754 doubles rounds the largest LEGAL index, 18446744073709551615, up to exactly 2**64 -- an exclusive bound at 2**64 then REFUSES a proof the reference verifier accepts, and a bound that is wrong in the direction of rejecting valid work is the worse failure. An inclusive bound at the largest legal value survives that round-trip. The residual cost, which no JSON Schema can remove, is that such a parser also admits the handful of values between 2**64 and the next representable double; a parser with exact integers admits none of them, and the verifier refuses them all either way.",
68
+ "type": "integer",
69
+ "minimum": 0,
70
+ "maximum": 18446744073709551615
71
+ }
72
+ },
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.",
75
+ "type": "integer",
76
+ "minimum": 0,
77
+ "maximum": 4294967295,
78
+ "description": "A u32 defaulting to 0. Folded into the PoW seed after the salt bytes as a little-endian u32; an extensibility point (currently always 0). A verifier reconstructs the seed as salt-bytes || header_nonce(LE u32), so a proof solved for a non-zero header_nonce MUST carry it."
79
+ }
80
+ }
81
+ },
82
+ "powHeader": {
83
+ "description": "The value of the Kiosk-PoW request header as raw JSON: one proof object, or an array of proofs. Repeated header lines and a comma-joined value give the same list and are not shown here.",
84
+ "oneOf": [
85
+ { "$ref": "#/$defs/proof" },
86
+ {
87
+ "type": "array",
88
+ "minItems": 1,
89
+ "items": { "$ref": "#/$defs/proof" }
90
+ }
91
+ ]
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiosk
4
+ module Server
5
+ # The identity a wire request runs as. {#open} wraps the request in a
6
+ # transaction and sets the principal's GUCs transaction-locally, for
7
+ # operators who opt into RLS.
8
+ class SessionContext
9
+ # `SET LOCAL` with bound values; the third argument `true` is LOCAL.
10
+ SET_GUC_SQL = 'SELECT set_config($1, $2, true)'
11
+
12
+ KEY = :kiosk_server_session_context
13
+
14
+ def self.open(connection:, identity:, &)
15
+ new(connection: connection, identity: identity).open(&)
16
+ end
17
+
18
+ def self.current = Thread.current[KEY]
19
+
20
+ def self.open? = !Thread.current[KEY].nil?
21
+
22
+ # The identity the wire resolved. Raises outside a session, where a scope
23
+ # built on it would otherwise match nothing.
24
+ def self.identity
25
+ return current.identity if open?
26
+
27
+ raise Errors::Unauthenticated,
28
+ 'no Kiosk session is open. Wrap the call in ' \
29
+ 'Kiosk::Server::SessionContext.open(connection:, identity:), or pass the principal in.'
30
+ end
31
+
32
+ attr_reader :connection, :identity
33
+
34
+ def initialize(connection:, identity:)
35
+ @connection = connection
36
+ @identity = identity
37
+ end
38
+
39
+ def open
40
+ connection.transaction do
41
+ apply_gucs
42
+ previous = Thread.current[KEY]
43
+ Thread.current[KEY] = self
44
+ begin
45
+ yield self
46
+ ensure
47
+ Thread.current[KEY] = previous
48
+ end
49
+ end
50
+ end
51
+
52
+ def guc_statements
53
+ stmts = principal_gucs
54
+ return stmts unless Kiosk.configuration.enforce_db_role
55
+
56
+ stmts + [["SET LOCAL ROLE #{quote_ident(Kiosk.configuration.app_role)}", []]]
57
+ end
58
+
59
+ private
60
+
61
+ def apply_gucs
62
+ guc_statements.each { |sql, binds| connection.exec_query(sql, 'Kiosk GUC', binds) }
63
+ end
64
+
65
+ # One `set_config` per principal field; role and agent_id only when present.
66
+ def principal_gucs
67
+ {
68
+ Kiosk::GUC::USER_ID => identity.user_id,
69
+ Kiosk::GUC::ROLE => identity.role,
70
+ Kiosk::GUC::ACTOR => identity.actor,
71
+ Kiosk::GUC::AGENT_ID => identity.agent_id
72
+ }.filter_map do |field, value|
73
+ next if !value && [Kiosk::GUC::ROLE, Kiosk::GUC::AGENT_ID].include?(field)
74
+
75
+ guc_sql(Kiosk::GUC.for(Kiosk.configuration.guc_namespace, field), value)
76
+ end
77
+ end
78
+
79
+ def guc_sql(name, value)
80
+ [SET_GUC_SQL, [name.to_s, value.to_s]]
81
+ end
82
+
83
+ def quote_ident(name)
84
+ name.to_s.split('.').map { |part| %("#{part.gsub('"', '""')}") }.join('.')
85
+ end
86
+ end
87
+ end
88
+ end