Module: KnoxCall::InterceptResolver
- Defined in:
- lib/knoxcall/intercept_resolver.rb
Overview
The route-aware interception decision table — pure, no I/O (docs/internal/sdk-wrapping/route-aware-interception-plan.md §2.2, PARITY §21.1). One request in, one decision out: send it DIRECT (untouched), through a ROUTE (the manifest says an intercept-enabled Route covers this host + path; the Route injects the stored secret), or through the EPHEMERAL proxy (the caller listed the host, no Route covers it; the SDK's own credential is lifted out-of-band). The order of the rules is the feature.
Every SDK's resolver passes the SAME fixtures — sdk/fixtures/intercept- resolver.json (spec/intercept_resolver_spec.rb runs them here) — so this is the Ruby copy of a contract whose reference is the Node SDK's src/intercept-resolver.ts, not a private heuristic.
Defined Under Namespace
Classes: Decision
Constant Summary collapse
- MODES =
%i[direct route ephemeral].freeze
- REASONS =
%i[kill_switch unparseable own_host route_around outside_context manifest no_base_path_match no_route unlisted].freeze
Class Method Summary collapse
-
.entries_for_host(manifest, host) ⇒ Object
Manifest entries for a host in the order the server sorts them — longest base_path first, then base_path, then slug — so the first entry whose base covers the path is the longest-prefix, lowest-slug match.
-
.entry_value(entry, key) ⇒ Object
Read a manifest-entry field tolerating string and symbol keys (the parsed manifest is string-keyed; a hand-built one may not be).
-
.kill_switch? ⇒ Boolean
KNOXCALL_INTERCEPT=off (or 0 / false): every interceptor and route-aware transport becomes pass-through, per request, with no deploy.
-
.platform_host?(host) ⇒ Boolean
KnoxCall's own domains are never intercepted, whatever a manifest or a host list says (anti-recursion).
-
.rebase_path(request_path, base_path) ⇒ Object
The request path with the route's base prefix removed (leading slash kept), or
nilwhen the request is not under the base. -
.resolve(url:, method:, hosts:, manifest:, own_hosts:, route_around:, kill_switch: false, require_context: false, in_context: false) ⇒ Decision
Apply the decision table.
Class Method Details
.entries_for_host(manifest, host) ⇒ Object
Manifest entries for a host in the order the server sorts them — longest base_path first, then base_path, then slug — so the first entry whose base covers the path is the longest-prefix, lowest-slug match.
73 74 75 76 77 78 79 |
# File 'lib/knoxcall/intercept_resolver.rb', line 73 def entries_for_host(manifest, host) return [] if manifest.nil? routes = entry_value(manifest, :routes) || [] routes.select { |e| WrapTransport.normalize_host(entry_value(e, :host)) == host } .sort_by { |e| bp = entry_value(e, :base_path).to_s; [-bp.length, bp, entry_value(e, :slug).to_s] } end |
.entry_value(entry, key) ⇒ Object
Read a manifest-entry field tolerating string and symbol keys (the parsed manifest is string-keyed; a hand-built one may not be).
48 49 50 51 52 |
# File 'lib/knoxcall/intercept_resolver.rb', line 48 def entry_value(entry, key) return nil unless entry.is_a?(Hash) entry.key?(key.to_s) ? entry[key.to_s] : entry[key.to_sym] end |
.kill_switch? ⇒ Boolean
KNOXCALL_INTERCEPT=off (or 0 / false): every interceptor and route-aware transport becomes pass-through, per request, with no deploy.
36 37 38 |
# File 'lib/knoxcall/intercept_resolver.rb', line 36 def kill_switch? %w[off 0 false].include?(ENV.fetch("KNOXCALL_INTERCEPT", "").strip.downcase) end |
.platform_host?(host) ⇒ Boolean
KnoxCall's own domains are never intercepted, whatever a manifest or a host list says (anti-recursion).
42 43 44 |
# File 'lib/knoxcall/intercept_resolver.rb', line 42 def platform_host?(host) host == "knoxcall.com" || host.end_with?(".knoxcall.com") end |
.rebase_path(request_path, base_path) ⇒ Object
The request path with the route's base prefix removed (leading slash
kept), or nil when the request is not under the base. "/crm/v3" covers
"/crm/v3" and "/crm/v3/x", never "/crm/v30" — the boundary is a path
segment. Mirrors the server's rebasePath (src/lib/route-target-host.ts).
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/knoxcall/intercept_resolver.rb', line 58 def rebase_path(request_path, base_path) req_path = request_path.to_s.empty? ? "/" : request_path.to_s if base_path.nil? || base_path == "/" || base_path == "" return req_path.start_with?("/") ? req_path : "/#{req_path}" end return "/" if req_path == base_path return nil unless req_path.start_with?("#{base_path}/") rest = req_path[base_path.length..] rest.empty? ? "/" : rest end |
.resolve(url:, method:, hosts:, manifest:, own_hosts:, route_around:, kill_switch: false, require_context: false, in_context: false) ⇒ Decision
Apply the decision table. First match wins.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/knoxcall/intercept_resolver.rb', line 94 def resolve(url:, method:, hosts:, manifest:, own_hosts:, route_around:, kill_switch: false, require_context: false, in_context: false) _ = method return Decision.new(mode: :direct, reason: :kill_switch, host: "") if kill_switch u = begin URI.parse(url.to_s) rescue URI::InvalidURIError nil end unless u && %w[http https].include?(u.scheme.to_s.downcase) return Decision.new(mode: :direct, reason: :unparseable, host: "") end host = WrapTransport.normalize_host(u.host) return Decision.new(mode: :direct, reason: :unparseable, host: "") if host.empty? if platform_host?(host) || own_hosts.include?(host) return Decision.new(mode: :direct, reason: :own_host, host: host) end around = WrapTransport.match_route_around(url.to_s, route_around) if around return Decision.new(mode: :direct, reason: :route_around, host: host, route_around_reason: WrapTransport.rule_value(around, :reason)) end return Decision.new(mode: :direct, reason: :outside_context, host: host) if require_context && !in_context entries = entries_for_host(manifest, host) entries.each do |entry| rebased = rebase_path(u.path, entry_value(entry, :base_path)) next if rebased.nil? path = u.query ? "#{rebased}?#{u.query}" : rebased return Decision.new(mode: :route, reason: :manifest, host: host, slug: entry_value(entry, :slug), path: path, entry: entry) end listed = hosts == :all || hosts.include?(host) if listed return Decision.new(mode: :ephemeral, reason: entries.empty? ? :no_route : :no_base_path_match, host: host) end Decision.new(mode: :direct, reason: :unlisted, host: host) end |