vallkey 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 074654bc2f673408657502e765d210f70111952010045420dc5ebc9e9145e8f6
4
+ data.tar.gz: 8afe3aa50b9cfb716e50479bd512f8434350469c4fb31a535b4bf71e9d05115a
5
+ SHA512:
6
+ metadata.gz: a26f6c8a1f422ff6dd939c9019b44f6f35d85711edfb386a2750031335b62d9ecd4c1f24978a688843faea2dcbac7bbd290f5355256cdb0de56ae49f6b8732c9
7
+ data.tar.gz: 45ef6f57c399b065d32faa0699520a55bc22f5dbc6c7c64d3f5ed94dbe2e69896030700649fc86c9e1b1ac91cc3a4cfa6d7156476de436b8806e88926caaec6f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vallkey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # vallkey (Ruby)
2
+
3
+ Send any Ruby SDK through [Vallkey](https://vallkey.com) with no code changes. Requests that carry a ghost key go to Vallkey, which swaps in the real key under your rules. Every other request is left alone.
4
+
5
+ ```sh
6
+ gem install vallkey
7
+ RUBYOPT=-rvallkey/auto bundle exec rails server
8
+ ```
9
+
10
+ Using `bundle exec`? Add `gem "vallkey"` to your Gemfile too: Bundler only loads the gems it lists.
11
+
12
+ Covers Net::HTTP, which carries almost every Ruby SDK (Stripe, OpenAI, Anthropic, AWS, and Twilio, Octokit and Slack through Faraday).
13
+
14
+ | Setting | What it does |
15
+ |---|---|
16
+ | `VALLKEY_API` | The Vallkey server (default `https://api.vallkey.com`) |
17
+ | `VALLKEY_AUTO=0` | Turns it off |
18
+ | `VALLKEY_DEBUG=1` | Logs each request sent to Vallkey (never the key) |
@@ -0,0 +1,5 @@
1
+ # RUBYOPT=-rvallkey/auto, or require "vallkey/auto": sends requests that carry a ghost
2
+ # key to Vallkey. VALLKEY_AUTO=0 turns it off.
3
+ require "vallkey"
4
+
5
+ Vallkey.install unless ENV["VALLKEY_AUTO"] == "0"
@@ -0,0 +1,200 @@
1
+ # Sends every request that carries a ghost key to Vallkey instead of the provider, with
2
+ # the same path, so any SDK works without setting a base URL. Requests without a ghost
3
+ # key are not touched.
4
+ #
5
+ # Covers Net::HTTP, which carries almost every Ruby SDK: on its own (Stripe, OpenAI,
6
+ # Anthropic, AWS), and under Faraday, HTTParty and rest-client (Twilio, Octokit, Slack).
7
+ # Net::HTTP is patched once the app loads it, never loaded from here: under Bundler,
8
+ # loading it before the app could pick another version than the app's Gemfile.lock.
9
+ #
10
+ # Each request sent to Vallkey says in x-vallkey-host which address it was for, and in
11
+ # x-vallkey-hook what sent it: this hook, Ruby's version, the platform and VALLKEY_ENV, so
12
+ # the dashboard can show each place a key is used. Never app names or secrets.
13
+ #
14
+ # VALLKEY_API the Vallkey server (default https://api.vallkey.com)
15
+ # VALLKEY_DEBUG=1 logs each request sent to Vallkey (never the key)
16
+ # VALLKEY_ENV a name for this place (production, staging...), shown in the dashboard
17
+ module Vallkey
18
+ module Patch
19
+ GHOST = /gk_[a-z]{2,20}_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{43}/.freeze
20
+ HOOK_HEADER = "x-vallkey-host".freeze
21
+ WHO_HEADER = "x-vallkey-hook".freeze
22
+
23
+ # The platform, from variables each one sets itself. The same list, in the same order,
24
+ # is in every Vallkey hook, so the dashboard names a place the same way in any language.
25
+ PLATFORMS = [
26
+ %w[VERCEL vercel], %w[AWS_LAMBDA_FUNCTION_NAME aws-lambda], %w[ECS_CONTAINER_METADATA_URI_V4 aws-ecs],
27
+ %w[ECS_CONTAINER_METADATA_URI aws-ecs], %w[FUNCTION_TARGET cloud-functions], %w[K_SERVICE cloud-run],
28
+ %w[GAE_APPLICATION app-engine], %w[FUNCTIONS_WORKER_RUNTIME azure-functions], %w[CONTAINER_APP_NAME azure-container-apps],
29
+ %w[WEBSITE_SITE_NAME azure-app-service], %w[RAILWAY_PROJECT_ID railway], %w[RENDER render], %w[FLY_APP_NAME fly],
30
+ %w[DYNO heroku], %w[NETLIFY netlify], %w[KOYEB_APP_NAME koyeb], %w[GITHUB_ACTIONS github-actions],
31
+ %w[GITLAB_CI gitlab-ci], %w[KUBERNETES_SERVICE_HOST kubernetes]
32
+ ].freeze
33
+
34
+ Target = Struct.new(:scheme, :host, :port) do
35
+ def host_with_port
36
+ default = scheme == "https" ? 443 : 80
37
+ port == default ? host : "#{host}:#{port}"
38
+ end
39
+ end
40
+
41
+ class << self
42
+ attr_reader :installed
43
+
44
+ def install
45
+ return if @installed
46
+ @installed = true
47
+ patch_net_http if defined?(::Net::HTTP)
48
+ watch_requires unless @net_http
49
+ end
50
+
51
+ def uninstall
52
+ @installed = false
53
+ end
54
+
55
+ def target
56
+ raw = ENV["VALLKEY_API"].to_s.empty? ? "https://api.vallkey.com" : ENV["VALLKEY_API"]
57
+ m = %r{\A(https?)://([^/:]+)(?::(\d+))?}.match(raw)
58
+ return Target.new("https", "api.vallkey.com", 443) unless m
59
+ Target.new(m[1], m[2], (m[3] || (m[1] == "https" ? 443 : 80)).to_i)
60
+ end
61
+
62
+ # A ghost key in a header value, including inside Basic credentials.
63
+ def in_value?(value)
64
+ text = value.to_s
65
+ return true if GHOST.match?(text)
66
+ basic = /\A\s*basic\s+([A-Za-z0-9+\/=]+)\s*\z/i.match(text)
67
+ return false unless basic
68
+ GHOST.match?(basic[1].unpack1("m").to_s)
69
+ rescue StandardError
70
+ false
71
+ end
72
+
73
+ def ghost?(path, headers, body)
74
+ query = path.to_s.split("?", 2)[1]
75
+ return true if query && GHOST.match?(unescape(query))
76
+ headers.each { |_, value| return true if in_value?(value) }
77
+ body.is_a?(String) && GHOST.match?(body.b)
78
+ end
79
+
80
+ def unescape(text)
81
+ text.gsub(/%([0-9A-Fa-f]{2})/) { Regexp.last_match(1).hex.chr }.gsub("+", " ")
82
+ end
83
+
84
+ def who
85
+ @who ||= begin
86
+ parts = ["ruby/#{Vallkey::VERSION}", "runtime=#{RUBY_VERSION}"]
87
+ platform = PLATFORMS.find { |name, _| !ENV[name].to_s.empty? }&.last
88
+ platform ||= "docker" if File.exist?("/.dockerenv")
89
+ parts << "platform=#{platform}" if platform
90
+ place = ENV["VALLKEY_ENV"].to_s
91
+ parts << "env=#{place}" if place.match?(/\A[A-Za-z0-9._-]{1,40}\z/)
92
+ parts.join("; ").freeze
93
+ end
94
+ end
95
+
96
+ def log(from, to)
97
+ warn "[vallkey] #{from} -> #{to}" if ENV["VALLKEY_DEBUG"] == "1"
98
+ end
99
+
100
+ def patch_net_http
101
+ return if @net_http
102
+ @net_http = true
103
+ ::Net::HTTP.prepend(NetHTTP)
104
+ end
105
+
106
+ # Patches Net::HTTP as soon as anything loads it (require, or an autoload).
107
+ def watch_requires
108
+ return if @watching
109
+ @watching = true
110
+ ::Kernel.module_eval do
111
+ alias_method :vallkey_original_require, :require
112
+ class << self
113
+ alias_method :vallkey_original_require, :require
114
+ end
115
+
116
+ def require(path)
117
+ loaded = vallkey_original_require(path)
118
+ Vallkey::Patch.patch_net_http if loaded && defined?(::Net::HTTP) && Vallkey::Patch.installed
119
+ loaded
120
+ end
121
+ module_function :require
122
+ end
123
+ end
124
+
125
+ # One kept-open connection to Vallkey for each thread, as the app's own one to the
126
+ # provider would be; a forked worker opens its own.
127
+ def connection(target, from)
128
+ cache = (Thread.current[:__vallkey_connections] ||= {})
129
+ key = [Process.pid, target.scheme, target.host, target.port]
130
+ http = cache[key]
131
+ unless http
132
+ http = ::Net::HTTP.new(target.host, target.port)
133
+ http.use_ssl = target.scheme == "https"
134
+ cache[key] = http
135
+ end
136
+ %i[open_timeout read_timeout write_timeout ssl_timeout continue_timeout keep_alive_timeout].each do |name|
137
+ next unless from.respond_to?(name) && http.respond_to?("#{name}=")
138
+ value = from.public_send(name)
139
+ http.public_send("#{name}=", value) unless value.nil?
140
+ end
141
+ http
142
+ end
143
+ end
144
+
145
+ module NetHTTP
146
+ # Most libraries start the connection first and send requests on it after
147
+ # (Faraday, the AWS SDK, connection pools): the connection is only opened when the
148
+ # first request that stays with the provider is sent, so one whose ghost key sends
149
+ # it to Vallkey never opens a connection to the provider at all.
150
+ def request(req, body = nil, &block)
151
+ moved = Vallkey::Patch.installed && Vallkey::Patch.redirect?(self, req, body)
152
+ unless moved
153
+ if @vallkey_deferred
154
+ @vallkey_deferred = false
155
+ connect
156
+ end
157
+ return super
158
+ end
159
+ target, from = moved
160
+ req.delete("host")
161
+ req[HOOK_HEADER] = from
162
+ req[WHO_HEADER] = Vallkey::Patch.who
163
+ req["host"] = target.host_with_port
164
+ http = Vallkey::Patch.connection(target, self)
165
+ http.start unless http.started?
166
+ http.request(req, body, &block)
167
+ end
168
+
169
+ private
170
+
171
+ def do_start
172
+ return super unless Vallkey::Patch.installed
173
+ @vallkey_deferred = true
174
+ @started = true
175
+ end
176
+
177
+ def do_finish
178
+ @vallkey_deferred = false
179
+ super
180
+ end
181
+ end
182
+
183
+ class << self
184
+ def redirect?(http, req, body)
185
+ t = target
186
+ return false if http.address == t.host && http.port == t.port
187
+ headers = []
188
+ req.each_header { |name, value| headers << [name, value] }
189
+ return false unless ghost?(req.path, headers, body || req.body)
190
+ scheme = http.use_ssl? ? "https" : "http"
191
+ default = http.use_ssl? ? 443 : 80
192
+ from = http.port == default ? http.address : "#{http.address}:#{http.port}"
193
+ log("#{scheme}://#{from}#{req.path.to_s.split('?', 2)[0]}", "#{t.scheme}://#{t.host_with_port}")
194
+ [t, from]
195
+ rescue StandardError
196
+ false # anything unusual goes out as it was
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,3 @@
1
+ module Vallkey
2
+ VERSION = "0.1.0"
3
+ end
data/lib/vallkey.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "vallkey/version"
2
+ require "vallkey/patch"
3
+
4
+ # Vallkey for Ruby. Start with RUBYOPT=-rvallkey/auto, or require "vallkey/auto".
5
+ module Vallkey
6
+ def self.install
7
+ Patch.install
8
+ end
9
+
10
+ def self.uninstall
11
+ Patch.uninstall
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vallkey
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vallkey
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Requests that carry a Vallkey ghost key go to Vallkey; nothing else is
13
+ touched. Turn it on with RUBYOPT=-rvallkey/auto.
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - LICENSE
19
+ - README.md
20
+ - lib/vallkey.rb
21
+ - lib/vallkey/auto.rb
22
+ - lib/vallkey/patch.rb
23
+ - lib/vallkey/version.rb
24
+ homepage: https://vallkey.com
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ documentation_uri: https://vallkey.com/docs/
29
+ rubygems_mfa_required: 'true'
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '2.7'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 4.0.11
45
+ specification_version: 4
46
+ summary: Send any Ruby SDK through Vallkey with no code changes.
47
+ test_files: []