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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +549 -0
- data/LICENSE.txt +202 -0
- data/README.md +635 -0
- data/app/channels/kiosk_events.rb +138 -0
- data/app/controllers/kiosk/server/application_controller.rb +17 -0
- data/app/controllers/kiosk/server/assistants_controller.rb +96 -0
- data/app/controllers/kiosk/server/auth_controller.rb +187 -0
- data/app/controllers/kiosk/server/device_verify_controller.rb +101 -0
- data/app/controllers/kiosk/server/discovery_controller.rb +66 -0
- data/app/controllers/kiosk/server/jwks_controller.rb +15 -0
- data/app/controllers/kiosk/server/kyc_attestation_controller.rb +69 -0
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +15 -0
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +73 -0
- data/app/controllers/kiosk/server/oauth_token_controller.rb +59 -0
- data/app/controllers/kiosk/server/open_api_controller.rb +20 -0
- data/app/controllers/kiosk/server/payment_setup_controller.rb +31 -0
- data/app/controllers/kiosk/server/verb_controller.rb +61 -0
- data/app/controllers/kiosk/server/wire_controller.rb +43 -0
- data/app/models/kiosk/cart_mandate.rb +21 -0
- data/app/models/kiosk/server/application_record.rb +10 -0
- data/app/models/kiosk/settlement.rb +12 -0
- data/app/models/kiosk/visit.rb +13 -0
- data/app/views/kiosk/server/assistants/show.html.erb +94 -0
- data/app/views/kiosk/server/device_verify/decided.html.erb +14 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +53 -0
- data/lib/generators/kiosk/install/install_generator.rb +125 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +17 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +22 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +20 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +21 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +17 -0
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +16 -0
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +53 -0
- data/lib/kiosk/handler.rb +43 -0
- data/lib/kiosk/owned.rb +17 -0
- data/lib/kiosk/payable.rb +90 -0
- data/lib/kiosk/server/account_binding.rb +188 -0
- data/lib/kiosk/server/account_holder_gate.rb +58 -0
- data/lib/kiosk/server/action_event.rb +60 -0
- data/lib/kiosk/server/actions.rb +14 -0
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +89 -0
- data/lib/kiosk/server/agent_login.rb +49 -0
- data/lib/kiosk/server/agent_registration.rb +88 -0
- data/lib/kiosk/server/argument_decoder.rb +260 -0
- data/lib/kiosk/server/audit_sink.rb +42 -0
- data/lib/kiosk/server/auth_challenge.rb +33 -0
- data/lib/kiosk/server/auth_challenge_stores.rb +74 -0
- data/lib/kiosk/server/auth_toll.rb +44 -0
- data/lib/kiosk/server/binding_module_gate.rb +25 -0
- data/lib/kiosk/server/caller_timezone.rb +50 -0
- data/lib/kiosk/server/column_spending_cap.rb +26 -0
- data/lib/kiosk/server/configuration_extension.rb +328 -0
- data/lib/kiosk/server/controller_verb.rb +63 -0
- data/lib/kiosk/server/current_request.rb +31 -0
- data/lib/kiosk/server/cursor.rb +33 -0
- data/lib/kiosk/server/device_authorization.rb +136 -0
- data/lib/kiosk/server/device_authorization_stores.rb +184 -0
- data/lib/kiosk/server/device_code_grant.rb +145 -0
- data/lib/kiosk/server/device_verification.rb +46 -0
- data/lib/kiosk/server/engine.rb +203 -0
- data/lib/kiosk/server/errors.rb +339 -0
- data/lib/kiosk/server/event_stores.rb +124 -0
- data/lib/kiosk/server/events.rb +86 -0
- data/lib/kiosk/server/events_cable.rb +102 -0
- data/lib/kiosk/server/events_connection.rb +170 -0
- data/lib/kiosk/server/executor/repeat.rb +42 -0
- data/lib/kiosk/server/executor/spending_cap.rb +102 -0
- data/lib/kiosk/server/executor.rb +397 -0
- data/lib/kiosk/server/failure_log.rb +28 -0
- data/lib/kiosk/server/handler_mixin.rb +418 -0
- data/lib/kiosk/server/handler_registrations.rb +57 -0
- data/lib/kiosk/server/handler_serving.rb +197 -0
- data/lib/kiosk/server/headers.rb +82 -0
- data/lib/kiosk/server/headers_middleware.rb +31 -0
- data/lib/kiosk/server/identity_resolution.rb +20 -0
- data/lib/kiosk/server/issuer_middleware.rb +22 -0
- data/lib/kiosk/server/jwks.rb +14 -0
- data/lib/kiosk/server/jwt_issuer.rb +98 -0
- data/lib/kiosk/server/kyc.rb +261 -0
- data/lib/kiosk/server/kyc_verifier.rb +106 -0
- data/lib/kiosk/server/link_code.rb +90 -0
- data/lib/kiosk/server/linked_assistants.rb +116 -0
- data/lib/kiosk/server/mandate_verifier.rb +221 -0
- data/lib/kiosk/server/open_api.rb +543 -0
- data/lib/kiosk/server/page.rb +19 -0
- data/lib/kiosk/server/payment_claim.rb +293 -0
- data/lib/kiosk/server/payment_setup.rb +102 -0
- data/lib/kiosk/server/pop_verifier.rb +87 -0
- data/lib/kiosk/server/pow_gate.rb +121 -0
- data/lib/kiosk/server/pow_spent_stores.rb +96 -0
- data/lib/kiosk/server/pow_toll.rb +136 -0
- data/lib/kiosk/server/queries.rb +14 -0
- data/lib/kiosk/server/reputation_factors.rb +38 -0
- data/lib/kiosk/server/request_validation.rb +171 -0
- data/lib/kiosk/server/response_validation.rb +45 -0
- data/lib/kiosk/server/result.rb +25 -0
- data/lib/kiosk/server/revocation_stores.rb +57 -0
- data/lib/kiosk/server/routing.rb +76 -0
- data/lib/kiosk/server/schema_definitions.rb +351 -0
- data/lib/kiosk/server/schema_document.rb +116 -0
- data/lib/kiosk/server/schema_slots.rb +119 -0
- data/lib/kiosk/server/schemas/auth.schema.json +135 -0
- data/lib/kiosk/server/schemas/binding.schema.json +145 -0
- data/lib/kiosk/server/schemas/kyc.schema.json +114 -0
- data/lib/kiosk/server/schemas/mandates.schema.json +75 -0
- data/lib/kiosk/server/schemas/pow.schema.json +94 -0
- data/lib/kiosk/server/session_context.rb +88 -0
- data/lib/kiosk/server/settled_spend.rb +21 -0
- data/lib/kiosk/server/signing_key.rb +82 -0
- data/lib/kiosk/server/verb_registry.rb +77 -0
- data/lib/kiosk/server/verb_vocabulary.rb +163 -0
- data/lib/kiosk/server/verb_wire.rb +85 -0
- data/lib/kiosk/server/version.rb +7 -0
- data/lib/kiosk/server/well_known.rb +416 -0
- data/lib/kiosk/server/wire_rendering.rb +116 -0
- data/lib/kiosk/server/wrong_door.rb +42 -0
- data/lib/kiosk/server.rb +42 -0
- data/lib/kiosk/skill_link.rb +16 -0
- data/listen.py +278 -0
- data/requirements.txt +1 -0
- metadata +349 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# What an assistant spent: each settlement less what was refunded of it,
|
|
6
|
+
# never below zero, one tally per currency. The «Link an assistant» page
|
|
7
|
+
# shows this figure. The spending cap counts it plus the carts in flight.
|
|
8
|
+
module SettledSpend
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# The net cents of the current `settlements` row.
|
|
12
|
+
def net_cents_sql(schema)
|
|
13
|
+
<<~SQL.squish
|
|
14
|
+
GREATEST(settlements.settled_amount_cents - (
|
|
15
|
+
SELECT COALESCE(SUM(r.refunded_amount_cents), 0) FROM #{schema}.refunds r WHERE r.settlement_id = settlements.id
|
|
16
|
+
), 0)
|
|
17
|
+
SQL
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Kiosk
|
|
9
|
+
module Server
|
|
10
|
+
# The RSA key that signs this deployment's JWTs, and its public JWK.
|
|
11
|
+
class SigningKey
|
|
12
|
+
MIN_KEY_BITS = 2048
|
|
13
|
+
|
|
14
|
+
ALGORITHM = 'RS256'
|
|
15
|
+
|
|
16
|
+
attr_reader :rsa
|
|
17
|
+
|
|
18
|
+
def initialize(rsa)
|
|
19
|
+
raise ArgumentError, "expected OpenSSL::PKey::RSA, got #{rsa.class}" unless rsa.is_a?(OpenSSL::PKey::RSA)
|
|
20
|
+
if rsa.n.num_bits < MIN_KEY_BITS
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"RSA key is #{rsa.n.num_bits} bits; minimum is #{MIN_KEY_BITS}"
|
|
23
|
+
end
|
|
24
|
+
@rsa = rsa
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.generate(bits: MIN_KEY_BITS)
|
|
28
|
+
new(OpenSSL::PKey::RSA.generate(bits))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A public-only PEM verifies; a full keypair also signs.
|
|
32
|
+
def self.from_pem(pem)
|
|
33
|
+
new(OpenSSL::PKey::RSA.new(pem))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
delegate :public_key, to: :rsa
|
|
37
|
+
|
|
38
|
+
delegate :private?, to: :rsa
|
|
39
|
+
|
|
40
|
+
def to_pem
|
|
41
|
+
raise 'cannot export PEM: signing key is public-only' unless private?
|
|
42
|
+
|
|
43
|
+
rsa.to_pem
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The RFC 7638 thumbprint.
|
|
47
|
+
def kid
|
|
48
|
+
@kid ||= compute_thumbprint
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# RFC 7517 §4; never the private parameters.
|
|
52
|
+
def to_jwk
|
|
53
|
+
{
|
|
54
|
+
kty: 'RSA',
|
|
55
|
+
use: 'sig',
|
|
56
|
+
alg: ALGORITHM,
|
|
57
|
+
kid: kid,
|
|
58
|
+
n: b64url(rsa.n.to_s(2)),
|
|
59
|
+
e: b64url(rsa.e.to_s(2))
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def compute_thumbprint
|
|
66
|
+
# RFC 7638 §3.2: the required members, in alphabetical order.
|
|
67
|
+
canonical = JSON.generate(
|
|
68
|
+
{
|
|
69
|
+
e: b64url(rsa.e.to_s(2)),
|
|
70
|
+
kty: 'RSA',
|
|
71
|
+
n: b64url(rsa.n.to_s(2))
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
b64url(Digest::SHA256.digest(canonical))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def b64url(bytes)
|
|
78
|
+
Base64.urlsafe_encode64(bytes, padding: false)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# A verb registry: name → handler plus the descriptor an origin publishes.
|
|
6
|
+
# {Queries} and {Actions} are this module plus a `SCOPE`. Verbs are declared
|
|
7
|
+
# through {Kiosk::Handler}; `input_schema` and `output_schema` are required (§8.3).
|
|
8
|
+
module VerbRegistry
|
|
9
|
+
Entry = Data.define(:handler, :reach, :description, :input_schema, :output_schema,
|
|
10
|
+
:example_params, :example_row)
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Undeclared optional keys are omitted rather than null.
|
|
14
|
+
def add_declared_slots(descriptor, found)
|
|
15
|
+
descriptor[:input_schema] = found.input_schema unless found.input_schema.nil?
|
|
16
|
+
descriptor[:output_schema] = found.output_schema unless found.output_schema.nil?
|
|
17
|
+
descriptor[:example_params] = found.example_params unless found.example_params.nil?
|
|
18
|
+
descriptor[:example_row] = found.example_row unless found.example_row.nil?
|
|
19
|
+
descriptor
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @api private — the {HandlerMixin} is the only caller.
|
|
24
|
+
# `slots`: any of {SchemaSlots::RESOLVABLE_SLOTS}; an undeclared one is nil.
|
|
25
|
+
def declare(name, handler, reach: HandlerMixin::DEFAULT_REACH, description: nil, **slots)
|
|
26
|
+
slots = SchemaSlots::RESOLVABLE_SLOTS.index_with(nil).merge(slots)
|
|
27
|
+
entry = Entry.new(handler: handler, reach: reach, description: description, **slots)
|
|
28
|
+
SchemaSlots.note_declaration(slots)
|
|
29
|
+
registry[name.to_s] = entry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch(name)
|
|
33
|
+
entry(name).handler
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def describe(name)
|
|
37
|
+
found = entry(name)
|
|
38
|
+
SchemaSlots.descriptor(self::SCOPE, name, found) do
|
|
39
|
+
descriptor = { name: name.to_s, description: found.description, reach: found.reach.to_s }
|
|
40
|
+
VerbRegistry.add_declared_slots(descriptor, found)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def catalog
|
|
45
|
+
registry.keys.sort.map { |name| describe(name) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def known
|
|
49
|
+
registry.keys
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def unregister(name)
|
|
53
|
+
registry.delete(name.to_s)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reset!
|
|
57
|
+
@registry = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The 404's hint lists the registered names, already public via `/schema`.
|
|
63
|
+
def entry(name)
|
|
64
|
+
registry.fetch(name.to_s) do
|
|
65
|
+
raise Errors::VerbNotFound.new(
|
|
66
|
+
"Unknown #{self::SCOPE}: #{name.inspect}",
|
|
67
|
+
hint: Errors.unknown_name_hint(name, self::SCOPE.to_s, registry.keys.sort)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def registry
|
|
73
|
+
@registry ||= {}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kiosk
|
|
4
|
+
module Server
|
|
5
|
+
# A policy hook is asked about writes as `:run` (§13), never `:action`, and
|
|
6
|
+
# a branch on `:action` would silently toll nothing. Refuses one at load by
|
|
7
|
+
# parsing the hook's source; returns `:unreadable` when it cannot.
|
|
8
|
+
module VerbVocabulary
|
|
9
|
+
WRITE_KIND = :run
|
|
10
|
+
|
|
11
|
+
DECLARATION_ALIAS = :action
|
|
12
|
+
|
|
13
|
+
COMPARISONS = %i[== != eql? equal?].freeze
|
|
14
|
+
|
|
15
|
+
def self.assert!(target, method_name, label)
|
|
16
|
+
location = HookSource.location_for(target, method_name)
|
|
17
|
+
return :unreadable if location.nil?
|
|
18
|
+
|
|
19
|
+
node = HookSource.node_at(*location)
|
|
20
|
+
return :unreadable if node.nil?
|
|
21
|
+
|
|
22
|
+
return :clean unless alias_branch?(node)
|
|
23
|
+
|
|
24
|
+
raise Errors::ConfigurationError, alias_branch_message(label, *location)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.alias_branch_message(label, file, line)
|
|
28
|
+
"Kiosk::Server: #{label} branches on :#{DECLARATION_ALIAS}, which this hook never " \
|
|
29
|
+
"receives (#{file}:#{line}). The gate hands it one of " \
|
|
30
|
+
"Kiosk::Server::Executor::VERBS — #{Executor::VERBS.map(&:inspect).join(', ')} — in which a " \
|
|
31
|
+
"handler declared `kind :#{DECLARATION_ALIAS}` arrives as :#{WRITE_KIND}. Branch on " \
|
|
32
|
+
":#{WRITE_KIND}. (Protocol Section 13, Reputation.) A branch on :#{DECLARATION_ALIAS} " \
|
|
33
|
+
'would match nothing and silently decline to toll every write.'
|
|
34
|
+
end
|
|
35
|
+
private_class_method :alias_branch_message
|
|
36
|
+
|
|
37
|
+
def self.alias_branch?(node)
|
|
38
|
+
found = false
|
|
39
|
+
HookSource.walk(node) do |candidate|
|
|
40
|
+
next if found
|
|
41
|
+
|
|
42
|
+
found = true if comparison_against_alias?(candidate) ||
|
|
43
|
+
when_clause_against_alias?(candidate) ||
|
|
44
|
+
inclusion_against_alias?(candidate)
|
|
45
|
+
end
|
|
46
|
+
found
|
|
47
|
+
end
|
|
48
|
+
private_class_method :alias_branch?
|
|
49
|
+
|
|
50
|
+
def self.comparison_against_alias?(node)
|
|
51
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
52
|
+
return false unless COMPARISONS.include?(node.name)
|
|
53
|
+
|
|
54
|
+
operands = [node.receiver, *(node.arguments&.arguments || [])]
|
|
55
|
+
operands.any? { |operand| alias_literal?(operand) }
|
|
56
|
+
end
|
|
57
|
+
private_class_method :comparison_against_alias?
|
|
58
|
+
|
|
59
|
+
def self.when_clause_against_alias?(node)
|
|
60
|
+
return false unless node.is_a?(Prism::WhenNode)
|
|
61
|
+
|
|
62
|
+
node.conditions.any? { |condition| alias_literal?(condition) }
|
|
63
|
+
end
|
|
64
|
+
private_class_method :when_clause_against_alias?
|
|
65
|
+
|
|
66
|
+
def self.inclusion_against_alias?(node)
|
|
67
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
68
|
+
return false unless %i[include? member? exclude?].include?(node.name)
|
|
69
|
+
|
|
70
|
+
inclusion_candidates(node).any? { |candidate| alias_literal?(candidate) }
|
|
71
|
+
end
|
|
72
|
+
private_class_method :inclusion_against_alias?
|
|
73
|
+
|
|
74
|
+
# The receiver's array elements, then each argument and its array elements.
|
|
75
|
+
def self.inclusion_candidates(node)
|
|
76
|
+
candidates = node.receiver.is_a?(Prism::ArrayNode) ? node.receiver.elements.dup : []
|
|
77
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
78
|
+
candidates.concat(argument.elements) if argument.is_a?(Prism::ArrayNode)
|
|
79
|
+
candidates << argument
|
|
80
|
+
end
|
|
81
|
+
candidates
|
|
82
|
+
end
|
|
83
|
+
private_class_method :inclusion_candidates
|
|
84
|
+
|
|
85
|
+
def self.alias_literal?(node)
|
|
86
|
+
case node
|
|
87
|
+
when Prism::SymbolNode, Prism::StringNode
|
|
88
|
+
node.unescaped == DECLARATION_ALIAS.to_s
|
|
89
|
+
else
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :alias_literal?
|
|
94
|
+
|
|
95
|
+
# Finds a hook's source and parses it; every answer is nil when it cannot.
|
|
96
|
+
module HookSource
|
|
97
|
+
module_function
|
|
98
|
+
|
|
99
|
+
def location_for(target, method_name)
|
|
100
|
+
callable = callable_for(target, method_name)
|
|
101
|
+
return nil unless callable.respond_to?(:source_location)
|
|
102
|
+
|
|
103
|
+
readable_location(callable.source_location)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def callable_for(target, method_name)
|
|
107
|
+
target.method(method_name) if target.respond_to?(method_name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `[file, line]` when the file can be read.
|
|
111
|
+
def readable_location(location)
|
|
112
|
+
return nil unless location.is_a?(Array) && location[0].is_a?(String) && location[1].is_a?(Integer)
|
|
113
|
+
|
|
114
|
+
location if File.readable?(location[0])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The widest construct starting on the hook's source line.
|
|
118
|
+
def node_at(file, line)
|
|
119
|
+
tree = parse(file)
|
|
120
|
+
return nil if tree.nil?
|
|
121
|
+
|
|
122
|
+
best = nil
|
|
123
|
+
walk(tree) do |node|
|
|
124
|
+
next unless node.location.start_line == line
|
|
125
|
+
next if best && node.location.end_line <= best.location.end_line
|
|
126
|
+
|
|
127
|
+
best = node
|
|
128
|
+
end
|
|
129
|
+
best
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse(file)
|
|
133
|
+
@parsed ||= {}
|
|
134
|
+
key = [file, mtime_of(file)]
|
|
135
|
+
return @parsed[key] if @parsed.key?(key)
|
|
136
|
+
|
|
137
|
+
@parsed[key] = parse_tree(file)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mtime_of(file)
|
|
141
|
+
File.mtime(file)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_tree(file)
|
|
147
|
+
require 'prism'
|
|
148
|
+
result = Prism.parse_file(file)
|
|
149
|
+
result.success? ? result.value : nil
|
|
150
|
+
rescue LoadError, StandardError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def walk(node, &block)
|
|
155
|
+
return if node.nil?
|
|
156
|
+
|
|
157
|
+
block.call(node)
|
|
158
|
+
node.compact_child_nodes.each { |child| walk(child, &block) }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Kiosk
|
|
6
|
+
module Server
|
|
7
|
+
# The gates of a call on the wire, in order: identity (401), the arguments
|
|
8
|
+
# (400), the caller's timezone (400) and the toll (402). Then the call runs
|
|
9
|
+
# inside a session. {WireController} and every controller that includes
|
|
10
|
+
# {Kiosk::Handler} serve the wire with them.
|
|
11
|
+
module VerbWire
|
|
12
|
+
include WireRendering
|
|
13
|
+
|
|
14
|
+
BODY_HINT = "an action's arguments are a JSON object in the request body; " \
|
|
15
|
+
"a query's are in the query string."
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_identity!
|
|
20
|
+
identity = IdentityResolution.resolve(request)
|
|
21
|
+
raise Errors::Unauthenticated, 'no identity resolved from request' if identity.nil?
|
|
22
|
+
|
|
23
|
+
identity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Query string for a query, JSON body for an action; never both.
|
|
27
|
+
def arguments_for(command, name, descriptor)
|
|
28
|
+
args = if command == :query
|
|
29
|
+
ArgumentDecoder.decode(request.query_string, input_schema: descriptor[:input_schema])
|
|
30
|
+
else
|
|
31
|
+
parse_body!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Unconditional: §8.1 item 5 makes coerce-then-validate a MUST.
|
|
35
|
+
RequestValidation.validate_arguments!(
|
|
36
|
+
args, input_schema: descriptor[:input_schema], verb: name, query: command == :query
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
args
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_body!
|
|
43
|
+
raw = request.raw_post
|
|
44
|
+
return {} if raw.to_s.empty?
|
|
45
|
+
|
|
46
|
+
parsed = JSON.parse(raw, symbolize_names: true)
|
|
47
|
+
raise Errors::BadRequest, 'request body must be a JSON object' unless parsed.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
parsed
|
|
50
|
+
rescue JSON::ParserError
|
|
51
|
+
raise Errors.malformed_json(hint: BODY_HINT)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A {Result}. `command` is the policy verb ({Executor::VERBS}); `name` the wire name;
|
|
55
|
+
# `handler` answers in place of the registered one.
|
|
56
|
+
def call_wire(command:, args:, identity:, name:, handler: nil)
|
|
57
|
+
# Refused with the arguments, before the toll.
|
|
58
|
+
timezone = CallerTimezone.from_env(request.env)
|
|
59
|
+
toll!(identity: identity, command: command, name: name, body: args)
|
|
60
|
+
|
|
61
|
+
CurrentRequest.with(identity: identity, timezone: timezone) do
|
|
62
|
+
Executor.new(connection: connection_for(identity), identity: identity)
|
|
63
|
+
.call(kind: command, args: args, name: name, handler: handler)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def toll!(identity:, command:, name:, body:)
|
|
68
|
+
pow = PowGate.proofs_from_header(request.get_header('HTTP_KIOSK_POW'))
|
|
69
|
+
|
|
70
|
+
# A malformed proof is a 400, not a silent fresh 402.
|
|
71
|
+
RequestValidation.validate_proofs!(pow) if Kiosk.configuration.validate_requests && pow
|
|
72
|
+
|
|
73
|
+
PowGate.gate(
|
|
74
|
+
identity: identity, command: command, method: request.request_method,
|
|
75
|
+
verb: name, body: body, pow: pow
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A lease: the GUCs and `pay`'s three transactions need one connection per request.
|
|
80
|
+
def connection_for(_identity)
|
|
81
|
+
::ActiveRecord::Base.lease_connection
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|