kiosk-server 0.5.20 → 0.6.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +196 -0
- data/README.md +192 -138
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +21 -65
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +24 -7
- data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
- data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +17 -34
- data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
- data/app/controllers/kiosk/server/oauth_token_controller.rb +36 -32
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +20 -0
- data/app/models/kiosk/settlement.rb +1 -3
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -69
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -15
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +19 -25
- data/lib/kiosk/server/account_binding.rb +32 -62
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
- data/lib/kiosk/server/agent_rotation.rb +62 -0
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/assistant_key.rb +86 -0
- data/lib/kiosk/server/auth_toll.rb +6 -6
- data/lib/kiosk/server/binding_module_gate.rb +1 -1
- data/lib/kiosk/server/checkout.rb +57 -0
- data/lib/kiosk/server/client.rb +61 -0
- data/lib/kiosk/server/client_assertion.rb +154 -0
- data/lib/kiosk/server/client_registration.rb +98 -0
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +64 -75
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +38 -69
- data/lib/kiosk/server/dpop.rb +126 -0
- data/lib/kiosk/server/eddsa.rb +38 -0
- data/lib/kiosk/server/engine.rb +16 -13
- data/lib/kiosk/server/errors.rb +28 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +38 -10
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +13 -12
- data/lib/kiosk/server/executor/spending_cap.rb +17 -76
- data/lib/kiosk/server/executor.rb +86 -105
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +12 -3
- data/lib/kiosk/server/jwk.rb +83 -0
- data/lib/kiosk/server/jwks.rb +6 -0
- data/lib/kiosk/server/jwt_issuer.rb +7 -13
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +28 -29
- data/lib/kiosk/server/link_code.rb +9 -18
- data/lib/kiosk/server/linked_assistants.rb +22 -17
- data/lib/kiosk/server/mandate_verifier.rb +97 -139
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +31 -0
- data/lib/kiosk/server/oauth_error.rb +33 -0
- data/lib/kiosk/server/open_api.rb +31 -29
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +47 -78
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +30 -0
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +3 -3
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +9 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +369 -58
- data/lib/kiosk/server/schemas/binding.schema.json +24 -59
- data/lib/kiosk/server/schemas/kyc.schema.json +40 -78
- data/lib/kiosk/server/schemas/mandates.schema.json +123 -39
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/sd_jwt.rb +104 -0
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/signing_key.rb +18 -52
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +4 -4
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +272 -205
- data/lib/kiosk/server/wire_rendering.rb +2 -2
- data/lib/kiosk/server.rb +8 -2
- data/lib/kiosk/skill_link.rb +6 -2
- data/listen.py +38 -8
- metadata +34 -24
- data/app/models/kiosk/cart_mandate.rb +0 -21
- data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/agent_login.rb +0 -49
- data/lib/kiosk/server/agent_registration.rb +0 -88
- data/lib/kiosk/server/auth_challenge.rb +0 -33
- data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
- data/lib/kiosk/server/pop_verifier.rb +0 -87
- data/lib/kiosk/server/schema_definitions.rb +0 -351
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
|
@@ -4,55 +4,59 @@ require 'action_controller'
|
|
|
4
4
|
|
|
5
5
|
module Kiosk
|
|
6
6
|
module Server
|
|
7
|
-
# POST <endpoint>/oauth/token
|
|
8
|
-
# (RFC
|
|
7
|
+
# POST <endpoint>/oauth/token (RFC 6749 Section 3.2): a registered client
|
|
8
|
+
# authenticates with a client assertion (RFC 7523) and a DPoP proof of its
|
|
9
|
+
# registered key (RFC 9449), and gets an access token bound to that key.
|
|
10
|
+
# The client_credentials grant is always served; the device-code grant
|
|
11
|
+
# (RFC 8628 Section 3.4) only where the origin serves account binding.
|
|
9
12
|
class OauthTokenController < ::ActionController::API
|
|
10
|
-
include BindingModuleGate
|
|
11
13
|
include AuthController::Answers
|
|
12
14
|
include AuthController::Proofs
|
|
13
|
-
|
|
14
|
-
prepend_before_action :refuse_unserved_binding
|
|
15
|
-
|
|
16
|
-
UNSUPPORTED_GRANT = 'only the device_code grant is served here — ' \
|
|
17
|
-
'kiosk-pop (POST /auth/login) is the token-refresh path'
|
|
15
|
+
include OauthAnswers
|
|
18
16
|
|
|
19
17
|
def create
|
|
20
18
|
case params[:grant_type].to_s
|
|
21
|
-
when
|
|
22
|
-
when
|
|
23
|
-
|
|
19
|
+
when Client::CLIENT_CREDENTIALS then render_token(authenticated_client)
|
|
20
|
+
when Client::DEVICE_CODE then device_code_grant
|
|
21
|
+
when '' then raise OauthError.new(:invalid_request, 'grant_type required')
|
|
22
|
+
else raise OauthError.new(:unsupported_grant_type, unsupported_grant)
|
|
24
23
|
end
|
|
24
|
+
rescue OauthError => e
|
|
25
|
+
render_oauth_error(e)
|
|
26
|
+
rescue Errors::Base => e
|
|
27
|
+
render_error(e)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
28
31
|
|
|
29
|
-
# The
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# The toll comes before the client authenticates and before the code is read.
|
|
33
|
+
def device_code_grant
|
|
34
|
+
device_code = params[:device_code].to_s
|
|
35
|
+
raise OauthError.new(:unsupported_grant_type, unsupported_grant) unless Kiosk.configuration.serve_account_binding
|
|
36
|
+
raise OauthError.new(:invalid_request, 'device_code parameter required') if device_code.empty?
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
render_error(e)
|
|
38
|
+
DeviceCodeGrant.pay_toll!(device_code, proofs)
|
|
39
|
+
render_token(DeviceCodeGrant.exchange(device_code: device_code, client: authenticated_client))
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
# is the one error the server SHOULD signal with 401.
|
|
42
|
-
def render_grant_failure(result)
|
|
43
|
-
status = result[:error] == 'invalid_client' ? 401 : 400
|
|
44
|
-
render json: { error: result[:error], error_description: result[:description] }, status: status
|
|
45
|
-
end
|
|
42
|
+
def unsupported_grant = "the grant types served here are #{Client.grant_types.join(', ')}"
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
# The client assertion, then the DPoP proof of the registered key.
|
|
45
|
+
def authenticated_client
|
|
46
|
+
client = authenticate_client!
|
|
47
|
+
Dpop.verify_token_request!(request.headers['DPoP'], jkt: client.thumbprint, url: "#{Kiosk.current_issuer}#{request.path}")
|
|
48
|
+
client
|
|
51
49
|
end
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
# RFC 6749 Section 5.1. The scope is the role of the client. A requested
|
|
52
|
+
# `scope` is ignored (RFC 6749 Section 3.3): an assistant gets another
|
|
53
|
+
# role only when a human who holds that role binds it to their account,
|
|
54
|
+
# or through the identity provider.
|
|
55
|
+
def render_token(client)
|
|
56
|
+
token = AgentIdentityProviders::DefaultAgentIdp.new.issue(agent_id: client.id, role: client.role)
|
|
57
|
+
body = { access_token: token, token_type: Dpop::SCHEME, expires_in: Kiosk.configuration.access_token_ttl }
|
|
58
|
+
body[:scope] = client.role if client.role
|
|
59
|
+
render_oauth(body)
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
end
|
|
@@ -18,12 +18,11 @@ module Kiosk
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
+
# No script and no inline style: a host Content-Security-Policy of `default-src 'self'` holds.
|
|
21
22
|
def page(title, text)
|
|
22
23
|
<<~HTML
|
|
23
|
-
<!DOCTYPE html><html><head><meta charset="utf-8"><title>#{title}</title></head>
|
|
24
|
-
<body
|
|
25
|
-
<h1>#{title}</h1><p>#{text}</p>
|
|
26
|
-
</body></html>
|
|
24
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>#{title}</title></head>
|
|
25
|
+
<body><main><h1>#{title}</h1><p>#{text}</p></main></body></html>
|
|
27
26
|
HTML
|
|
28
27
|
end
|
|
29
28
|
end
|
|
@@ -6,8 +6,8 @@ require 'action_dispatch/http/parameters'
|
|
|
6
6
|
module Kiosk
|
|
7
7
|
module Server
|
|
8
8
|
# The reserved `GET <endpoint>/schema` (public) and `POST <endpoint>/pay`,
|
|
9
|
-
# and the base every
|
|
10
|
-
#
|
|
9
|
+
# and the base every WireController inherits its seams from.
|
|
10
|
+
# Response (JSON): success is the handler's payload VERBATIM; an error
|
|
11
11
|
# is an RFC 9457 problem document.
|
|
12
12
|
class WireController < ::ActionController::API
|
|
13
13
|
include VerbWire
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
# The checkout mandate the engine records when it settles a `pay`: the line
|
|
5
|
+
# items of the operator's checkout that the assistant agreed to pay for.
|
|
6
|
+
# Read-only for the operator.
|
|
7
|
+
class CheckoutMandate < Server::ApplicationRecord
|
|
8
|
+
has_many :settlements, class_name: 'Kiosk::Settlement', dependent: nil, inverse_of: :checkout_mandate
|
|
9
|
+
|
|
10
|
+
# Checkout mandates with a line item carrying these attributes, by jsonb
|
|
11
|
+
# containment: `referencing(order_id: id)` matches `[{"order_id": id, "qty": 2}]`.
|
|
12
|
+
# The values are quoted by the adapter, never interpolated.
|
|
13
|
+
scope :referencing, lambda { |**attributes|
|
|
14
|
+
where(Arel::Nodes::InfixOperation.new(
|
|
15
|
+
'@>', arel_table[:line_items],
|
|
16
|
+
Arel::Nodes.build_quoted([attributes.transform_values(&:to_s)].to_json)
|
|
17
|
+
))
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,8 +5,6 @@ module Kiosk
|
|
|
5
5
|
class Settlement < Server::ApplicationRecord
|
|
6
6
|
include Kiosk::Owned
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
belongs_to :cart_mandate, class_name: 'Kiosk::CartMandate', inverse_of: :settlements
|
|
8
|
+
belongs_to :checkout_mandate, class_name: 'Kiosk::CheckoutMandate', inverse_of: :settlements
|
|
11
9
|
end
|
|
12
10
|
end
|
data/app/models/kiosk/visit.rb
CHANGED
|
@@ -5,8 +5,6 @@ module Kiosk
|
|
|
5
5
|
# value counts toward the customer's recent spend, so an operator that takes
|
|
6
6
|
# no payment can still lower the proof-of-work toll for a returning customer.
|
|
7
7
|
class Visit < Server::ApplicationRecord
|
|
8
|
-
def self.table_name = "#{Kiosk.configuration.schema}.visits"
|
|
9
|
-
|
|
10
8
|
# Call it when the visit happens, with what the visit was worth.
|
|
11
9
|
def self.record!(user_id:, value_cents:) = create!(user_id:, value_cents:)
|
|
12
10
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%# «Link an assistant» page (link flow — Kiosk extension).
|
|
2
2
|
Minimal on purpose — override by shipping
|
|
3
3
|
app/views/kiosk/server/assistants/show.html.erb in the host app. %>
|
|
4
|
+
<% content_for :title, 'Linked assistants' %>
|
|
4
5
|
<main>
|
|
5
6
|
<h1>Linked assistant accounts</h1>
|
|
6
7
|
|
|
@@ -21,26 +22,17 @@
|
|
|
21
22
|
<section>
|
|
22
23
|
<h2>Link a new assistant</h2>
|
|
23
24
|
<p>
|
|
24
|
-
|
|
25
|
-
its own key and is then
|
|
25
|
+
Get a link code and paste it into your assistant's chat. The assistant
|
|
26
|
+
uses the code once, with its own key, and is then linked to your account.
|
|
26
27
|
</p>
|
|
27
28
|
<form action="<%= @page_path %>/link" method="post">
|
|
28
29
|
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
29
|
-
<button type="submit">
|
|
30
|
+
<button type="submit">Get a link code</button>
|
|
30
31
|
</form>
|
|
31
32
|
</section>
|
|
32
33
|
|
|
33
34
|
<section>
|
|
34
35
|
<h2>Currently linked</h2>
|
|
35
|
-
<% unless @spending_cap_enforced %>
|
|
36
|
-
<p role="note">
|
|
37
|
-
<strong>Note:</strong> this origin configures no <code>spending_cap</code>
|
|
38
|
-
seam, so a cap saved below is recorded against the assistant but nothing
|
|
39
|
-
reads it when a charge is settled. To make it bind, set
|
|
40
|
-
<code>c.spending_cap = Kiosk::Server::ColumnSpendingCap.new</code> in the
|
|
41
|
-
Kiosk initializer.
|
|
42
|
-
</p>
|
|
43
|
-
<% end %>
|
|
44
36
|
<% if @assistants.empty? %>
|
|
45
37
|
<p>No assistant accounts are linked yet.</p>
|
|
46
38
|
<% else %>
|
|
@@ -49,36 +41,40 @@
|
|
|
49
41
|
<li>
|
|
50
42
|
<strong><%= assistant[:human_label].to_s.empty? ? "(unnamed)" : assistant[:human_label] %></strong>
|
|
51
43
|
<code><%= assistant[:fingerprint] %></code>
|
|
52
|
-
|
|
44
|
+
<% if assistant[:bound_at] %>— linked <%= assistant[:bound_at] %><% end %>
|
|
45
|
+
<% if @payments %>
|
|
53
46
|
<p>
|
|
54
|
-
<%
|
|
55
|
-
|
|
56
|
-
%>
|
|
57
|
-
spent: <%= spent.empty? ? "0 cents" : spent.join(", ") %> —
|
|
47
|
+
<% spent = assistant[:spent_cents].map { |currency, cents| Kiosk::Server::Money.format(cents, currency) } %>
|
|
48
|
+
Spent: <%= spent.empty? ? "nothing" : spent.join(", ") %>
|
|
49
|
+
<% if @spending_cap %>
|
|
58
50
|
<%
|
|
59
51
|
cap = assistant[:spending_cap_cents]
|
|
60
52
|
cap_label = if cap.nil?
|
|
61
|
-
"cap
|
|
53
|
+
"no spending cap"
|
|
62
54
|
elsif cap.zero?
|
|
63
|
-
"
|
|
55
|
+
"no payments allowed"
|
|
64
56
|
else
|
|
65
|
-
"cap
|
|
57
|
+
"spending cap #{Kiosk::Server::Money.format(cap, assistant[:spending_cap_currency])} per payment"
|
|
66
58
|
end
|
|
67
59
|
%>
|
|
68
|
-
<%= cap_label %>
|
|
60
|
+
— <%= cap_label %>
|
|
61
|
+
<% end %>
|
|
69
62
|
</p>
|
|
63
|
+
<% end %>
|
|
70
64
|
<form action="<%= @page_path %>/update" method="post">
|
|
71
65
|
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
72
66
|
<input type="hidden" name="agent_id" value="<%= assistant[:agent_id] %>">
|
|
73
67
|
<label>Label
|
|
74
68
|
<input type="text" name="human_label" value="<%= assistant[:human_label] %>">
|
|
75
69
|
</label>
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
<% if @spending_cap %>
|
|
71
|
+
<label>Spending cap per payment (blank: no cap; 0: no payments)
|
|
72
|
+
<input type="text" name="spending_cap" inputmode="decimal" value="<%= assistant[:spending_cap_cents] && Kiosk::Server::Money.amount(assistant[:spending_cap_cents], assistant[:spending_cap_currency]) %>">
|
|
78
73
|
</label>
|
|
79
|
-
<label>Cap currency (
|
|
74
|
+
<label>Cap currency (a three-letter code such as EUR; the assistant cannot pay in another currency)
|
|
80
75
|
<input type="text" name="spending_cap_currency" maxlength="3" value="<%= assistant[:spending_cap_currency]&.upcase %>">
|
|
81
76
|
</label>
|
|
77
|
+
<% end %>
|
|
82
78
|
<button type="submit">Save</button>
|
|
83
79
|
</form>
|
|
84
80
|
<form action="<%= @page_path %>/unlink" method="post">
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<%# The document around the account holder's pages. Override by shipping
|
|
3
|
+
app/views/layouts/kiosk/server/application.html.erb in the host app. %>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title><%= yield :title %> — <%= request.host %></title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<%= yield %>
|
|
12
|
+
<footer><a href="/">Back to <%= request.host %></a></footer>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -6,24 +6,22 @@ require 'rails/generators/migration'
|
|
|
6
6
|
|
|
7
7
|
module Kiosk
|
|
8
8
|
module Generators
|
|
9
|
-
# `bin/rails g kiosk:install`: writes the Kiosk initializer, the
|
|
10
|
-
# and the migrations
|
|
9
|
+
# `bin/rails g kiosk:install`: writes the Kiosk initializer, the Kiosk routes
|
|
10
|
+
# and the migrations of Kiosk's `kiosk_*` tables.
|
|
11
11
|
class InstallGenerator < ::Rails::Generators::Base
|
|
12
12
|
include ::Rails::Generators::Migration
|
|
13
13
|
|
|
14
14
|
source_root File.expand_path('templates', __dir__)
|
|
15
15
|
|
|
16
16
|
desc 'Generate the Kiosk initializer and its canonical base migrations. ' \
|
|
17
|
-
'Draws the engine mount, so the origin answers
|
|
17
|
+
'Draws the engine mount, so the origin answers Kiosk requests.'
|
|
18
18
|
|
|
19
19
|
class_option :user_table, type: :string, default: 'users',
|
|
20
20
|
desc: "Provider's user table name"
|
|
21
|
-
class_option :user_id_type, type: :string, default: 'uuid',
|
|
21
|
+
class_option :user_id_type, type: :string, default: 'uuid', enum: %w[uuid bigint integer text],
|
|
22
22
|
desc: 'User-id column type: uuid | bigint | integer | text'
|
|
23
|
-
class_option :schema, type: :string, default: '
|
|
24
|
-
desc:
|
|
25
|
-
class_option :guc_namespace, type: :string, default: 'app',
|
|
26
|
-
desc: 'GUC namespace prefix used in the session GUC names'
|
|
23
|
+
class_option :schema, type: :string, default: 'public',
|
|
24
|
+
desc: "Postgres schema of Kiosk's tables"
|
|
27
25
|
|
|
28
26
|
# Ascending timestamps within one run, so the migrations sort in order.
|
|
29
27
|
@migration_counter = 0
|
|
@@ -36,23 +34,30 @@ module Kiosk
|
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
37
|
+
MIGRATIONS = %w[identity_tables device_authorizations mandates kyc_attributes events
|
|
38
|
+
pow_spent revocations visits refunds].freeze
|
|
39
|
+
|
|
39
40
|
no_commands do
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
def public_schema? = options[:schema] == 'public'
|
|
42
|
+
|
|
43
|
+
# A Kiosk table as a migration names it: `:kiosk_agents`, or `"ops.kiosk_agents"`.
|
|
44
|
+
def table(name) = public_schema? ? ":kiosk_#{name}" : %("#{options[:schema]}.kiosk_#{name}")
|
|
45
|
+
|
|
46
|
+
# The type of `users.id`, which every `user_id` column takes.
|
|
47
|
+
def user_id_type = ":#{options[:user_id_type]}"
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
# The foreign key of a `user_id` column: a row goes with its user.
|
|
50
|
+
def users_key = "{ to_table: :#{options[:user_table]}, on_delete: :cascade }"
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def create_initializer
|
|
50
54
|
template 'initializer.rb.tt', 'config/initializers/kiosk.rb'
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
# schema.rb dumps only the search path's schemas unless told otherwise
|
|
54
|
-
# public goes first because the kiosk tables reference the user table.
|
|
57
|
+
# schema.rb dumps only the search path's schemas unless told otherwise.
|
|
55
58
|
def dump_kiosk_schema
|
|
59
|
+
return if public_schema?
|
|
60
|
+
|
|
56
61
|
application %(config.active_record.dump_schemas = "public,#{options[:schema]}")
|
|
57
62
|
end
|
|
58
63
|
|
|
@@ -66,59 +71,10 @@ module Kiosk
|
|
|
66
71
|
RUBY
|
|
67
72
|
end
|
|
68
73
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def create_identity_tables_migration
|
|
75
|
-
migration_template 'create_kiosk_identity_tables.rb.tt',
|
|
76
|
-
'db/migrate/create_kiosk_identity_tables.rb'
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def create_device_authorizations_migration
|
|
80
|
-
migration_template 'create_kiosk_device_authorizations.rb.tt',
|
|
81
|
-
'db/migrate/create_kiosk_device_authorizations.rb'
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def create_mandates_migration
|
|
85
|
-
migration_template 'create_kiosk_mandates.rb.tt',
|
|
86
|
-
'db/migrate/create_kiosk_mandates.rb'
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def create_kyc_attributes_migration
|
|
90
|
-
migration_template 'create_kiosk_kyc_attributes.rb.tt',
|
|
91
|
-
'db/migrate/create_kiosk_kyc_attributes.rb'
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def create_events_migration
|
|
95
|
-
migration_template 'create_kiosk_events.rb.tt',
|
|
96
|
-
'db/migrate/create_kiosk_events.rb'
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def create_pow_spent_migration
|
|
100
|
-
migration_template 'create_kiosk_pow_spent.rb.tt',
|
|
101
|
-
'db/migrate/create_kiosk_pow_spent.rb'
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def create_revocations_migration
|
|
105
|
-
migration_template 'create_kiosk_revocations.rb.tt',
|
|
106
|
-
'db/migrate/create_kiosk_revocations.rb'
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def create_auth_challenges_migration
|
|
110
|
-
migration_template 'create_kiosk_auth_challenges.rb.tt',
|
|
111
|
-
'db/migrate/create_kiosk_auth_challenges.rb'
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def create_visits_migration
|
|
115
|
-
migration_template 'create_kiosk_visits.rb.tt',
|
|
116
|
-
'db/migrate/create_kiosk_visits.rb'
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def create_refunds_migration
|
|
120
|
-
migration_template 'create_kiosk_refunds.rb.tt',
|
|
121
|
-
'db/migrate/create_kiosk_refunds.rb'
|
|
74
|
+
def create_migrations
|
|
75
|
+
MIGRATIONS.each do |name|
|
|
76
|
+
migration_template "create_kiosk_#{name}.rb.tt", "db/migrate/create_kiosk_#{name}.rb"
|
|
77
|
+
end
|
|
122
78
|
end
|
|
123
79
|
end
|
|
124
80
|
end
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# user codes, the bound public key, claim vs link).
|
|
3
|
+
# The account-binding requests behind the claim and link ceremonies (RFC 8628
|
|
4
|
+
# shape). The device and user codes are stored hashed. `requested_role` is the
|
|
5
|
+
# role of the approving human, never one a client asks for.
|
|
7
6
|
class CreateKioskDeviceAuthorizations < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
def change
|
|
8
|
+
create_table <%= table :device_authorizations %>, id: :uuid do |t|
|
|
9
|
+
t.text :device_code_hash, null: false, index: { unique: true }
|
|
10
|
+
# Unique among pending requests only: a finished request may share a code with an old one.
|
|
11
|
+
t.text :user_code_hash, null: false, index: { unique: true, where: "status = 'pending'" }
|
|
12
|
+
t.text :public_key_pem
|
|
13
|
+
t.text :kind, null: false, default: 'claim'
|
|
14
|
+
t.text :client_id, null: false
|
|
15
|
+
t.text :requested_role
|
|
16
|
+
t.text :status, null: false
|
|
17
|
+
t.column :user_id, <%= user_id_type %>
|
|
18
|
+
t.timestamptz :expires_at, null: false, index: { where: "status IN ('pending', 'approved')" }
|
|
19
|
+
t.timestamptz :consumed_at
|
|
20
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
21
|
+
t.check_constraint "status IN ('pending', 'approved', 'denied', 'consumed', 'expired')",
|
|
22
|
+
name: 'kiosk_device_authorizations_status'
|
|
23
|
+
t.check_constraint "kind IN ('claim', 'link')", name: 'kiosk_device_authorizations_kind'
|
|
24
|
+
end
|
|
16
25
|
end
|
|
17
26
|
end
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# integer — one cursor then resumes every subscription on a socket with one
|
|
7
|
-
# integer comparison.
|
|
8
|
-
# `identity_key` is a user_id, not an agent_id: a human's second assistant must
|
|
9
|
-
# read the same tail, and a revoked assistant is stopped at the socket.
|
|
10
|
-
# Laid down unconditionally, like kiosk.kyc_attributes: the route is drawn
|
|
11
|
-
# whether or not this origin declares a topic, and an empty table costs nothing.
|
|
3
|
+
# The event tail. `id` is the origin's one ascending cursor: a single integer
|
|
4
|
+
# resumes every subscription on a socket. `identity_key` is a user id, so every
|
|
5
|
+
# assistant of one person reads the same tail.
|
|
12
6
|
class CreateKioskEvents < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
def change
|
|
8
|
+
create_table <%= table :events %> do |t|
|
|
9
|
+
t.text :identity_key, null: false
|
|
10
|
+
t.text :topic, null: false
|
|
11
|
+
t.text :subject
|
|
12
|
+
t.timestamptz :occurred_at, null: false
|
|
13
|
+
t.jsonb :data, null: false
|
|
14
|
+
# The retention sweep reads it.
|
|
15
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }, index: true
|
|
16
|
+
# `since` for one subscriber: the cursor scan, in id order.
|
|
17
|
+
t.index %i[identity_key id]
|
|
18
|
+
end
|
|
21
19
|
end
|
|
22
20
|
end
|
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Kiosk's identity tables: the registered assistants, the tolled calls of
|
|
4
|
+
# each account since its last proof, and the assistant accounts that
|
|
5
|
+
# registration opened, with the account each one merged into at its first binding.
|
|
4
6
|
class CreateKioskIdentityTables < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
5
|
-
def
|
|
6
|
-
|
|
7
|
-
<%=
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
def change
|
|
8
|
+
<% unless public_schema? -%>
|
|
9
|
+
create_schema "<%= options[:schema] %>", if_not_exists: true
|
|
10
|
+
|
|
11
|
+
<% end -%>
|
|
12
|
+
create_table <%= table :agents %>, id: :uuid do |t|
|
|
13
|
+
t.references :user, type: <%= user_id_type %>, null: false, foreign_key: <%= users_key %>,
|
|
14
|
+
index: { where: 'revoked_at IS NULL' }
|
|
15
|
+
t.text :allowed_roles, array: true, null: false, default: []
|
|
16
|
+
t.text :public_key
|
|
17
|
+
t.text :human_label
|
|
18
|
+
# The most one payment may charge. NULL: no cap. 0: no payments. A cap has its currency.
|
|
19
|
+
t.bigint :spending_cap_cents
|
|
20
|
+
t.text :spending_cap_currency
|
|
21
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
22
|
+
# The time of the latest binding.
|
|
23
|
+
t.timestamptz :bound_at
|
|
24
|
+
t.timestamptz :revoked_at
|
|
25
|
+
t.text :issuer, null: false
|
|
26
|
+
t.check_constraint "spending_cap_currency ~ '^[a-z]{3}$'", name: 'kiosk_agents_spending_cap_currency_code'
|
|
27
|
+
t.check_constraint '(spending_cap_cents IS NULL) = (spending_cap_currency IS NULL)',
|
|
28
|
+
name: 'kiosk_agents_spending_cap_currency_set'
|
|
29
|
+
# One live row per public key on an origin, enforced by the database; a revoked key can register again.
|
|
30
|
+
t.index %i[issuer public_key], unique: true, where: 'revoked_at IS NULL'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
create_table <%= table :tolled_calls %>, primary_key: :user_id, id: <%= user_id_type %>, default: nil do |t|
|
|
34
|
+
t.bigint :calls_since_proof, null: false, default: 0
|
|
35
|
+
t.foreign_key :<%= options[:user_table] %>, column: :user_id, on_delete: :cascade
|
|
36
|
+
end
|
|
10
37
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
create_table <%= table :assistant_accounts %>, primary_key: :user_id, id: <%= user_id_type %>, default: nil do |t|
|
|
39
|
+
t.column :merged_into, <%= user_id_type %>, index: { where: 'merged_into IS NOT NULL' }
|
|
40
|
+
t.timestamptz :merged_at
|
|
41
|
+
t.foreign_key :<%= options[:user_table] %>, column: :user_id, on_delete: :cascade
|
|
42
|
+
t.foreign_key :<%= options[:user_table] %>, column: :merged_into, on_delete: :cascade
|
|
43
|
+
end
|
|
15
44
|
end
|
|
16
45
|
end
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# never the DOB, licence number, or any underlying document.
|
|
7
|
-
# There is no value column: the grant IS the row, so every gate is an EXISTS
|
|
8
|
-
# and no reader has to decide which spelling of `true` counts.
|
|
9
|
-
# Additive/opt-in: needed only if an Action gates on specific KYC attributes.
|
|
3
|
+
# The KYC grants and the open verifications, keyed on the person. A grant is
|
|
4
|
+
# one row per anonymized attribute a valid attestation granted (`age_over_18`):
|
|
5
|
+
# only the name, never the document or the value behind it.
|
|
10
6
|
class CreateKioskKycAttributes < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
def change
|
|
8
|
+
create_table <%= table :kyc_attributes %>, primary_key: %i[user_id name] do |t|
|
|
9
|
+
t.references :user, type: <%= user_id_type %>, null: false, index: false, foreign_key: <%= users_key %>
|
|
10
|
+
t.text :name, null: false
|
|
11
|
+
t.timestamptz :granted_at, null: false, default: -> { 'now()' }
|
|
12
|
+
end
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
create_table <%= table :kyc_requests %>, id: :text do |t|
|
|
15
|
+
t.references :user, type: <%= user_id_type %>, null: false, index: false, foreign_key: <%= users_key %>
|
|
16
|
+
t.text :nonce, null: false
|
|
17
|
+
t.timestamptz :approved_at
|
|
18
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
19
|
+
t.index %i[user_id created_at]
|
|
20
|
+
end
|
|
19
21
|
end
|
|
20
22
|
end
|