secretspec 0.13.0-x86_64-linux
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/README.md +47 -0
- data/ext/secretspec/extconf.rb +61 -0
- data/ext/secretspec/secretspec_ext.c +64 -0
- data/lib/secretspec.rb +239 -0
- data/vendor/libsecretspec_ffi.a +0 -0
- data/vendor/native-static-libs.txt +1 -0
- data/vendor/secretspec.h +51 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dc73c5ea8c78355e47695fa927734801e216aa8030a5ccc4a6d302b001016395
|
|
4
|
+
data.tar.gz: 22617049b8d9524dbf3ec9b618d288dc35a33ecfc4a50da093c09cf168ceaf7d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 376fe5488f605295ad89700536a691eca4454096a0f6db26b622ac3152447326d8bc9dc87f2f31100558622a1fb4e16fa44f15b718fc119ec99e5a607c456c7a
|
|
7
|
+
data.tar.gz: fbd4e2b1412b451bf4b87ec538b1faa439b8f2e0aee9880332833f98593316e22a6b243fa16ab1d6e608989ec65ac8a0dbb58c8183dd43981c255c036b371828
|
data/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# secretspec (Ruby SDK)
|
|
2
|
+
|
|
3
|
+
Ruby bindings for [SecretSpec](https://secretspec.dev/), a declarative secrets
|
|
4
|
+
manager. A thin client over the `secretspec-ffi` C ABI, statically linked into a
|
|
5
|
+
native C extension at build time (no runtime library to locate). Resolution
|
|
6
|
+
happens in the Rust core, so the SDK inherits every provider with no Ruby-side
|
|
7
|
+
logic.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "secretspec"
|
|
11
|
+
|
|
12
|
+
resolved = Secretspec::SecretSpec.builder
|
|
13
|
+
.with_provider("keyring://")
|
|
14
|
+
.with_profile("production")
|
|
15
|
+
.with_reason("boot web app")
|
|
16
|
+
.load
|
|
17
|
+
|
|
18
|
+
puts resolved.provider, resolved.profile
|
|
19
|
+
db = resolved.secrets["DATABASE_URL"]
|
|
20
|
+
puts db.get # the value, or the file path for as_path secrets
|
|
21
|
+
resolved.set_as_env! # export everything into ENV
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A missing required secret raises `Secretspec::MissingRequiredError`; any other
|
|
25
|
+
failure raises `Secretspec::Error` (with a stable `#kind`).
|
|
26
|
+
|
|
27
|
+
## Cleanup
|
|
28
|
+
|
|
29
|
+
`as_path` secrets are materialized to temp files that outlive the call. Pass a
|
|
30
|
+
block to `load` (which closes automatically) or call `resolved.close` when done
|
|
31
|
+
so the secret files do not accumulate in the temp dir.
|
|
32
|
+
|
|
33
|
+
## Value-free report
|
|
34
|
+
|
|
35
|
+
`report` returns the inventory/preflight view: per-secret status and provenance,
|
|
36
|
+
never a value. Unlike `load`, it does not raise when a required secret is missing
|
|
37
|
+
— it appears as a `SecretReport` with status `"missing_required"`.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
report = Secretspec::SecretSpec.builder.with_profile("production").report
|
|
41
|
+
report.secrets.each { |s| puts [s.name, s.status, s.required].join(" ") }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Library discovery
|
|
45
|
+
|
|
46
|
+
The native library is found via the `SECRETSPEC_FFI_LIB` environment variable,
|
|
47
|
+
or a Cargo `target` directory found by searching up from the working directory.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds the secretspec native extension, statically linking the secretspec-ffi
|
|
4
|
+
# archive (libsecretspec_ffi.a) into the extension object. A Rust staticlib does
|
|
5
|
+
# not carry its own native dependency closure, so the archive's transitive system
|
|
6
|
+
# libs (captured from `rustc --print native-static-libs`, never hardcoded) are
|
|
7
|
+
# appended to the link line after it.
|
|
8
|
+
|
|
9
|
+
require "mkmf"
|
|
10
|
+
|
|
11
|
+
ext_dir = __dir__
|
|
12
|
+
pkg_dir = File.expand_path("../..", ext_dir) # secretspec-rb
|
|
13
|
+
repo_root = File.expand_path("..", pkg_dir) # workspace root (dev checkout)
|
|
14
|
+
vendor = File.join(pkg_dir, "vendor")
|
|
15
|
+
|
|
16
|
+
# The staticlib: explicit contract, the bundled platform-gem copy, or a Cargo
|
|
17
|
+
# target dir (dev checkout, newest of release/debug).
|
|
18
|
+
def find_staticlib(vendor, repo_root)
|
|
19
|
+
env = ENV["SECRETSPEC_FFI_STATICLIB"]
|
|
20
|
+
return env if env && !env.empty? && File.exist?(env)
|
|
21
|
+
|
|
22
|
+
bundled = File.join(vendor, "libsecretspec_ffi.a")
|
|
23
|
+
return bundled if File.exist?(bundled)
|
|
24
|
+
|
|
25
|
+
%w[release debug]
|
|
26
|
+
.map { |p| File.join(repo_root, "target", p, "libsecretspec_ffi.a") }
|
|
27
|
+
.select { |c| File.exist?(c) }
|
|
28
|
+
.max_by { |c| File.mtime(c) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The archive's transitive native deps: explicit contract, the bundled manifest,
|
|
32
|
+
# or captured live from rustc (dev checkout).
|
|
33
|
+
def find_native_libs(vendor, repo_root)
|
|
34
|
+
env = ENV["SECRETSPEC_FFI_NATIVE_LIBS"]
|
|
35
|
+
return env if env && !env.empty?
|
|
36
|
+
|
|
37
|
+
manifest = File.join(vendor, "native-static-libs.txt")
|
|
38
|
+
return File.read(manifest).strip if File.exist?(manifest)
|
|
39
|
+
|
|
40
|
+
note = `cd #{repo_root} && cargo rustc -q -p secretspec-ffi --crate-type staticlib -- --print native-static-libs 2>&1`
|
|
41
|
+
note[/native-static-libs:\s*(.*)/, 1].to_s.strip
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
staticlib = find_staticlib(vendor, repo_root)
|
|
45
|
+
abort("secretspec: could not locate libsecretspec_ffi.a; set SECRETSPEC_FFI_STATICLIB") unless staticlib
|
|
46
|
+
|
|
47
|
+
# Header: the bundled vendor copy (platform gem) or the ffi crate's include dir.
|
|
48
|
+
include_dir =
|
|
49
|
+
if File.exist?(File.join(vendor, "secretspec.h"))
|
|
50
|
+
vendor
|
|
51
|
+
else
|
|
52
|
+
File.join(repo_root, "secretspec-ffi", "include")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
$INCFLAGS << " -I#{include_dir}"
|
|
56
|
+
# $LOCAL_LIBS is emitted before $libs on the link line, so the archive (pulled
|
|
57
|
+
# for the referenced symbols) precedes the system libs it depends on.
|
|
58
|
+
$LOCAL_LIBS << " #{staticlib}"
|
|
59
|
+
$libs = "#{$libs} #{find_native_libs(vendor, repo_root)}"
|
|
60
|
+
|
|
61
|
+
create_makefile("secretspec/secretspec_ext")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Native glue for the secretspec Ruby SDK.
|
|
3
|
+
*
|
|
4
|
+
* A thin C extension that statically links the secretspec-ffi archive
|
|
5
|
+
* (libsecretspec_ffi.a) and exposes its three C ABI functions to Ruby as
|
|
6
|
+
* Secretspec::Native.c_resolve / c_abi_version. The Rust resolver is embedded in
|
|
7
|
+
* this extension object, so there is no separate cdylib to ship or dlopen.
|
|
8
|
+
*/
|
|
9
|
+
#include <ruby.h>
|
|
10
|
+
#include <ruby/thread.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include "secretspec.h"
|
|
13
|
+
|
|
14
|
+
static void *
|
|
15
|
+
resolve_nogvl(void *arg)
|
|
16
|
+
{
|
|
17
|
+
return secretspec_resolve((const char *)arg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Secretspec::Native.c_resolve(request_json) -> String or nil
|
|
22
|
+
*
|
|
23
|
+
* Marshals the JSON request to the Rust resolver and copies the owned response
|
|
24
|
+
* into a Ruby String before freeing it. Returns nil if the resolver returns NULL
|
|
25
|
+
* (catastrophic allocation failure); the Ruby wrapper turns that into an Error.
|
|
26
|
+
*
|
|
27
|
+
* The resolver may block on network-backed providers (1Password, LastPass,
|
|
28
|
+
* Vault), so it runs with the GVL released — otherwise the round-trip would
|
|
29
|
+
* freeze every other Ruby thread. The request bytes are copied into a C-owned
|
|
30
|
+
* buffer first: the Ruby string may move once the GVL is released.
|
|
31
|
+
*/
|
|
32
|
+
static VALUE
|
|
33
|
+
native_resolve(VALUE self, VALUE request_json)
|
|
34
|
+
{
|
|
35
|
+
char *request = strdup(StringValueCStr(request_json));
|
|
36
|
+
if (request == NULL) {
|
|
37
|
+
return Qnil;
|
|
38
|
+
}
|
|
39
|
+
char *result = rb_thread_call_without_gvl(
|
|
40
|
+
resolve_nogvl, request, RUBY_UBF_IO, NULL);
|
|
41
|
+
free(request);
|
|
42
|
+
if (result == NULL) {
|
|
43
|
+
return Qnil;
|
|
44
|
+
}
|
|
45
|
+
VALUE out = rb_str_new_cstr(result);
|
|
46
|
+
secretspec_free(result);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Secretspec::Native.c_abi_version -> String (static, not freed). */
|
|
51
|
+
static VALUE
|
|
52
|
+
native_abi_version(VALUE self)
|
|
53
|
+
{
|
|
54
|
+
return rb_str_new_cstr(secretspec_abi_version());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
void
|
|
58
|
+
Init_secretspec_ext(void)
|
|
59
|
+
{
|
|
60
|
+
VALUE mod = rb_define_module("Secretspec");
|
|
61
|
+
VALUE native = rb_define_module_under(mod, "Native");
|
|
62
|
+
rb_define_singleton_method(native, "c_resolve", native_resolve, 1);
|
|
63
|
+
rb_define_singleton_method(native, "c_abi_version", native_abi_version, 0);
|
|
64
|
+
}
|
data/lib/secretspec.rb
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby SDK for SecretSpec, a declarative secrets manager.
|
|
4
|
+
#
|
|
5
|
+
# A thin client over the secretspec-ffi C ABI. The Rust resolver is statically
|
|
6
|
+
# linked into a native extension (secretspec_ext), so the SDK inherits every
|
|
7
|
+
# provider with no Ruby-side logic and there is nothing to locate at runtime.
|
|
8
|
+
# Mirrors the Rust derive crate's vocabulary.
|
|
9
|
+
|
|
10
|
+
require "json"
|
|
11
|
+
|
|
12
|
+
# The compiled extension lives next to this file in a source/dev checkout, but in
|
|
13
|
+
# an installed gem RubyGems places it in a separate extensions dir already on
|
|
14
|
+
# $LOAD_PATH. Put this file's dir on the path so the absolute require resolves in
|
|
15
|
+
# both layouts.
|
|
16
|
+
$LOAD_PATH.unshift(__dir__) unless $LOAD_PATH.include?(__dir__)
|
|
17
|
+
require "secretspec/secretspec_ext"
|
|
18
|
+
|
|
19
|
+
module Secretspec
|
|
20
|
+
# Response wire-format version this SDK understands. Tracks secretspec-ffi's
|
|
21
|
+
# RESOLVE_SCHEMA_VERSION; a mismatch means the loaded library is incompatible.
|
|
22
|
+
RESOLVE_SCHEMA_VERSION = 1
|
|
23
|
+
|
|
24
|
+
# Wire-format version of the value-free report. Tracks secretspec's
|
|
25
|
+
# RESOLUTION_REPORT_SCHEMA_VERSION.
|
|
26
|
+
REPORT_SCHEMA_VERSION = 1
|
|
27
|
+
|
|
28
|
+
# A resolution failure (bad manifest, provider error, reason policy).
|
|
29
|
+
class Error < StandardError
|
|
30
|
+
attr_reader :kind
|
|
31
|
+
|
|
32
|
+
def initialize(kind, message)
|
|
33
|
+
@kind = kind
|
|
34
|
+
super("#{message} (kind: #{kind})")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One or more required secrets were not found anywhere.
|
|
39
|
+
class MissingRequiredError < Error
|
|
40
|
+
attr_reader :missing
|
|
41
|
+
|
|
42
|
+
def initialize(missing)
|
|
43
|
+
@missing = missing
|
|
44
|
+
super("missing_required", "missing required secret(s): #{missing.join(', ')}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# One resolved secret. Exactly one of +value+ / +path+ is set.
|
|
49
|
+
ResolvedSecret = Struct.new(:value, :path, :as_path, :source, :source_provider) do
|
|
50
|
+
# The usable string: the file path for as_path secrets, else the value.
|
|
51
|
+
def get
|
|
52
|
+
as_path ? path : value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A successful resolution, mirroring the Rust Resolved wrapper.
|
|
57
|
+
Resolved = Struct.new(:provider, :profile, :secrets, :missing_optional) do
|
|
58
|
+
# Export each resolved secret into ENV by its declared name. Secrets with no
|
|
59
|
+
# usable value (e.g. under no_values) are skipped rather than deleted from
|
|
60
|
+
# ENV (assigning nil would remove the variable).
|
|
61
|
+
def set_as_env!
|
|
62
|
+
secrets.each do |name, secret|
|
|
63
|
+
value = secret.get
|
|
64
|
+
ENV[name] = value unless value.nil?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Flat { "SECRET_NAME" => value } hash (the file path for as_path). A secret
|
|
69
|
+
# with no usable value (e.g. under no_values) maps to nil, matching the null
|
|
70
|
+
# the other SDKs emit. Feed this to a quicktype-generated deserializer (e.g.
|
|
71
|
+
# from_dynamic!). See `secretspec schema`.
|
|
72
|
+
def fields
|
|
73
|
+
secrets.transform_values(&:get)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Remove the temp files backing any as_path secrets in this result. The
|
|
77
|
+
# resolver persists those files (mode 0400) so their paths stay valid after
|
|
78
|
+
# resolve returns; the caller owns their lifetime. Call #close (or pass a
|
|
79
|
+
# block to Builder#load, which closes automatically) when done so secret
|
|
80
|
+
# files do not accumulate in the temp dir. A file already gone is not an
|
|
81
|
+
# error.
|
|
82
|
+
def close
|
|
83
|
+
secrets.each_value do |secret|
|
|
84
|
+
next unless secret.as_path && secret.path
|
|
85
|
+
|
|
86
|
+
File.delete(secret.path) if File.exist?(secret.path)
|
|
87
|
+
end
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Value-free resolution outcome for one declared secret: how it would resolve
|
|
93
|
+
# and from where, never the value itself.
|
|
94
|
+
SecretReport = Struct.new(:name, :status, :required, :source_provider,
|
|
95
|
+
:default_applied, :generated, :as_path)
|
|
96
|
+
|
|
97
|
+
# A value-free resolution snapshot. Unlike Resolved, a missing required secret
|
|
98
|
+
# is a "missing_required" status here, not an error, so a report describes a
|
|
99
|
+
# profile even when its secrets are not all available.
|
|
100
|
+
Report = Struct.new(:provider, :profile, :secrets)
|
|
101
|
+
|
|
102
|
+
# The narrow C ABI, statically linked into the secretspec_ext extension. The
|
|
103
|
+
# Native.c_resolve / c_abi_version C functions are defined in
|
|
104
|
+
# ext/secretspec/secretspec_ext.c; these wrappers add the Ruby-side error type.
|
|
105
|
+
module Native
|
|
106
|
+
class << self
|
|
107
|
+
def resolve(request_json)
|
|
108
|
+
result = c_resolve(request_json)
|
|
109
|
+
raise Error.new("ffi", "secretspec_resolve returned null") if result.nil?
|
|
110
|
+
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def abi_version
|
|
115
|
+
c_abi_version
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Entry point mirroring the derive crate's SecretSpec::builder().
|
|
121
|
+
class SecretSpec
|
|
122
|
+
def self.builder
|
|
123
|
+
Builder.new
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fluent builder for a resolution.
|
|
128
|
+
class Builder
|
|
129
|
+
def initialize
|
|
130
|
+
@request = {}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def with_path(path)
|
|
134
|
+
@request["path"] = path if path
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def with_provider(provider)
|
|
139
|
+
@request["provider"] = provider if provider
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def with_profile(profile)
|
|
144
|
+
@request["profile"] = profile if profile
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def with_reason(reason)
|
|
149
|
+
@request["reason"] = reason if reason
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Omit secret values, returning only structure and provenance.
|
|
154
|
+
def with_no_values(no_values = true)
|
|
155
|
+
@request["no_values"] = no_values
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolve the secrets. Raises MissingRequiredError if a required secret is
|
|
160
|
+
# missing, and Error for any other failure.
|
|
161
|
+
#
|
|
162
|
+
# Without a block, returns the Resolved (the caller should #close it when
|
|
163
|
+
# done to clean up any as_path temp files). With a block, yields the Resolved
|
|
164
|
+
# and closes it afterwards, returning the block's value.
|
|
165
|
+
def load
|
|
166
|
+
response = parse_response(JSON.generate(@request), "resolve", RESOLVE_SCHEMA_VERSION)
|
|
167
|
+
|
|
168
|
+
missing = response["missing_required"] || []
|
|
169
|
+
raise MissingRequiredError.new(missing) unless missing.empty?
|
|
170
|
+
|
|
171
|
+
secrets = {}
|
|
172
|
+
(response["secrets"] || {}).each do |name, entry|
|
|
173
|
+
secrets[name] = ResolvedSecret.new(
|
|
174
|
+
entry["value"], entry["path"], entry["as_path"] || false,
|
|
175
|
+
entry["source"], entry["source_provider"]
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
resolved = Resolved.new(
|
|
180
|
+
response["provider"], response["profile"], secrets,
|
|
181
|
+
response["missing_optional"] || []
|
|
182
|
+
)
|
|
183
|
+
return resolved unless block_given?
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
yield resolved
|
|
187
|
+
ensure
|
|
188
|
+
resolved.close
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Resolve a value-free Report (the inventory/preflight view, the same one the
|
|
193
|
+
# CLI exposes as `check --json`). Unlike #load, never raises
|
|
194
|
+
# MissingRequiredError: a missing required secret appears as a SecretReport
|
|
195
|
+
# with status "missing_required".
|
|
196
|
+
def report
|
|
197
|
+
request = @request.merge("mode" => "report")
|
|
198
|
+
response = parse_response(JSON.generate(request), "report", REPORT_SCHEMA_VERSION)
|
|
199
|
+
|
|
200
|
+
secrets = (response["secrets"] || []).map do |s|
|
|
201
|
+
SecretReport.new(s["name"], s["status"], s["required"],
|
|
202
|
+
s["source_provider"], s["default_applied"],
|
|
203
|
+
s["generated"], s["as_path"])
|
|
204
|
+
end
|
|
205
|
+
Report.new(response["provider"], response["profile"], secrets)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Resolve a JSON request payload and return the validated "response" hash, or
|
|
211
|
+
# raise. +kind+ is "resolve" or "report"; it selects the schema version to
|
|
212
|
+
# enforce and labels the version-mismatch message.
|
|
213
|
+
def parse_response(payload, kind, expected_version)
|
|
214
|
+
envelope = JSON.parse(Native.resolve(payload))
|
|
215
|
+
|
|
216
|
+
unless envelope["ok"]
|
|
217
|
+
err = envelope["error"] || {}
|
|
218
|
+
raise Error.new(err["kind"] || "unknown", err["message"] || "")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
response = envelope["response"]
|
|
222
|
+
raise Error.new("ffi", "secretspec_resolve reported ok with no response") if response.nil?
|
|
223
|
+
|
|
224
|
+
version = response["schema_version"]
|
|
225
|
+
unless version == expected_version
|
|
226
|
+
raise Error.new("version",
|
|
227
|
+
"unsupported #{kind} schema version #{version} " \
|
|
228
|
+
"(expected #{expected_version}); the secretspec-ffi " \
|
|
229
|
+
"library and this SDK are out of sync")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
response
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def self.abi_version
|
|
237
|
+
Native.abi_version
|
|
238
|
+
end
|
|
239
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
-ldbus-1 -lgcc_s -lutil -lrt -lpthread -lm -ldl -lc
|
data/vendor/secretspec.h
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SecretSpec C ABI.
|
|
3
|
+
*
|
|
4
|
+
* A deliberately narrow, JSON-in / JSON-out boundary. The entire native surface
|
|
5
|
+
* is the three functions below; all richness lives in the versioned JSON
|
|
6
|
+
* contract so language bindings stay thin.
|
|
7
|
+
*
|
|
8
|
+
* Request JSON (all fields optional):
|
|
9
|
+
* { "path": ".../secretspec.toml", "provider": "keyring://",
|
|
10
|
+
* "profile": "production", "reason": "boot", "no_values": false }
|
|
11
|
+
*
|
|
12
|
+
* Response envelope:
|
|
13
|
+
* { "ok": true, "response": { ...resolve response... } }
|
|
14
|
+
* { "ok": false, "error": { "kind": "io", "message": "..." } }
|
|
15
|
+
*
|
|
16
|
+
* The response (when ok) carries secret values unless "no_values" was set.
|
|
17
|
+
* Treat returned strings as sensitive and free them promptly.
|
|
18
|
+
*/
|
|
19
|
+
#ifndef SECRETSPEC_H
|
|
20
|
+
#define SECRETSPEC_H
|
|
21
|
+
|
|
22
|
+
#ifdef __cplusplus
|
|
23
|
+
extern "C" {
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Resolve secrets described by `request_json` (a NUL-terminated UTF-8 JSON
|
|
28
|
+
* string). Returns a newly allocated, NUL-terminated JSON response envelope
|
|
29
|
+
* that the caller OWNS and must release with secretspec_free().
|
|
30
|
+
*
|
|
31
|
+
* Returns NULL only on catastrophic allocation failure.
|
|
32
|
+
*/
|
|
33
|
+
char *secretspec_resolve(const char *request_json);
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* Free a string previously returned by secretspec_resolve(). NULL is ignored.
|
|
37
|
+
* Must not be called twice on the same pointer.
|
|
38
|
+
*/
|
|
39
|
+
void secretspec_free(char *ptr);
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* Return the ABI version as a static NUL-terminated string. Do NOT free; the
|
|
43
|
+
* pointer is valid for the lifetime of the loaded library.
|
|
44
|
+
*/
|
|
45
|
+
const char *secretspec_abi_version(void);
|
|
46
|
+
|
|
47
|
+
#ifdef __cplusplus
|
|
48
|
+
} /* extern "C" */
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
#endif /* SECRETSPEC_H */
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: secretspec
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.13.0
|
|
5
|
+
platform: x86_64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- Cachix
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Ruby bindings for SecretSpec: a native extension that statically links
|
|
14
|
+
the secretspec-ffi C ABI.'
|
|
15
|
+
email:
|
|
16
|
+
executables: []
|
|
17
|
+
extensions:
|
|
18
|
+
- ext/secretspec/extconf.rb
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- ext/secretspec/extconf.rb
|
|
23
|
+
- ext/secretspec/secretspec_ext.c
|
|
24
|
+
- lib/secretspec.rb
|
|
25
|
+
- vendor/libsecretspec_ffi.a
|
|
26
|
+
- vendor/native-static-libs.txt
|
|
27
|
+
- vendor/secretspec.h
|
|
28
|
+
homepage: https://secretspec.dev/
|
|
29
|
+
licenses:
|
|
30
|
+
- Apache-2.0
|
|
31
|
+
metadata: {}
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.5.22
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Declarative secrets, every environment, any provider (Ruby SDK)
|
|
51
|
+
test_files: []
|