seekrit-sdk 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: ba399e476a1d0cf22163313d57429d3e0755a0abe529a0d6753c5aa15b59d5f1
4
+ data.tar.gz: 5a4d6d97926a59c8d69dae239e5de3ad8ff3cba09918cc90c04acfcbc6b5766e
5
+ SHA512:
6
+ metadata.gz: 7392e7806c33b25da0c2667c6fab4bee773d557dfac3865a2cb6cc0e65298e5138ad3bd10996fbfc9f423303d74447d108b3fabe5e762edf6155ac22512ba557
7
+ data.tar.gz: 413ce7dc3f15b63ad2d16e5baac4bdf5fa723f3f3375df590c3922da733302477493f5de8237fa39c7a92d7feef3011059bb9f9b84179a9ab1ef74856fa90fcd
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 seekrit
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,96 @@
1
+ # seekrit — Ruby SDK
2
+
3
+ Read-path SDK for [seekrit](https://seekrit.dev). Authenticate with a service
4
+ token, resolve your environment, and get **decrypted** secrets — the API only
5
+ ever returns ciphertext; decryption happens in your process.
6
+
7
+ > This repo is a **read-only mirror** published from seekrit's monorepo so the
8
+ > code that holds your token and decrypts plaintext is auditable. Don't commit
9
+ > here — it's overwritten on each sync. Issues and PRs welcome.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ gem install seekrit-sdk
15
+ ```
16
+
17
+ Or in a Gemfile:
18
+
19
+ ```ruby
20
+ gem "seekrit-sdk"
21
+ ```
22
+
23
+ Requires Ruby 3.0+. **No runtime dependencies** — uses only the standard
24
+ library (`openssl`, `net/http`, `json`).
25
+
26
+ ## Usage
27
+
28
+ The gem is `seekrit-sdk`; the module is `Seekrit`. Require it as `seekrit/sdk`
29
+ (or `seekrit` — both work):
30
+
31
+ ```ruby
32
+ require "seekrit/sdk"
33
+
34
+ client = Seekrit::Client.new # token from ENV["SEEKRIT_TOKEN"]
35
+ secrets = client.resolve # { "DATABASE_URL" => "postgres://…", … }
36
+
37
+ db_url = client.get("DATABASE_URL")
38
+ api_key = client.get("API_KEY", "")
39
+ ```
40
+
41
+ Load everything into `ENV`:
42
+
43
+ ```ruby
44
+ Seekrit.load_env! # existing ENV vars win by default
45
+ ENV["DATABASE_URL"] # => "postgres://…"
46
+ ```
47
+
48
+ ### Rails
49
+
50
+ The gem ships a Railtie. Opt in from `config/application.rb` or an initializer:
51
+
52
+ ```ruby
53
+ config.seekrit.autoload = true # load $SEEKRIT_TOKEN's secrets into ENV on boot
54
+ config.seekrit.override = false # keep existing ENV vars (default)
55
+ ```
56
+
57
+ Nothing hits the network at boot unless `autoload` is set.
58
+
59
+ ### Configuration
60
+
61
+ | Keyword | Env var | Default |
62
+ | --- | --- | --- |
63
+ | `token:` | `SEEKRIT_TOKEN` | — (required) |
64
+ | `api_url:` | `SEEKRIT_API_URL` | `https://api.seekrit.dev` |
65
+ | `with:` | — | `{}` |
66
+ | `open_timeout:` / `read_timeout:` | — | `10` / `30` (seconds) |
67
+
68
+ A service token binds to a single app environment (plus its composed group
69
+ slices). Pass `with:` to pull a different environment slice of a composed group:
70
+
71
+ ```ruby
72
+ Seekrit::Client.new(with: { "shared" => "dev" }).resolve
73
+ ```
74
+
75
+ ### Errors
76
+
77
+ - `Seekrit::ApiError` — non-2xx from the API; has `#status` and `#code`
78
+ (`"unauthorized"`, `"forbidden"`, `"not_found"`, …).
79
+ - `Seekrit::CryptoError` — a token or ciphertext could not be parsed/decrypted.
80
+ - `Seekrit::Error` — base class (also covers network failures).
81
+
82
+ `#resolve` is **fail-closed**: any resolve or decrypt failure raises rather than
83
+ returning partial results.
84
+
85
+ ## Zero-knowledge
86
+
87
+ `GET /v1/resolve` returns ciphertext plus a data-encryption key wrapped to your
88
+ token's public key. This SDK recovers the token's private key, unwraps the DEK
89
+ (ECDH P-256 → HKDF-SHA256 → AES-256-GCM), and decrypts each secret
90
+ (AES-256-GCM, AAD-bound to `environmentId/NAME`) — the exact scheme used by the
91
+ CLI, `seekrit run`, and every other seekrit client. See
92
+ [seekrit.dev/docs](https://seekrit.dev/docs/concepts/encryption).
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require_relative "crypto"
8
+ require_relative "errors"
9
+
10
+ module Seekrit
11
+ # A read-only seekrit client bound to one service token. A service token
12
+ # selects exactly one app environment (plus its composed group slices).
13
+ #
14
+ # client = Seekrit::Client.new # token from ENV["SEEKRIT_TOKEN"]
15
+ # secrets = client.resolve # { "DATABASE_URL" => "...", ... }
16
+ class Client
17
+ DEFAULT_API_URL = "https://api.seekrit.dev"
18
+
19
+ # @param token [String, nil] skt_... service token; defaults to ENV["SEEKRIT_TOKEN"].
20
+ # @param api_url [String, nil] API base URL; defaults to ENV["SEEKRIT_API_URL"] or the hosted API.
21
+ # @param with [Hash] {group_slug => env_slug} overrides (the ?with= override).
22
+ # @param open_timeout [Numeric] connection timeout, seconds.
23
+ # @param read_timeout [Numeric] read timeout, seconds.
24
+ def initialize(token: nil, api_url: nil, with: {}, open_timeout: 10, read_timeout: 30)
25
+ @token = token || ENV["SEEKRIT_TOKEN"]
26
+ raise Error, "no service token: pass token: or set SEEKRIT_TOKEN" unless @token && !@token.empty?
27
+
28
+ @key = Crypto::TokenKey.parse(@token) # fail fast on a bad token
29
+ @api_url = (api_url || ENV["SEEKRIT_API_URL"] || DEFAULT_API_URL).sub(%r{/+\z}, "")
30
+ @with = with || {}
31
+ @open_timeout = open_timeout
32
+ @read_timeout = read_timeout
33
+ end
34
+
35
+ # Fetch, decrypt, and merge; returns { "NAME" => value }. Fail-closed.
36
+ # @return [Hash{String=>String}]
37
+ def resolve
38
+ Crypto.materialize(fetch, @key)
39
+ end
40
+
41
+ # Resolve and return a single secret's value, or +default+ if absent.
42
+ def get(name, default = nil)
43
+ resolve.fetch(name, default)
44
+ end
45
+
46
+ # Load resolved secrets into +env+ (default ENV). Existing keys are kept
47
+ # unless +override:+ is true. Returns the resolved secrets.
48
+ def into_env(env = ENV, override: false)
49
+ merged = resolve
50
+ merged.each do |name, value|
51
+ env[name] = value if override || !env.key?(name)
52
+ end
53
+ merged
54
+ end
55
+
56
+ private
57
+
58
+ def fetch
59
+ uri = URI("#{@api_url}/v1/resolve")
60
+ unless @with.empty?
61
+ uri.query = URI.encode_www_form(@with.sort.map { |group, env| ["with", "#{group}:#{env}"] })
62
+ end
63
+
64
+ request = Net::HTTP::Get.new(uri)
65
+ request["authorization"] = "Bearer #{@token}"
66
+ request["accept"] = "application/json"
67
+
68
+ response = Net::HTTP.start(
69
+ uri.host, uri.port,
70
+ use_ssl: uri.scheme == "https",
71
+ open_timeout: @open_timeout, read_timeout: @read_timeout
72
+ ) { |http| http.request(request) }
73
+
74
+ status = response.code.to_i
75
+ raise api_error(status, response.body) unless (200..299).cover?(status)
76
+
77
+ JSON.parse(response.body)
78
+ rescue SocketError, Timeout::Error, IOError => e
79
+ raise Error, "resolve request failed: #{e.message}"
80
+ end
81
+
82
+ def api_error(status, body)
83
+ code = "internal"
84
+ message = "HTTP #{status}"
85
+ begin
86
+ error = JSON.parse(body)["error"]
87
+ if error.is_a?(Hash) && error["code"]
88
+ code = error["code"]
89
+ message = error["message"] || message
90
+ end
91
+ rescue JSON::ParserError, TypeError
92
+ # non-JSON body — keep the fallback
93
+ end
94
+ ApiError.new(status, code, message)
95
+ end
96
+ end
97
+
98
+ # Convenience: load a token's secrets straight into ENV.
99
+ # Seekrit.load_env!
100
+ def self.load_env!(**opts)
101
+ override = opts.delete(:override) { false }
102
+ Client.new(**opts).into_env(ENV, override: override)
103
+ end
104
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Seekrit
7
+ # Zero-knowledge read path, byte-compatible with @seekrit/crypto and
8
+ # crates/seekrit-core. Three steps:
9
+ #
10
+ # 1. Recover the service token's P-256 private key (TokenKey.parse).
11
+ # 2. Unwrap each environment DEK (TokenKey#unwrap_dek).
12
+ # 3. Decrypt each secret (Crypto.decrypt_secret).
13
+ #
14
+ # Blob formats (all segments base64url, no padding):
15
+ # token skt_<id>_<pkcs8 private key>
16
+ # wrapped DEK wd1.<ephemeral pub (raw SEC1)>.<hkdf salt>.<iv>.<ciphertext||tag>
17
+ # secret sc1.<iv>.<ciphertext||tag>
18
+ module Crypto
19
+ HKDF_INFO = "seekrit/wrap-dek/v1"
20
+ CURVE = "prime256v1" # NIST P-256 / secp256r1
21
+ # Only the first two underscores are separators; the key segment may contain "_".
22
+ TOKEN_RE = /\A(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)\z/.freeze
23
+
24
+ module_function
25
+
26
+ def b64url_decode(text)
27
+ Base64.urlsafe_decode64(text + ("=" * ((4 - (text.length % 4)) % 4)))
28
+ end
29
+
30
+ def split_blob(blob, prefix, segments)
31
+ parts = blob.split(".")
32
+ unless parts[0] == prefix
33
+ raise CryptoError, %(expected a "#{prefix}" blob, got "#{parts[0]}")
34
+ end
35
+ if parts.length != segments + 1 || parts.any?(&:empty?)
36
+ raise CryptoError, %(malformed "#{prefix}" blob)
37
+ end
38
+
39
+ parts[1..]
40
+ end
41
+
42
+ # AAD binding a secret ciphertext to its environment and name.
43
+ def secret_aad(environment_id, name)
44
+ "#{environment_id}/#{name}"
45
+ end
46
+
47
+ # HKDF-SHA256 (RFC 5869), implemented with HMAC so it works on every Ruby /
48
+ # OpenSSL / LibreSSL build regardless of OpenSSL::KDF.hkdf availability.
49
+ def hkdf_sha256(ikm, salt, info, length)
50
+ prk = OpenSSL::HMAC.digest("SHA256", salt, ikm)
51
+ okm = +""
52
+ block = +""
53
+ counter = 0
54
+ while okm.bytesize < length
55
+ counter += 1
56
+ block = OpenSSL::HMAC.digest("SHA256", prk, block + info + [counter].pack("C"))
57
+ okm << block
58
+ end
59
+ okm[0, length]
60
+ end
61
+
62
+ def aes_gcm_decrypt(key, iv, ciphertext_and_tag, aad)
63
+ raise CryptoError, "ciphertext too short" if ciphertext_and_tag.bytesize < 16
64
+
65
+ tag = ciphertext_and_tag[-16, 16]
66
+ ciphertext = ciphertext_and_tag[0...-16]
67
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
68
+ cipher.decrypt
69
+ cipher.key = key
70
+ cipher.iv = iv
71
+ cipher.auth_tag = tag
72
+ # GCM with no AAD == empty AAD; skip the call so a zero-length AAD never
73
+ # trips broken bindings (e.g. some LibreSSL builds).
74
+ cipher.auth_data = aad unless aad.nil? || aad.empty?
75
+ cipher.update(ciphertext) + cipher.final
76
+ end
77
+
78
+ # The P-256 private key recovered from a skt_... service token.
79
+ class TokenKey
80
+ attr_reader :token_id
81
+
82
+ def initialize(token_id, ec_key)
83
+ @token_id = token_id
84
+ @ec_key = ec_key
85
+ end
86
+
87
+ def self.parse(token)
88
+ match = Crypto::TOKEN_RE.match(token)
89
+ raise CryptoError, "not a valid seekrit service token" unless match
90
+
91
+ der = Crypto.b64url_decode(match[2])
92
+ ec_key = begin
93
+ key = OpenSSL::PKey.read(der)
94
+ raise CryptoError, "service token key is not an EC private key" unless key.is_a?(OpenSSL::PKey::EC)
95
+
96
+ key
97
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
98
+ begin
99
+ OpenSSL::PKey::EC.new(der)
100
+ rescue StandardError
101
+ raise CryptoError, "service token private key is corrupted"
102
+ end
103
+ end
104
+ new(match[1], ec_key)
105
+ end
106
+
107
+ # Recover a 32-byte environment DEK from a wd1. blob.
108
+ def unwrap_dek(wrapped)
109
+ eph_b64, salt_b64, iv_b64, ct_b64 = Crypto.split_blob(wrapped, "wd1", 4)
110
+ eph = Crypto.b64url_decode(eph_b64)
111
+ salt = Crypto.b64url_decode(salt_b64)
112
+ iv = Crypto.b64url_decode(iv_b64)
113
+ ct = Crypto.b64url_decode(ct_b64)
114
+
115
+ group = OpenSSL::PKey::EC::Group.new(Crypto::CURVE)
116
+ peer_point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(eph, 2))
117
+ shared = @ec_key.dh_compute_key(peer_point)
118
+ wrapping_key = Crypto.hkdf_sha256(shared, salt, Crypto::HKDF_INFO, 32)
119
+
120
+ dek = begin
121
+ Crypto.aes_gcm_decrypt(wrapping_key, iv, ct, "")
122
+ rescue OpenSSL::Cipher::CipherError
123
+ raise CryptoError, "DEK unwrap failed: wrong private key or tampered grant"
124
+ end
125
+ raise CryptoError, "unwrapped DEK has wrong length" unless dek.bytesize == 32
126
+
127
+ dek
128
+ end
129
+ end
130
+
131
+ # Decrypt an sc1. secret ciphertext to its UTF-8 plaintext.
132
+ def decrypt_secret(dek, blob, aad)
133
+ iv_b64, ct_b64 = split_blob(blob, "sc1", 2)
134
+ iv = b64url_decode(iv_b64)
135
+ ct = b64url_decode(ct_b64)
136
+ plaintext = begin
137
+ aes_gcm_decrypt(dek, iv, ct, aad)
138
+ rescue OpenSSL::Cipher::CipherError
139
+ raise CryptoError, "secret decryption failed: wrong key, tampered data, or mismatched context"
140
+ end
141
+ plaintext.force_encoding(Encoding::UTF_8)
142
+ end
143
+
144
+ # Decrypt every layer and merge by precedence. Layers arrive lowest
145
+ # precedence first (composed groups, then the app environment); later layers
146
+ # overwrite earlier ones on a name collision.
147
+ def materialize(resolve_response, key)
148
+ merged = {}
149
+ resolve_response["layers"].each do |layer|
150
+ dek = key.unwrap_dek(layer["wrappedDek"])
151
+ env_id = layer["environmentId"]
152
+ layer["secrets"].each do |secret|
153
+ merged[secret["name"]] = decrypt_secret(dek, secret["ciphertext"], secret_aad(env_id, secret["name"]))
154
+ end
155
+ end
156
+ merged
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seekrit
4
+ # Base class for every error raised by this SDK.
5
+ class Error < StandardError; end
6
+
7
+ # A token, wrapped DEK, or secret ciphertext could not be parsed or decrypted.
8
+ # Deliberately unspecific: a decrypt failure does not distinguish "wrong key"
9
+ # from "tampered data" from "mismatched context".
10
+ class CryptoError < Error; end
11
+
12
+ # The resolve API returned a non-2xx response.
13
+ class ApiError < Error
14
+ # @return [Integer] HTTP status code.
15
+ attr_reader :status
16
+ # @return [String] error.code from the API body (e.g. "unauthorized",
17
+ # "forbidden", "not_found"), or "internal" if the body did not parse.
18
+ attr_reader :code
19
+
20
+ def initialize(status, code, message)
21
+ @status = status
22
+ @code = code
23
+ super("#{status} #{code}: #{message}")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client"
4
+
5
+ module Seekrit
6
+ # Optional Rails integration. Loaded automatically when Rails is present
7
+ # (see lib/seekrit.rb). Opt-in: resolved secrets are only loaded into ENV if
8
+ # you enable it, so nothing hits the network at boot unless you ask.
9
+ #
10
+ # # config/application.rb (or an initializer)
11
+ # config.seekrit.autoload = true # load $SEEKRIT_TOKEN's secrets into ENV on boot
12
+ # config.seekrit.override = false # keep existing ENV vars (default)
13
+ #
14
+ # Or call it yourself whenever you like:
15
+ #
16
+ # Seekrit.load_env!
17
+ class Railtie < ::Rails::Railtie
18
+ config.seekrit = ActiveSupport::OrderedOptions.new
19
+ config.seekrit.autoload = false
20
+ config.seekrit.override = false
21
+
22
+ initializer "seekrit.load_env", before: :load_environment_config do |app|
23
+ cfg = app.config.seekrit
24
+ Seekrit.load_env!(override: cfg.override) if cfg.autoload && ENV["SEEKRIT_TOKEN"]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entrypoint matching the `seekrit-sdk` gem name, so `require "seekrit/sdk"`
4
+ # and Bundler's auto-require of `gem "seekrit-sdk"` both work. `require "seekrit"`
5
+ # works too — both load the same code, and the module is `Seekrit`.
6
+ require_relative "../seekrit"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seekrit
4
+ VERSION = "0.1.0"
5
+ end
data/lib/seekrit.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "seekrit/version"
4
+ require_relative "seekrit/errors"
5
+ require_relative "seekrit/crypto"
6
+ require_relative "seekrit/client"
7
+
8
+ # seekrit — read-path SDK for the zero-knowledge secrets manager.
9
+ #
10
+ # require "seekrit"
11
+ #
12
+ # client = Seekrit::Client.new # token from ENV["SEEKRIT_TOKEN"]
13
+ # secrets = client.resolve # { "DATABASE_URL" => "...", ... }
14
+ #
15
+ # Secrets are decrypted in-process; the API only ever sees ciphertext.
16
+ module Seekrit
17
+ # Load the Rails integration only when Rails is present.
18
+ require_relative "seekrit/railtie" if defined?(::Rails::Railtie)
19
+ end
@@ -0,0 +1,65 @@
1
+ {
2
+ "token": "skt_r36AUXMxoX0xkQoH8vimHN_MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1bTaK96XZQzEUpfGe5dlc-sp3n7mF8WpZNVJweEJZ06hRANCAATgUoWLRDgyG0qsUzdQXzr1VSL-p9kSp_zhAULqF2qHUAWDPcuoX-f4JTUX6lyzPEs_0chQ9nKvgmEJUpXx9_q5",
3
+ "tokenId": "skt_r36AUXMxoX0xkQoH8vimHN",
4
+ "publicKeyJwk": "{\"key_ops\":[],\"ext\":true,\"kty\":\"EC\",\"x\":\"4FKFi0Q4MhtKrFM3UF869VUi_qfZEqf84QFC6hdqh1A\",\"y\":\"BYM9y6hf5_glNRfqXLM8Sz_RyFD2cq-CYQlSlfH3-rk\",\"crv\":\"P-256\"}",
5
+ "resolve": {
6
+ "scope": {
7
+ "orgId": "org_test",
8
+ "orgSlug": "acme",
9
+ "appId": "app_test",
10
+ "appSlug": "web",
11
+ "envId": "env_apptest0000000000001",
12
+ "envSlug": "production"
13
+ },
14
+ "layers": [
15
+ {
16
+ "source": "group",
17
+ "environmentId": "env_grouptest0000000001",
18
+ "slug": "production",
19
+ "groupSlug": "shared",
20
+ "wrappedDek": "wd1.BEoGSn3SoICO6tA13EjveZHvxTTRVijE0OUpBkzVkFU28q_jfgY4P9IkyrpjMVJK27zogh61rwC_NUBjoe2TKhY.5EI1On9xCtVuntRHMA6xFA.JNqiqNRxDpNN3hkP.8RxEujnGaEIUIVq9VVcOZlpC07-gxgyz25VCtffNZWSRh8J8iFfMA6p-_ubXSfv5",
21
+ "secrets": [
22
+ {
23
+ "name": "SHARED",
24
+ "ciphertext": "sc1.4XNyaS_ilRPDTlIg.LhAGzqP_cm4fx_dKeST8_3NITle_77x0Qjo"
25
+ },
26
+ {
27
+ "name": "DATABASE_URL",
28
+ "ciphertext": "sc1.-THIwVQA7yVq6SWt.1RaVPddl8QDuhc2wValcmtArCxafHBCAk24Fv00gB_FNc6k"
29
+ },
30
+ {
31
+ "name": "UNICODE",
32
+ "ciphertext": "sc1.7qB3RU8qhHKpqwbB.BsKeZYWDv4Qp9cWIB0I684pLazslk2nFtIq-OvgDjIaTaw3Pbw"
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "source": "app",
38
+ "environmentId": "env_apptest0000000000001",
39
+ "slug": "production",
40
+ "wrappedDek": "wd1.BMywXqAVq3Ee3XX7mGBt2KLcRRwPLQn9FjlXc19Z8HNdDPsg-KyKLwg_uz4tgQ8MdVevMgtjfBkOiqmAi7ENI2Y.FLBNZ3z8-8CnYoBN-Azr9g.2x8wkp6BSw9QjnG2.3b32qenHIHC92eCehRW6QGWNFTlIe3nTKPje0WsRFWSHkjV6PYIxCXmlQaAJJkp3",
41
+ "secrets": [
42
+ {
43
+ "name": "SHARED",
44
+ "ciphertext": "sc1.QF697pJ61KM9Xqj2.HH9PNlW7aIIWZvpLm6yc8ZCRRemlxoOS"
45
+ },
46
+ {
47
+ "name": "API_KEY",
48
+ "ciphertext": "sc1.x4G_KqJP7ags8SBf.obw89lhKqcMAy1vzjdTjch88R7Trx9JtFWS2Rw"
49
+ },
50
+ {
51
+ "name": "EMPTY",
52
+ "ciphertext": "sc1.qdub6o6QejpKH97e.KFe1fiDvtHF3agstKhL5ng"
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ },
58
+ "expectedManagedValues": {
59
+ "SHARED": "from-app",
60
+ "DATABASE_URL": "postgres://group/db",
61
+ "UNICODE": "héllo-🌍-\n-tab\tend",
62
+ "API_KEY": "sk-app-12345",
63
+ "EMPTY": ""
64
+ }
65
+ }
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seekrit-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - seekrit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Read-path SDK for seekrit, the zero-knowledge secrets manager. Authenticate
15
+ with a service token, resolve your environment, and get decrypted secrets —
16
+ the API only ever returns ciphertext; decryption happens in your process.
17
+ Uses only the Ruby standard library (openssl, net/http, json). Optional Rails
18
+ integration included. Require as "seekrit" or "seekrit/sdk"; the module is
19
+ Seekrit.
20
+ email:
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - LICENSE
26
+ - README.md
27
+ - lib/seekrit.rb
28
+ - lib/seekrit/client.rb
29
+ - lib/seekrit/crypto.rb
30
+ - lib/seekrit/errors.rb
31
+ - lib/seekrit/railtie.rb
32
+ - lib/seekrit/sdk.rb
33
+ - lib/seekrit/version.rb
34
+ - testdata/vectors.json
35
+ homepage: https://seekrit.dev
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://seekrit.dev
40
+ documentation_uri: https://seekrit.dev/docs
41
+ source_code_uri: https://github.com/seekritdev/ruby-sdk
42
+ rubygems_mfa_required: 'true'
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.22
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Read-path SDK for seekrit — resolve and decrypt secrets client-side with
62
+ a service token.
63
+ test_files: []