coolhand 0.5.1 → 0.6.0
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/AGENTS.harness.md +139 -0
- data/CHANGELOG.md +31 -0
- data/README.md +30 -1
- data/docs/configuration.md +40 -0
- data/docs/openai.md +31 -0
- data/docs/template-search.md +218 -0
- data/lib/coolhand/api_service.rb +49 -15
- data/lib/coolhand/base_interceptor.rb +27 -17
- data/lib/coolhand/configuration.rb +37 -4
- data/lib/coolhand/default_exclude_api_patterns.yml +3 -2
- data/lib/coolhand/default_intercept_addresses.yml +10 -5
- data/lib/coolhand/default_intercept_path_patterns.yml +12 -0
- data/lib/coolhand/errors.rb +19 -0
- data/lib/coolhand/logger_service.rb +1 -1
- data/lib/coolhand/net_http_interceptor.rb +127 -20
- data/lib/coolhand/open_ai/batch_result_processor.rb +1 -1
- data/lib/coolhand/open_ai/webhook_id_store.rb +45 -0
- data/lib/coolhand/open_ai/webhook_validator.rb +44 -10
- data/lib/coolhand/pagination.rb +82 -0
- data/lib/coolhand/read_requests.rb +77 -0
- data/lib/coolhand/template_service.rb +76 -0
- data/lib/coolhand/version.rb +1 -1
- data/lib/coolhand.rb +9 -4
- metadata +10 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
|
|
5
|
+
require_relative "webhook_id_store"
|
|
6
|
+
|
|
5
7
|
module Coolhand
|
|
6
8
|
module OpenAi
|
|
7
9
|
class WebhookValidator
|
|
@@ -23,7 +25,7 @@ module Coolhand
|
|
|
23
25
|
secret_bytes = extract_secret_bytes
|
|
24
26
|
webhook_signature, webhook_timestamp, webhook_id = extract_webhook_headers
|
|
25
27
|
|
|
26
|
-
return validate_headers_in_non_production_env unless webhook_signature && webhook_timestamp
|
|
28
|
+
return validate_headers_in_non_production_env unless webhook_signature && webhook_timestamp && webhook_id
|
|
27
29
|
|
|
28
30
|
verify_signature(webhook_signature, webhook_timestamp, webhook_id, secret_bytes)
|
|
29
31
|
end
|
|
@@ -40,7 +42,8 @@ module Coolhand
|
|
|
40
42
|
return true if @payload
|
|
41
43
|
|
|
42
44
|
if should_enforce_strict_validation?
|
|
43
|
-
@errors << "Empty webhook payload - rejecting webhook
|
|
45
|
+
@errors << "Empty webhook payload - rejecting webhook (Rails.env=#{Rails.env.inspect} " \
|
|
46
|
+
"not in development/test allowlist)"
|
|
44
47
|
Rails.logger.error(@errors.last)
|
|
45
48
|
false
|
|
46
49
|
else
|
|
@@ -51,7 +54,8 @@ module Coolhand
|
|
|
51
54
|
|
|
52
55
|
def validate_in_non_production_env
|
|
53
56
|
if should_enforce_strict_validation?
|
|
54
|
-
@errors << "OpenAI webhook secret not configured - rejecting webhook
|
|
57
|
+
@errors << "OpenAI webhook secret not configured - rejecting webhook (Rails.env=#{Rails.env.inspect} " \
|
|
58
|
+
"not in development/test allowlist)"
|
|
55
59
|
Rails.logger.error(@errors.last)
|
|
56
60
|
false
|
|
57
61
|
else
|
|
@@ -80,8 +84,8 @@ module Coolhand
|
|
|
80
84
|
|
|
81
85
|
def validate_headers_in_non_production_env
|
|
82
86
|
if should_enforce_strict_validation?
|
|
83
|
-
@errors << "Missing OpenAI webhook signature or
|
|
84
|
-
"rejecting webhook in
|
|
87
|
+
@errors << "Missing OpenAI webhook signature, timestamp, or id headers - " \
|
|
88
|
+
"rejecting webhook (Rails.env=#{Rails.env.inspect} not in development/test allowlist)"
|
|
85
89
|
Rails.logger.error(@errors.last)
|
|
86
90
|
false
|
|
87
91
|
else
|
|
@@ -96,13 +100,43 @@ module Coolhand
|
|
|
96
100
|
|
|
97
101
|
signature_valid = webhook_signature.start_with?("v1,") &&
|
|
98
102
|
secure_compare(webhook_signature[3..], expected_signature)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
else
|
|
103
|
+
|
|
104
|
+
unless signature_valid
|
|
102
105
|
@errors << "OpenAI webhook signature verification failed"
|
|
103
106
|
Rails.logger.error(@errors.last)
|
|
104
|
-
false
|
|
107
|
+
return false
|
|
105
108
|
end
|
|
109
|
+
|
|
110
|
+
return false unless timestamp_fresh?(webhook_timestamp)
|
|
111
|
+
return false unless webhook_id_unused?(webhook_id)
|
|
112
|
+
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Checked only after the signature is confirmed valid, so an
|
|
117
|
+
# unsigned/forged request can't poison the id-dedup store (or fail a
|
|
118
|
+
# freshness check) and DoS a later legitimate webhook with the same id.
|
|
119
|
+
def timestamp_fresh?(webhook_timestamp)
|
|
120
|
+
tolerance = Coolhand.configuration.webhook_replay_tolerance_seconds
|
|
121
|
+
age = (Time.now.to_i - webhook_timestamp.to_i).abs
|
|
122
|
+
return true if age <= tolerance
|
|
123
|
+
|
|
124
|
+
@errors << "OpenAI webhook timestamp outside replay-protection tolerance window"
|
|
125
|
+
Rails.logger.error(@errors.last)
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def webhook_id_unused?(webhook_id)
|
|
130
|
+
tolerance = Coolhand.configuration.webhook_replay_tolerance_seconds
|
|
131
|
+
return true if id_store.claim!(webhook_id, tolerance)
|
|
132
|
+
|
|
133
|
+
@errors << "OpenAI webhook id already processed (replay protection)"
|
|
134
|
+
Rails.logger.error(@errors.last)
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def id_store
|
|
139
|
+
Coolhand.configuration.webhook_id_store
|
|
106
140
|
end
|
|
107
141
|
|
|
108
142
|
def secure_compare(a, b)
|
|
@@ -124,7 +158,7 @@ module Coolhand
|
|
|
124
158
|
end
|
|
125
159
|
|
|
126
160
|
def should_enforce_strict_validation?
|
|
127
|
-
[
|
|
161
|
+
!%w[development test].include?(Rails.env)
|
|
128
162
|
end
|
|
129
163
|
end
|
|
130
164
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coolhand
|
|
4
|
+
# Paging state for a v2 list endpoint. These send a bare JSON array and carry paging in the
|
|
5
|
+
# `X-Page`, `X-Per-Page`, `X-Total-Count` and `X-Total-Pages` headers, never in the body.
|
|
6
|
+
Pagination = Struct.new(
|
|
7
|
+
:current_page,
|
|
8
|
+
:per_page,
|
|
9
|
+
:total_count,
|
|
10
|
+
:total_pages,
|
|
11
|
+
:has_next_page,
|
|
12
|
+
:has_prev_page,
|
|
13
|
+
keyword_init: true
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class Pagination
|
|
17
|
+
# Mirrors of the v2 controllers' values, used only to fill a header the server did not send.
|
|
18
|
+
DEFAULT_PER_PAGE = 25
|
|
19
|
+
MAX_PER_PAGE = 100
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def from_headers(response, items:, page: nil, per: nil)
|
|
23
|
+
requested_page = positive_int(page) || 1
|
|
24
|
+
requested_per = [positive_int(per) || DEFAULT_PER_PAGE, MAX_PER_PAGE].min
|
|
25
|
+
|
|
26
|
+
current_page = header_int(response, "X-Page") || requested_page
|
|
27
|
+
per_page = header_int(response, "X-Per-Page") || requested_per
|
|
28
|
+
reported_total_pages = header_int(response, "X-Total-Pages")
|
|
29
|
+
total_count = header_int(response, "X-Total-Count") || fallback_total_count(current_page, per_page, items)
|
|
30
|
+
total_pages = reported_total_pages || fallback_total_pages(total_count, per_page)
|
|
31
|
+
|
|
32
|
+
new(
|
|
33
|
+
current_page: current_page,
|
|
34
|
+
per_page: per_page,
|
|
35
|
+
total_count: total_count,
|
|
36
|
+
total_pages: total_pages,
|
|
37
|
+
has_next_page: next_page?(reported_total_pages, current_page, per_page, items),
|
|
38
|
+
has_prev_page: current_page > 1
|
|
39
|
+
).freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Falling back to the computed totals here would report "no next page" for a full page, and
|
|
45
|
+
# silently truncate a caller's loop.
|
|
46
|
+
def next_page?(reported_total_pages, current_page, per_page, items)
|
|
47
|
+
return current_page < reported_total_pages if reported_total_pages
|
|
48
|
+
return false unless per_page.positive?
|
|
49
|
+
|
|
50
|
+
items.size >= per_page
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A lower bound, not a count: every earlier page assumed full, plus this page.
|
|
54
|
+
def fallback_total_count(current_page, per_page, items)
|
|
55
|
+
return items.size unless per_page.positive?
|
|
56
|
+
|
|
57
|
+
[((current_page - 1) * per_page) + items.size, items.size].max
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fallback_total_pages(total_count, per_page)
|
|
61
|
+
return total_count.positive? ? 1 : 0 unless per_page.positive?
|
|
62
|
+
|
|
63
|
+
(total_count.to_f / per_page).ceil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Neither `Integer()` nor `to_i` is safe alone: the first raises on `""`, the second turns
|
|
67
|
+
# `"3.5"` into `3` and `"nonsense"` into `0` — a fabricated, legitimate-looking count.
|
|
68
|
+
def header_int(response, name)
|
|
69
|
+
raw = response[name]
|
|
70
|
+
return nil if raw.nil?
|
|
71
|
+
|
|
72
|
+
value = raw.strip
|
|
73
|
+
value.match?(/\A\d+\z/) ? value.to_i : nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def positive_int(value)
|
|
77
|
+
integer = Integer(value, exception: false)
|
|
78
|
+
integer&.positive? ? integer : nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Coolhand
|
|
9
|
+
# The GET half of {ApiService}, split out to keep that class inside this repo's 200-line budget.
|
|
10
|
+
#
|
|
11
|
+
# Reads raise where writes log-and-return-nil, on purpose: a write is instrumentation inline in
|
|
12
|
+
# the host app's request, while a read's caller must tell a 404 from a timeout from an empty result.
|
|
13
|
+
module ReadRequests
|
|
14
|
+
# 60s is deliberate, not a slip: writes allow 5, but the server bounds each *statement* at 10s
|
|
15
|
+
# and one response runs several. A tighter read timeout pre-empts the 504 callers should retry.
|
|
16
|
+
READ_OPEN_TIMEOUT = 5
|
|
17
|
+
READ_TIMEOUT = 60
|
|
18
|
+
|
|
19
|
+
# ERROR_BODY_LIMIT caps the message; this caps the body the exception object itself carries.
|
|
20
|
+
RETAINED_ERROR_BODY_LIMIT = 8_000
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def get_json(url, noun)
|
|
25
|
+
body, = get_json_with_headers(url, noun)
|
|
26
|
+
body
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Also returns the response, for endpoints that carry pagination in headers rather than the body.
|
|
30
|
+
def get_json_with_headers(url, noun)
|
|
31
|
+
raise Error, "#{noun} request failed: an API key is required" unless Coolhand.required_field?(api_key)
|
|
32
|
+
|
|
33
|
+
response = perform_get(url, noun)
|
|
34
|
+
|
|
35
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
36
|
+
raise HttpError.new(
|
|
37
|
+
"#{noun} request failed (#{response.code}): #{format_error_body(response.body)}",
|
|
38
|
+
status: response.code.to_i,
|
|
39
|
+
body: retained_error_body(response.body)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[parse_json_body(response.body, noun), response]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def perform_get(url, noun)
|
|
49
|
+
uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
|
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
51
|
+
http.use_ssl = (uri.scheme == "https")
|
|
52
|
+
http.open_timeout = READ_OPEN_TIMEOUT
|
|
53
|
+
http.read_timeout = READ_TIMEOUT
|
|
54
|
+
|
|
55
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
56
|
+
apply_headers(request, "Accept" => "application/json", "X-API-Key" => api_key)
|
|
57
|
+
|
|
58
|
+
# Net::HTTP does not follow redirects, so a 3xx raises rather than replaying the API key at
|
|
59
|
+
# an unapproved host. without_capture is the same recursion guard send_request uses.
|
|
60
|
+
Coolhand.without_capture { http.request(request) }
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
raise Error, "#{noun} request failed: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def retained_error_body(body)
|
|
66
|
+
return body if body.nil? || body.length <= RETAINED_ERROR_BODY_LIMIT
|
|
67
|
+
|
|
68
|
+
"#{body[0, RETAINED_ERROR_BODY_LIMIT]}... [truncated]"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_json_body(body, noun)
|
|
72
|
+
JSON.parse(body.to_s, symbolize_names: true)
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
raise Error, "#{noun} response was not valid JSON: #{format_error_body(body)}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "api_service"
|
|
5
|
+
require_relative "pagination"
|
|
6
|
+
|
|
7
|
+
module Coolhand
|
|
8
|
+
# Rows stay plain Symbol-keyed Hashes so a field the server adds later is not dropped in transit.
|
|
9
|
+
TemplateSearchResult = Struct.new(:templates, :pagination, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# Read-only. Requires the client's **private** key — the public key is write-only here and is
|
|
12
|
+
# rejected like an invalid one.
|
|
13
|
+
#
|
|
14
|
+
# Not a port of the MCP `search_templates` tool and does not agree with its `log_count`.
|
|
15
|
+
# See docs/template-search.md.
|
|
16
|
+
class TemplateService < ApiService
|
|
17
|
+
ERROR_NOUN = "Template"
|
|
18
|
+
BLANK_ID_MESSAGE = "get_template: id must be a non-empty template hashid"
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
super("v2/llm_request_templates")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Filters, error semantics and server behaviour: docs/template-search.md.
|
|
25
|
+
def search_templates(search: nil, workload_id: nil, status: nil, include_deprecated: nil,
|
|
26
|
+
include_system: nil, page: nil, per: nil)
|
|
27
|
+
query = {
|
|
28
|
+
search: search,
|
|
29
|
+
workload_id: workload_id,
|
|
30
|
+
status: status,
|
|
31
|
+
include_deprecated: include_deprecated,
|
|
32
|
+
include_system: include_system,
|
|
33
|
+
page: page,
|
|
34
|
+
per: per
|
|
35
|
+
}.compact
|
|
36
|
+
|
|
37
|
+
templates, response = get_json_with_headers(list_url(query), ERROR_NOUN)
|
|
38
|
+
raise Error, "#{ERROR_NOUN} response was not a JSON array" unless templates.is_a?(Array)
|
|
39
|
+
|
|
40
|
+
TemplateSearchResult.new(
|
|
41
|
+
templates: templates,
|
|
42
|
+
pagination: Pagination.from_headers(response, items: templates, page: page, per: per)
|
|
43
|
+
).freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Adds `user_prompt_pattern` / `system_prompt_pattern`, which the list omits, and unlike the
|
|
47
|
+
# list reaches deprecated and system templates by id with no opt-in flag.
|
|
48
|
+
def get_template(id)
|
|
49
|
+
get_json(resource_url(id), ERROR_NOUN)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def list_url(query)
|
|
55
|
+
uri = URI.parse(api_endpoint)
|
|
56
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
57
|
+
uri
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resource_url(id)
|
|
61
|
+
raise Error, BLANK_ID_MESSAGE unless id.is_a?(String)
|
|
62
|
+
|
|
63
|
+
trimmed = id.strip
|
|
64
|
+
# A blank id resolves to the index route (bare array, not one template); a bare dot segment
|
|
65
|
+
# retargets the request at another path. Neither would 404, so both are rejected here.
|
|
66
|
+
raise Error, BLANK_ID_MESSAGE if trimmed.empty? || [".", ".."].include?(trimmed)
|
|
67
|
+
|
|
68
|
+
URI.parse("#{api_endpoint}/#{escape_path_segment(trimmed)}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Escapes to RFC 3986 unreserved, so an id carrying `/`, `?` or `#` cannot retarget the request.
|
|
72
|
+
def escape_path_segment(value)
|
|
73
|
+
URI::DEFAULT_PARSER.escape(value, /[^A-Za-z0-9\-._~]/)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/coolhand/version.rb
CHANGED
data/lib/coolhand.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "json"
|
|
|
6
6
|
require "base64"
|
|
7
7
|
require "time"
|
|
8
8
|
|
|
9
|
+
require_relative "coolhand/errors"
|
|
9
10
|
require_relative "coolhand/version"
|
|
10
11
|
require_relative "coolhand/configuration"
|
|
11
12
|
require_relative "coolhand/collector"
|
|
@@ -14,13 +15,12 @@ require_relative "coolhand/net_http_interceptor"
|
|
|
14
15
|
require_relative "coolhand/api_service"
|
|
15
16
|
require_relative "coolhand/logger_service"
|
|
16
17
|
require_relative "coolhand/feedback_service"
|
|
18
|
+
require_relative "coolhand/template_service"
|
|
17
19
|
require_relative "coolhand/webhook_interceptor"
|
|
18
20
|
|
|
19
21
|
# The main module for the Coolhand gem.
|
|
20
22
|
# It provides the configuration interface and initializes the patching.
|
|
21
23
|
module Coolhand
|
|
22
|
-
class Error < StandardError; end
|
|
23
|
-
|
|
24
24
|
# Class-level instance variables to hold the configuration
|
|
25
25
|
@configuration = Configuration.new
|
|
26
26
|
|
|
@@ -62,12 +62,11 @@ module Coolhand
|
|
|
62
62
|
|
|
63
63
|
return yield unless configuration.enabled
|
|
64
64
|
|
|
65
|
-
patched = NetHttpInterceptor.patched?
|
|
66
65
|
NetHttpInterceptor.patch!
|
|
67
66
|
begin
|
|
68
67
|
yield
|
|
69
68
|
ensure
|
|
70
|
-
NetHttpInterceptor.unpatch!
|
|
69
|
+
NetHttpInterceptor.unpatch!
|
|
71
70
|
end
|
|
72
71
|
end
|
|
73
72
|
|
|
@@ -104,6 +103,12 @@ module Coolhand
|
|
|
104
103
|
LoggerService.new
|
|
105
104
|
end
|
|
106
105
|
|
|
106
|
+
# Creates a new TemplateService instance, for reading LLM request templates back out of
|
|
107
|
+
# Coolhand. Needs the private API key - the public key is write-only on this API.
|
|
108
|
+
def template_service
|
|
109
|
+
TemplateService.new
|
|
110
|
+
end
|
|
111
|
+
|
|
107
112
|
def required_field?(value)
|
|
108
113
|
return false if value.nil?
|
|
109
114
|
return false if value.respond_to?(:empty?) && value.empty?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coolhand
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Carroll
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: base64
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- ".rspec"
|
|
40
40
|
- ".rubocop.yml"
|
|
41
41
|
- ".simplecov"
|
|
42
|
+
- AGENTS.harness.md
|
|
42
43
|
- CHANGELOG.md
|
|
43
44
|
- LICENSE
|
|
44
45
|
- README.md
|
|
@@ -49,6 +50,7 @@ files:
|
|
|
49
50
|
- docs/elevenlabs.md
|
|
50
51
|
- docs/feedback.md
|
|
51
52
|
- docs/openai.md
|
|
53
|
+
- docs/template-search.md
|
|
52
54
|
- docs/vertex.md
|
|
53
55
|
- lib/coolhand.rb
|
|
54
56
|
- lib/coolhand/api_service.rb
|
|
@@ -57,11 +59,17 @@ files:
|
|
|
57
59
|
- lib/coolhand/configuration.rb
|
|
58
60
|
- lib/coolhand/default_exclude_api_patterns.yml
|
|
59
61
|
- lib/coolhand/default_intercept_addresses.yml
|
|
62
|
+
- lib/coolhand/default_intercept_path_patterns.yml
|
|
63
|
+
- lib/coolhand/errors.rb
|
|
60
64
|
- lib/coolhand/feedback_service.rb
|
|
61
65
|
- lib/coolhand/logger_service.rb
|
|
62
66
|
- lib/coolhand/net_http_interceptor.rb
|
|
63
67
|
- lib/coolhand/open_ai/batch_result_processor.rb
|
|
68
|
+
- lib/coolhand/open_ai/webhook_id_store.rb
|
|
64
69
|
- lib/coolhand/open_ai/webhook_validator.rb
|
|
70
|
+
- lib/coolhand/pagination.rb
|
|
71
|
+
- lib/coolhand/read_requests.rb
|
|
72
|
+
- lib/coolhand/template_service.rb
|
|
65
73
|
- lib/coolhand/version.rb
|
|
66
74
|
- lib/coolhand/vertex/batch_result_processor.rb
|
|
67
75
|
- lib/coolhand/webhook_interceptor.rb
|