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 +7 -0
- data/LICENSE +21 -0
- data/README.md +18 -0
- data/lib/vallkey/auto.rb +5 -0
- data/lib/vallkey/patch.rb +200 -0
- data/lib/vallkey/version.rb +3 -0
- data/lib/vallkey.rb +13 -0
- metadata +47 -0
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) |
|
data/lib/vallkey/auto.rb
ADDED
|
@@ -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
|
data/lib/vallkey.rb
ADDED
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: []
|