kiosk-server 0.6.1 → 0.6.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -0
- data/README.md +134 -102
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +3 -3
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +2 -7
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +8 -7
- data/app/controllers/kiosk/server/oauth_token_controller.rb +4 -1
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +0 -2
- data/app/models/kiosk/settlement.rb +0 -2
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -64
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +1 -1
- data/lib/kiosk/server/account_binding.rb +14 -5
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +1 -1
- data/lib/kiosk/server/agent_rotation.rb +1 -1
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/auth_toll.rb +5 -5
- data/lib/kiosk/server/client.rb +1 -1
- data/lib/kiosk/server/client_assertion.rb +3 -2
- data/lib/kiosk/server/client_registration.rb +6 -2
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +15 -38
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +2 -2
- data/lib/kiosk/server/dpop.rb +6 -1
- data/lib/kiosk/server/engine.rb +2 -2
- data/lib/kiosk/server/errors.rb +21 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +23 -5
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +1 -1
- data/lib/kiosk/server/executor/spending_cap.rb +8 -66
- data/lib/kiosk/server/executor.rb +10 -10
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +1 -1
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +26 -27
- data/lib/kiosk/server/linked_assistants.rb +21 -16
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +1 -0
- data/lib/kiosk/server/oauth_error.rb +3 -0
- data/lib/kiosk/server/open_api.rb +9 -9
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +3 -3
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +4 -3
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +2 -1
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +1 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +2 -2
- data/lib/kiosk/server/schemas/binding.schema.json +3 -3
- data/lib/kiosk/server/schemas/kyc.schema.json +22 -3
- data/lib/kiosk/server/schemas/mandates.schema.json +5 -5
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +3 -3
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +14 -11
- data/lib/kiosk/server/wire_rendering.rb +1 -1
- data/lib/kiosk/server.rb +6 -2
- metadata +9 -8
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/schema_definitions.rb +0 -320
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
|
@@ -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
|
|
@@ -1,20 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# The AP2 trail of a `pay`: the checkout mandate, the payment mandate and the
|
|
4
|
+
# settlement. `reference` is the hash of the presented SD-JWT, unique per user.
|
|
5
|
+
# A checkout has one live checkout mandate: `declined_at` records a charge
|
|
6
|
+
# refused with no money moved, and frees the checkout.
|
|
6
7
|
class CreateKioskMandates < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
def change
|
|
9
|
+
create_table <%= table :checkout_mandates %>, id: :uuid do |t|
|
|
10
|
+
t.text :reference, null: false
|
|
11
|
+
t.text :checkout_hash, null: false
|
|
12
|
+
t.column :user_id, <%= user_id_type %>, null: false
|
|
13
|
+
t.uuid :agent_id, null: false
|
|
14
|
+
t.text :issuer, null: false
|
|
15
|
+
t.jsonb :line_items, null: false
|
|
16
|
+
t.bigint :total_amount_cents, null: false
|
|
17
|
+
t.text :currency, null: false
|
|
18
|
+
t.timestamptz :expires_at, null: false
|
|
19
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
20
|
+
t.text :sd_jwt, null: false
|
|
21
|
+
t.timestamptz :declined_at
|
|
22
|
+
t.index %i[user_id reference], unique: true
|
|
23
|
+
t.index %i[user_id checkout_hash], unique: true, where: 'declined_at IS NULL'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_table <%= table :payment_mandates %>, id: :uuid do |t|
|
|
27
|
+
t.text :reference, null: false
|
|
28
|
+
t.references :checkout_mandate, type: :uuid, null: false,
|
|
29
|
+
foreign_key: { to_table: <%= table :checkout_mandates %>, on_delete: :cascade }
|
|
30
|
+
t.column :user_id, <%= user_id_type %>, null: false
|
|
31
|
+
t.uuid :agent_id, null: false
|
|
32
|
+
t.text :issuer, null: false
|
|
33
|
+
t.text :payment_instrument, null: false
|
|
34
|
+
t.bigint :amount_cents, null: false
|
|
35
|
+
t.text :currency, null: false
|
|
36
|
+
t.timestamptz :expires_at
|
|
37
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
38
|
+
t.text :sd_jwt, null: false
|
|
39
|
+
t.index %i[user_id reference], unique: true
|
|
40
|
+
end
|
|
12
41
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
# The server writes a settlement; nobody signs it. One per checkout mandate.
|
|
43
|
+
create_table <%= table :settlements %>, id: :uuid do |t|
|
|
44
|
+
t.references :checkout_mandate, type: :uuid, null: false, index: { unique: true },
|
|
45
|
+
foreign_key: { to_table: <%= table :checkout_mandates %>, on_delete: :cascade }
|
|
46
|
+
t.column :user_id, <%= user_id_type %>, null: false, index: true
|
|
47
|
+
t.uuid :agent_id, null: false
|
|
48
|
+
t.text :issuer, null: false
|
|
49
|
+
t.text :psp_reference, null: false
|
|
50
|
+
t.bigint :settled_amount_cents, null: false
|
|
51
|
+
t.text :currency, null: false
|
|
52
|
+
t.text :network_confirmation_id
|
|
53
|
+
t.timestamptz :settled_at, null: false
|
|
54
|
+
end
|
|
19
55
|
end
|
|
20
56
|
end
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# every process and every deploy (PowSpentStores::ActiveRecord, the default).
|
|
3
|
+
# The spent ids of proof-of-work challenges and client assertions. The primary
|
|
4
|
+
# key accepts each id once, in every process and across deploys.
|
|
6
5
|
class CreateKioskPowSpent < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def down
|
|
14
|
-
execute %(DROP TABLE IF EXISTS "<%= options[:schema] %>".pow_spent)
|
|
6
|
+
def change
|
|
7
|
+
create_table <%= table :pow_spent %>, id: :text do |t|
|
|
8
|
+
# The expiry sweep reads it.
|
|
9
|
+
t.timestamptz :expires_at, null: false, index: true
|
|
10
|
+
end
|
|
15
11
|
end
|
|
16
12
|
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# recent spend leaves out of the proof-of-work toll.
|
|
3
|
+
# The refunds of settled charges, one row per PSP refund. Recent spend counts
|
|
4
|
+
# a settlement less what was refunded of it.
|
|
6
5
|
class CreateKioskRefunds < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
def change
|
|
7
|
+
create_table <%= table :refunds %>, id: :uuid do |t|
|
|
8
|
+
t.references :settlement, type: :uuid, null: false,
|
|
9
|
+
foreign_key: { to_table: <%= table :settlements %>, on_delete: :cascade }
|
|
10
|
+
t.text :psp_reference, null: false, index: { unique: true }
|
|
11
|
+
t.bigint :refunded_amount_cents, null: false
|
|
12
|
+
t.timestamptz :refunded_at, null: false
|
|
13
|
+
t.check_constraint 'refunded_amount_cents > 0', name: 'kiosk_refunds_refunded_amount_cents'
|
|
14
|
+
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# every process and across restarts (RevocationStores::ActiveRecord, the default).
|
|
3
|
+
# Each agent's revocation watermark: a token issued before it stops verifying,
|
|
4
|
+
# in every process and after a restart.
|
|
6
5
|
class CreateKioskRevocations < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def down
|
|
14
|
-
execute %(DROP TABLE IF EXISTS "<%= options[:schema] %>".revocations)
|
|
6
|
+
def change
|
|
7
|
+
create_table <%= table :revocations %>, primary_key: :agent_id, id: :uuid, default: nil do |t|
|
|
8
|
+
t.timestamptz :revoked_before, null: false
|
|
9
|
+
t.foreign_key <%= table :agents %>, column: :agent_id, on_delete: :cascade
|
|
10
|
+
end
|
|
15
11
|
end
|
|
16
12
|
end
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# lower the proof-of-work toll for a returning customer.
|
|
3
|
+
# The visits the operator records as fulfilled (Kiosk::Visit.record!). Their
|
|
4
|
+
# value lowers the proof-of-work toll for a returning customer.
|
|
6
5
|
class CreateKioskVisits < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
def change
|
|
7
|
+
create_table <%= table :visits %>, id: :uuid do |t|
|
|
8
|
+
t.column :user_id, <%= user_id_type %>, null: false
|
|
9
|
+
t.bigint :value_cents, null: false
|
|
10
|
+
t.timestamptz :visited_at, null: false, default: -> { 'now()' }
|
|
11
|
+
t.check_constraint 'value_cents >= 0', name: 'kiosk_visits_value_cents'
|
|
12
|
+
t.index %i[user_id visited_at]
|
|
13
|
+
end
|
|
15
14
|
end
|
|
16
15
|
end
|
|
@@ -9,17 +9,21 @@ Kiosk.configure do |c|
|
|
|
9
9
|
# Type of users.id: :uuid (default), :bigint, :integer or :text.
|
|
10
10
|
c.user_id_type = :<%= options[:user_id_type] %>
|
|
11
11
|
|
|
12
|
-
# coerce-then-validate on the per-verb
|
|
12
|
+
# coerce-then-validate on the per-verb endpoints is UNCONDITIONAL: arguments are
|
|
13
13
|
# always checked against the verb's `input_schema` (§8.1). This flag adds a check:
|
|
14
14
|
# it validates every answer against the verb's `output_schema`. Off in production,
|
|
15
15
|
# where a descriptor typo would become a 500 for a caller.
|
|
16
16
|
c.validate_responses = !Rails.env.production?
|
|
17
17
|
|
|
18
18
|
# GUC namespace for SET LOCAL (and RLS policies). Default "app".
|
|
19
|
-
# c.guc_namespace = "
|
|
19
|
+
# c.guc_namespace = "app"
|
|
20
20
|
|
|
21
|
-
# Postgres schema
|
|
22
|
-
|
|
21
|
+
# Postgres schema of Kiosk's kiosk_* tables. Default "public".
|
|
22
|
+
<% if public_schema? -%>
|
|
23
|
+
# c.schema = "public"
|
|
24
|
+
<% else -%>
|
|
25
|
+
c.schema = "<%= options[:schema] %>"
|
|
26
|
+
<% end -%>
|
|
23
27
|
|
|
24
28
|
# Opt-in Postgres RLS: add `gem "kiosk-rls"` and create these database roles yourself.
|
|
25
29
|
# c.app_role = "app_role" # runtime role with NOBYPASSRLS
|
|
@@ -42,6 +46,11 @@ Kiosk.configure do |c|
|
|
|
42
46
|
# Owner block surfaced in /.well-known/kiosk.json.
|
|
43
47
|
# c.owner = { name: "Your Company", support: "support@example.com" }
|
|
44
48
|
|
|
49
|
+
# The SHA-256 of the skill file that kiosk-server gives. Without it, discovery advertises no skill pin.
|
|
50
|
+
# curl -fsS "$(bin/rails runner 'puts Kiosk.configuration.skill_url')" | shasum -a 256
|
|
51
|
+
# Kiosk::TestHelpers::SkillPin (kiosk-test-support) fails when an upgrade gives a different skill.
|
|
52
|
+
# c.skill_sha256 = "<the 64 hexadecimal characters that the command prints>"
|
|
53
|
+
|
|
45
54
|
# Optional: an agent IdP of your own (subclass Kiosk::AgentIdentityProviders::Base; agent ids must be UUIDs).
|
|
46
55
|
# c.agent_idp = MyExternalAgentIdp.new
|
|
47
56
|
|
data/lib/kiosk/payable.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Kiosk
|
|
|
30
30
|
|
|
31
31
|
class_methods do
|
|
32
32
|
# Adds `settled_currency`, the currency of a settlement among `settlements`
|
|
33
|
-
# whose checkout names the row, or NULL: the caller's own
|
|
33
|
+
# whose checkout names the row, or NULL: the caller's own in the protocol, all
|
|
34
34
|
# of them in a back office.
|
|
35
35
|
def with_settlement(settlements)
|
|
36
36
|
select(arel_table[Arel.star],
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
module Kiosk
|
|
4
4
|
module Server
|
|
5
5
|
# Binds a registered client to a human's account: the client keeps its
|
|
6
|
-
# agent_id and reputation and takes the role of its new holder.
|
|
6
|
+
# agent_id and reputation and takes the role of its new holder. The first
|
|
7
|
+
# binding merges the assistant account into the human's account.
|
|
7
8
|
module AccountBinding
|
|
8
9
|
module_function
|
|
9
10
|
|
|
10
11
|
# Call only after the client has authenticated.
|
|
11
|
-
# `requested_role` always comes from a human's identity, never the
|
|
12
|
+
# `requested_role` always comes from a human's identity, never the request.
|
|
12
13
|
#
|
|
13
14
|
# @return [Client] the client under its new holder
|
|
14
15
|
def bind!(agent_id:, user_id:, requested_role: nil)
|
|
@@ -61,6 +62,7 @@ module Kiosk
|
|
|
61
62
|
transition = client.user_id.to_s != user_id.to_s
|
|
62
63
|
conn.transaction do
|
|
63
64
|
Rows.rebind(conn, config.schema, client.id, user_id, role)
|
|
65
|
+
carry_reputation(client.user_id, user_id) if transition
|
|
64
66
|
claimed(config, client.id, client.user_id, user_id) if transition && config.assistant_claimed
|
|
65
67
|
end
|
|
66
68
|
# The old holder's tokens die (§6.3). The IdP dates new mints at the
|
|
@@ -68,6 +70,13 @@ module Kiosk
|
|
|
68
70
|
revoke_tokens(config, client.id)
|
|
69
71
|
end
|
|
70
72
|
|
|
73
|
+
# The new account adds the previous account's count of tolled calls,
|
|
74
|
+
# and the first binding merges the assistant account into it.
|
|
75
|
+
def carry_reputation(from, to)
|
|
76
|
+
TolledCalls.carry!(from:, to:)
|
|
77
|
+
AssistantAccounts.merge!(from:, into: to)
|
|
78
|
+
end
|
|
79
|
+
|
|
71
80
|
# §6.3: the tokens of the previous holder stop.
|
|
72
81
|
def revoke_tokens(config, agent_id)
|
|
73
82
|
RevocationStores.revoke_issued(agent_id, store: config.revocation_store)
|
|
@@ -122,7 +131,7 @@ module Kiosk
|
|
|
122
131
|
|
|
123
132
|
def revoke_owned(conn, schema, agent_id, user_id)
|
|
124
133
|
conn.exec_query(<<~SQL.squish, 'Kiosk agent unlink', [agent_id, user_id, Kiosk.current_issuer]).to_a.first
|
|
125
|
-
UPDATE #{schema}.
|
|
134
|
+
UPDATE #{schema}.kiosk_agents
|
|
126
135
|
SET revoked_at = now()
|
|
127
136
|
WHERE id = $1
|
|
128
137
|
AND user_id = $2
|
|
@@ -137,8 +146,8 @@ module Kiosk
|
|
|
137
146
|
role_set, role_binds =
|
|
138
147
|
role ? [', allowed_roles = ARRAY[$3]::text[]', [role]] : [", allowed_roles = '{}'::text[]", []]
|
|
139
148
|
conn.exec_query(<<~SQL.squish, 'Kiosk agent rebind', [user_id, agent_id, *role_binds])
|
|
140
|
-
UPDATE #{schema}.
|
|
141
|
-
SET user_id = $1#{role_set}
|
|
149
|
+
UPDATE #{schema}.kiosk_agents
|
|
150
|
+
SET user_id = $1, bound_at = now()#{role_set}
|
|
142
151
|
WHERE id = $2
|
|
143
152
|
SQL
|
|
144
153
|
end
|
|
@@ -4,7 +4,8 @@ module Kiosk
|
|
|
4
4
|
module Server
|
|
5
5
|
# Requires a human signed in through the operator's own session
|
|
6
6
|
# (`user_idp`), never an agent token. A browser is sent to `sign_in_path`;
|
|
7
|
-
# any other caller gets a 401
|
|
7
|
+
# any other caller gets a 401 with the `Kiosk-Session` challenge, naming
|
|
8
|
+
# where to sign in.
|
|
8
9
|
module AccountHolderGate
|
|
9
10
|
private
|
|
10
11
|
|
|
@@ -22,6 +23,7 @@ module Kiosk
|
|
|
22
23
|
store_return_location
|
|
23
24
|
redirect_to sign_in_path
|
|
24
25
|
else
|
|
26
|
+
response.set_header('WWW-Authenticate', Errors::SessionRequired.challenge)
|
|
25
27
|
render plain: sign_in_path ? "#{prompt} #{sign_in_directions(sign_in_path)}" : prompt,
|
|
26
28
|
status: :unauthorized
|
|
27
29
|
end
|
|
@@ -70,7 +70,7 @@ module Kiosk
|
|
|
70
70
|
# `columns` is always a literal from this class, never caller input.
|
|
71
71
|
def agent_row(columns, agent_id)
|
|
72
72
|
::ActiveRecord::Base.lease_connection.exec_query(
|
|
73
|
-
"SELECT #{columns} FROM #{schema}.
|
|
73
|
+
"SELECT #{columns} FROM #{schema}.kiosk_agents WHERE id = $1 AND revoked_at IS NULL",
|
|
74
74
|
"Kiosk agent #{columns}",
|
|
75
75
|
[agent_id]
|
|
76
76
|
).to_a.first
|
|
@@ -51,7 +51,7 @@ module Kiosk
|
|
|
51
51
|
conn = ::ActiveRecord::Base.lease_connection
|
|
52
52
|
conn.transaction do
|
|
53
53
|
conn.exec_query(<<~SQL.squish, 'Kiosk agent key rotation', [pem, agent_id])
|
|
54
|
-
UPDATE #{Kiosk.configuration.schema}.
|
|
54
|
+
UPDATE #{Kiosk.configuration.schema}.kiosk_agents SET public_key = $1 WHERE id = $2
|
|
55
55
|
SQL
|
|
56
56
|
RevocationStores.revoke_issued(agent_id)
|
|
57
57
|
end
|
|
@@ -56,7 +56,7 @@ module Kiosk
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# What a caller has to send for each string `format` the
|
|
59
|
+
# What a caller has to send for each string `format` the protocol reads.
|
|
60
60
|
FORMAT_SPELLINGS = {
|
|
61
61
|
'date' => 'a calendar date as YYYY-MM-DD, e.g. 2026-08-19',
|
|
62
62
|
'date-time' => 'an ISO 8601 timestamp, e.g. 2026-08-19T14:00:00Z'
|
|
@@ -76,7 +76,7 @@ module Kiosk
|
|
|
76
76
|
"an object argument's leaves are SCALARS: `o%5Bk%5D=v`, not " \
|
|
77
77
|
'`o%5Bk%5D%5B%5D=v`. Anything richer is an ACTION (POST).'
|
|
78
78
|
|
|
79
|
-
# The query string's shape: what Rack can decode and the
|
|
79
|
+
# The query string's shape: what Rack can decode and the protocol accepts.
|
|
80
80
|
module QueryShape
|
|
81
81
|
module_function
|
|
82
82
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# The accounts that the engine opened for assistants at registration, in
|
|
6
|
+
# `assistant_accounts`. The first binding of the assistant merges its
|
|
7
|
+
# assistant account into the human's account: from then on the spend of
|
|
8
|
+
# the human's account includes the settlements and visits of the assistant
|
|
9
|
+
# account. Unlinking the assistant later keeps the merge.
|
|
10
|
+
module AssistantAccounts
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Records `user_id` as the account of a new assistant. An account that
|
|
14
|
+
# `assistant_creation` gave to an earlier key is recorded once.
|
|
15
|
+
def record!(user_id)
|
|
16
|
+
connection.exec_query(%(INSERT INTO "#{schema}".kiosk_assistant_accounts (user_id) VALUES ($1) ON CONFLICT DO NOTHING),
|
|
17
|
+
'Kiosk assistant account', [user_id])
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Merges the account `from` into the account `into` when `from` is an
|
|
22
|
+
# assistant account that is not merged yet. A binding moves an assistant
|
|
23
|
+
# to the account of the approving human, which is never an assistant
|
|
24
|
+
# account: so the account of one human never merges into the account of
|
|
25
|
+
# another, and merges do not form chains.
|
|
26
|
+
def merge!(from:, into:)
|
|
27
|
+
connection.exec_query(<<~SQL.squish, 'Kiosk assistant account merge', [from, into])
|
|
28
|
+
UPDATE "#{schema}".kiosk_assistant_accounts SET merged_into = $2, merged_at = now()
|
|
29
|
+
WHERE user_id = $1 AND merged_into IS NULL
|
|
30
|
+
SQL
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def connection = ::ActiveRecord::Base.lease_connection
|
|
35
|
+
def schema = Kiosk.configuration.schema
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -5,15 +5,15 @@ module Kiosk
|
|
|
5
5
|
# Optional proof-of-work toll on the auth requests that come before a
|
|
6
6
|
# token: `POST /oauth/register`, the link-code claim `POST /auth/claim`, and
|
|
7
7
|
# the first token poll of each device code. It runs before any lookup, so
|
|
8
|
-
# the answer never tells a caller whether a key or a code exists. It
|
|
9
|
-
# the cost
|
|
10
|
-
# (deploy/README.md "Edge rate-limit").
|
|
8
|
+
# the answer never tells a caller whether a key or a code exists. It sets
|
|
9
|
+
# the cost of each request, not a rate. The shipped deploy has no edge rate
|
|
10
|
+
# limit (deploy/README.md "Edge rate-limit").
|
|
11
11
|
module AuthToll
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
14
|
def on?(config = Kiosk.configuration) = config.registration_pow_count.to_i.positive?
|
|
15
15
|
|
|
16
|
-
# `verb` and `body` make the fingerprint, so a proof
|
|
16
|
+
# `verb` and `body` make the fingerprint, so a proof clears the toll of this request only.
|
|
17
17
|
def gate(verb:, body:, pow:, config: Kiosk.configuration)
|
|
18
18
|
return unless on?(config)
|
|
19
19
|
|
|
@@ -26,7 +26,7 @@ module Kiosk
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def secret!(config)
|
|
29
|
-
unless defined?(::Kiosk::Reputation) && defined?(::Kiosk::Pow::Equihash)
|
|
29
|
+
unless defined?(::Kiosk::Reputation::Challenge) && defined?(::Kiosk::Pow::Equihash.params)
|
|
30
30
|
raise Errors::ConfigurationError,
|
|
31
31
|
'registration_pow_count > 0 requires kiosk-reputation and kiosk-pow-equihash. ' \
|
|
32
32
|
'Add both gems (and `require` them) to your app.'
|
data/lib/kiosk/server/client.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Kiosk
|
|
|
31
31
|
def self.lookup(condition, value)
|
|
32
32
|
row = ::ActiveRecord::Base.lease_connection.exec_query(<<~SQL.squish, 'Kiosk client lookup', [value, Kiosk.current_issuer]).to_a.first
|
|
33
33
|
SELECT id, user_id, public_key, allowed_roles, extract(epoch FROM created_at)::bigint AS issued_at
|
|
34
|
-
FROM #{Kiosk.configuration.schema}.
|
|
34
|
+
FROM #{Kiosk.configuration.schema}.kiosk_agents
|
|
35
35
|
WHERE #{condition} AND issuer = $2 AND revoked_at IS NULL
|
|
36
36
|
LIMIT 1
|
|
37
37
|
SQL
|
|
@@ -17,7 +17,8 @@ module Kiosk
|
|
|
17
17
|
REQUIRED_CLAIMS = %w[iss sub aud exp jti].freeze
|
|
18
18
|
|
|
19
19
|
# A refused client assertion. The `/oauth/*` endpoints answer it as
|
|
20
|
-
# `invalid_client`, the other endpoints as `401 unauthenticated
|
|
20
|
+
# `invalid_client`, the other endpoints as `401 unauthenticated` with the
|
|
21
|
+
# `Kiosk-Client-Assertion` challenge.
|
|
21
22
|
class Refused < StandardError
|
|
22
23
|
attr_reader :hint
|
|
23
24
|
|
|
@@ -27,7 +28,7 @@ module Kiosk
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def to_oauth = OauthError.new(:invalid_client, "#{message}: #{hint}")
|
|
30
|
-
def to_problem = Errors::
|
|
31
|
+
def to_problem = Errors::ClientAssertionRefused.new(message, hint: hint)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# A verified assertion that is not yet spent.
|
|
@@ -69,7 +69,11 @@ module Kiosk
|
|
|
69
69
|
def register(config, key, role)
|
|
70
70
|
pem = AssistantKey.to_pem(key)
|
|
71
71
|
conn = ::ActiveRecord::Base.lease_connection
|
|
72
|
-
conn.transaction(requires_new: true)
|
|
72
|
+
conn.transaction(requires_new: true) do
|
|
73
|
+
account = create_assistant_account(config, pem)
|
|
74
|
+
AssistantAccounts.record!(account)
|
|
75
|
+
insert_agent(conn, config, [account, pem], role)
|
|
76
|
+
end
|
|
73
77
|
Client.with_key(key)
|
|
74
78
|
rescue ::ActiveRecord::RecordNotUnique
|
|
75
79
|
Client.with_key(key)
|
|
@@ -79,7 +83,7 @@ module Kiosk
|
|
|
79
83
|
def insert_agent(conn, config, binds, role)
|
|
80
84
|
allowed_roles_sql, role_binds = role ? ['ARRAY[$4]::text[]', [role]] : ["'{}'::text[]", []]
|
|
81
85
|
conn.exec_query(<<~SQL.squish, 'Kiosk agent insert', [*binds, Kiosk.current_issuer, *role_binds])
|
|
82
|
-
INSERT INTO #{config.schema}.
|
|
86
|
+
INSERT INTO #{config.schema}.kiosk_agents (user_id, public_key, issuer, allowed_roles)
|
|
83
87
|
VALUES ($1, $2, $3, #{allowed_roles_sql})
|
|
84
88
|
SQL
|
|
85
89
|
end
|
|
@@ -14,7 +14,7 @@ module Kiosk
|
|
|
14
14
|
# The leased connection: the cap is read inside the transaction that charges.
|
|
15
15
|
row = ::ActiveRecord::Base.lease_connection.exec_query(<<~SQL.squish, 'Kiosk spending cap', [agent_id]).to_a.first
|
|
16
16
|
SELECT spending_cap_cents, spending_cap_currency
|
|
17
|
-
FROM "#{Kiosk.configuration.schema}".
|
|
17
|
+
FROM "#{Kiosk.configuration.schema}".kiosk_agents
|
|
18
18
|
WHERE id = $1 AND revoked_at IS NULL
|
|
19
19
|
SQL
|
|
20
20
|
return nil if row.nil? || row['spending_cap_cents'].nil?
|