globiguard 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: dfe6ca1d82d5c27cd8b501fbf9306911785035cd09384c41e081e1c286cac576
4
+ data.tar.gz: ae191efdeca8f3ca701fdfd999878bdb46e60e1ca473e87db8af0d997a678957
5
+ SHA512:
6
+ metadata.gz: 698b401af1ea2603753d2d99093bfc559a87b5c0b02ca30a5e59c0cb071f3a088d10ab2849a86d976fffcb35fc2049fb96d7896b88d72aaf96fd12492d1d0cd9
7
+ data.tar.gz: c0dd8138ca55a3575ab55df1e829a0660caf014ef09ff42e15639dc9c98d5f47b3c9072d015b2cdf054d501c40a706852b64db6de751d7bcfa1d0db8a4e07694
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial dependency-minimal Ruby SDK foundation.
6
+ - Added credential helpers, safe request transport, resource clients, governed action helpers, trust webhook verification, bootstrap helpers, and offline entitlement manifest verification through Ruby OpenSSL Ed25519 support.
7
+
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,14 @@
1
+ # Contributing
2
+
3
+ GlobiGuard Ruby SDK changes should avoid gem runtime dependencies unless a security review accepts a specific exception.
4
+
5
+ ## Validate locally
6
+
7
+ ```bash
8
+ ruby -c lib/globiguard.rb
9
+ ruby test/smoke_test.rb
10
+ gem build globiguard.gemspec
11
+ ```
12
+
13
+ Examples must use placeholder secrets only and webhook handlers must pass raw request body bytes into verification.
14
+
data/LICENSE ADDED
@@ -0,0 +1,106 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined in Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity.
18
+
19
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
20
+ permissions granted by this License.
21
+
22
+ "Source" form shall mean the preferred form for making modifications,
23
+ including but not limited to software source code, documentation
24
+ source, and configuration files.
25
+
26
+ "Object" form shall mean any form resulting from mechanical
27
+ transformation or translation of a Source form.
28
+
29
+ "Work" shall mean the work of authorship, whether in Source or Object
30
+ form, made available under the License.
31
+
32
+ "Derivative Works" shall mean any work that is based on (or derived
33
+ from) the Work and for which the editorial revisions represent an
34
+ original work of authorship.
35
+
36
+ "Contribution" shall mean any work of authorship that is submitted
37
+ to, included in, or in any way used by Licensor, or incorporated
38
+ into, the Work.
39
+
40
+ "Contributor" shall mean Licensor and any individual or Legal Entity
41
+ on behalf of whom a Contribution has been received by Licensor and
42
+ subsequently incorporated within the Work.
43
+
44
+ 2. Grant of Copyright License. Subject to the terms and conditions of
45
+ this License, each Contributor hereby grants to You a perpetual,
46
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
47
+ copyright license to reproduce, prepare Derivative Works of, publicly
48
+ display, publicly perform, sublicense, and distribute the Work and
49
+ such Derivative Works in Source or Object form.
50
+
51
+ 3. Grant of Patent License. Subject to the terms and conditions of
52
+ this License, each Contributor hereby grants to You a perpetual,
53
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
54
+ (except as stated in this section) patent license to make, have made,
55
+ use, offer to sell, sell, import, and otherwise transfer the Work.
56
+
57
+ 4. Redistribution. You may reproduce and distribute copies of the
58
+ Work or Derivative Works thereof in any medium, with or without
59
+ modifications, and in Source or Object form, provided that You
60
+ meet the following conditions:
61
+
62
+ (a) You must give any other recipients of the Work or
63
+ Derivative Works a copy of this License; and
64
+
65
+ (b) You must cause any modified files to carry prominent notices
66
+ stating that You changed the files; and
67
+
68
+ (c) You must retain, in the Source form of any Derivative Works
69
+ that You distribute, all copyright, patent, trademark, and
70
+ attribution notices from the Source form of the Work.
71
+
72
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
73
+ any Contribution intentionally submitted for inclusion in the Work
74
+ by You to Licensor shall be under the terms and conditions of
75
+ this License, without any limitation.
76
+
77
+ 6. Trademarks. This License does not grant permission to use the trade
78
+ names, trademarks, service marks, or product names of the Licensor
79
+ or its Subsidiaries or Licensors, except as required for reasonable
80
+ and customary use in describing the origin of the Work and
81
+ reproducing the content of the NOTICE file.
82
+
83
+ 7. Disclaimer of Warranty. Unless required by applicable law or
84
+ agreed to in writing, Licensor provides the Work (and each
85
+ Contributor provides its Contributions) on an "AS-IS" BASIS,
86
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
87
+ or implied, including, without limitation, any warranties or
88
+ conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
89
+ FOR A PARTICULAR PURPOSE.
90
+
91
+ 8. Limitation of Liability. In no event and under no legal theory,
92
+ whether in tort (including negligence), contract, or otherwise,
93
+ unless required by applicable law (such as deliberate and grossly
94
+ negligent acts) or agreed to in writing, shall any Contributor be
95
+ liable to You for damages, including any direct, indirect, special,
96
+ incidental, or consequential damages of any character arising as a
97
+ result of this License or out of the use or inability to use the
98
+ Work.
99
+
100
+ 9. Accepting Warranty or Additional Liability. While redistributing
101
+ the Work or Derivative Works thereof, You may choose to offer,
102
+ and charge a fee for, acceptance of support, warranty, indemnity,
103
+ or other liability obligations and/or rights consistent with this
104
+ License.
105
+
106
+ END OF TERMS AND CONDITIONS
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # globiguard-ruby
2
+
3
+ Official dependency-minimal Ruby SDK for GlobiGuard.
4
+
5
+ The package has no gem runtime dependencies. It uses Ruby stdlib for HTTP, HMAC, JSON, URL handling, and OpenSSL-backed Ed25519 entitlement verification where the installed Ruby/OpenSSL build supports Ed25519.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ gem install globiguard
11
+ ```
12
+
13
+ ## Server client
14
+
15
+ ```ruby
16
+ require "globiguard"
17
+
18
+ client = GlobiGuard::Client.server(
19
+ environment: "sandbox",
20
+ services: { "controlPlane" => "https://api.globiguard.com" },
21
+ credential: GlobiGuard::Credential.secret("proj_example", "ggsk_example_replace_me", "sandbox")
22
+ )
23
+
24
+ decision = client.governed_actions.authorize_action_or_throw(
25
+ actionType: "refund",
26
+ actor: { id: "user_123" },
27
+ target: { id: "order_456" }
28
+ )
29
+ ```
30
+
31
+ ## Webhooks
32
+
33
+ Pass the exact raw request body string. Do not parse and re-serialize JSON before verification.
34
+
35
+ ```ruby
36
+ result = GlobiGuard::TrustWebhook.verify(headers, raw_body, "whsec_example_replace_me")
37
+ raise result[:error] unless result[:ok]
38
+ ```
39
+
40
+ ## Development
41
+
42
+ ```bash
43
+ ruby -c lib/globiguard.rb
44
+ ruby test/smoke_test.rb
45
+ gem build globiguard.gemspec
46
+ ```
data/SECURITY.md ADDED
@@ -0,0 +1,15 @@
1
+ # Security Policy
2
+
3
+ Report suspected vulnerabilities privately through GitHub security advisories for `globiguard/globiguard-ruby`.
4
+
5
+ ## Supported versions
6
+
7
+ The `0.x` series receives security fixes while the SDK surface is stabilizing.
8
+
9
+ ## Security expectations
10
+
11
+ - Do not log secret keys, webhook secrets, raw entitlement signing keys, or raw webhook bodies that may contain sensitive data.
12
+ - Always verify trust webhooks against the exact raw request body bytes.
13
+ - Use `sandbox` for tests and `live` only for production credentials issued by the GlobiGuard app.
14
+ - Keep gem runtime dependencies at zero unless a reviewed security need outweighs the supply-chain cost.
15
+
data/lib/globiguard.rb ADDED
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "time"
8
+ require "uri"
9
+
10
+ module GlobiGuard
11
+ VERSION = "0.1.0"
12
+ ENVIRONMENTS = %w[local sandbox live].freeze
13
+
14
+ Credential = Struct.new(:kind, :project_id, :token, :environment, keyword_init: true) do
15
+ def self.secret(project_id, token, environment)
16
+ new(kind: "secret", project_id: project_id, token: token, environment: environment)
17
+ end
18
+
19
+ def self.publishable(project_id, token, environment)
20
+ new(kind: "publishable", project_id: project_id, token: token, environment: environment)
21
+ end
22
+
23
+ def self.local(token = nil)
24
+ new(kind: "local", project_id: nil, token: token, environment: "local")
25
+ end
26
+ end
27
+
28
+ class Client
29
+ attr_reader :transport
30
+
31
+ def self.server(environment:, services:, credential:)
32
+ raise ArgumentError, "Server clients require secret or local credentials." if credential.kind == "publishable"
33
+ new(environment: environment, services: services, credential: credential)
34
+ end
35
+
36
+ def self.browser(environment:, services:, credential:)
37
+ raise ArgumentError, "Browser clients cannot use secret credentials." if credential.kind == "secret"
38
+ new(environment: environment, services: services, credential: credential)
39
+ end
40
+
41
+ def initialize(environment:, services:, credential:)
42
+ @transport = Transport.new(environment: environment, services: services, credential: credential)
43
+ end
44
+
45
+ def actions = ResourceClient.new(@transport, "/v1/actions")
46
+ def audit = ResourceClient.new(@transport, "/v1/audit")
47
+ def installs = ResourceClient.new(@transport, "/v1/installs")
48
+ def orgs = ResourceClient.new(@transport, "/v1/orgs")
49
+ def policies = ResourceClient.new(@transport, "/v1/policies")
50
+ def queue = ResourceClient.new(@transport, "/v1/queue")
51
+ def workflows = ResourceClient.new(@transport, "/v1/workflows")
52
+ def governed_actions = GovernedActions.new(@transport)
53
+ end
54
+
55
+ class Transport
56
+ RESERVED_HEADERS = %w[
57
+ x-globiguard-project-id
58
+ x-globiguard-secret-key
59
+ x-globiguard-publishable-key
60
+ x-globiguard-local-mode
61
+ x-globiguard-local-token
62
+ x-globiguard-client
63
+ x-globiguard-environment
64
+ ].freeze
65
+
66
+ def initialize(environment:, services:, credential:)
67
+ raise ArgumentError, "Environment must be local, sandbox, or live." unless ENVIRONMENTS.include?(environment)
68
+ raise ArgumentError, "Credential environment must match client environment." unless credential.environment == environment
69
+
70
+ @environment = environment
71
+ @services = services
72
+ @credential = credential
73
+ @base_uri = URI(services.fetch("controlPlane"))
74
+ raise ArgumentError, "HTTPS is required outside local." if environment != "local" && @base_uri.scheme != "https"
75
+ if credential.kind == "local" && !%w[localhost 127.0.0.1 ::1].include?(@base_uri.host)
76
+ raise ArgumentError, "Local credentials require localhost or loopback URLs."
77
+ end
78
+ end
79
+
80
+ def request(method, path, body: nil, headers: {})
81
+ self.class.validate_path(path)
82
+ headers.each_key do |name|
83
+ raise ArgumentError, "Reserved GlobiGuard header cannot be overridden: #{name}" if RESERVED_HEADERS.include?(name.downcase)
84
+ end
85
+
86
+ uri = URI(@base_uri.to_s.sub(%r{/+\z}, "") + path)
87
+ request = Net::HTTP.const_get(method.capitalize).new(uri)
88
+ auth_headers.merge(headers).each { |name, value| request[name] = value }
89
+ request["content-type"] = "application/json" if body
90
+ request.body = JSON.generate(body) if body
91
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 30, open_timeout: 30) do |http|
92
+ http.request(request)
93
+ end
94
+ raise "GlobiGuard request failed with #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
95
+
96
+ response.body.nil? || response.body.empty? ? {} : JSON.parse(response.body)
97
+ end
98
+
99
+ def auth_headers
100
+ headers = {
101
+ "x-globiguard-client" => "globiguard-ruby/#{VERSION}",
102
+ "x-globiguard-environment" => @environment
103
+ }
104
+ if @credential.kind == "local"
105
+ headers["x-globiguard-local-mode"] = "true"
106
+ headers["x-globiguard-local-token"] = @credential.token if @credential.token && !@credential.token.empty?
107
+ else
108
+ headers["x-globiguard-project-id"] = require_value(@credential.project_id, "project id")
109
+ headers[@credential.kind == "secret" ? "x-globiguard-secret-key" : "x-globiguard-publishable-key"] = require_value(@credential.token, "credential token")
110
+ end
111
+ headers
112
+ end
113
+
114
+ def self.validate_path(path)
115
+ raise ArgumentError, "Request path must start with /." unless path.start_with?("/")
116
+ if path.start_with?("//") || path.include?("\\") || path.include?("?") || path.include?("#") || path.match?(/%(?![0-9A-Fa-f]{2})/)
117
+ raise ArgumentError, "Unsafe request path."
118
+ end
119
+ raise ArgumentError, "Absolute request paths are not allowed." if URI(path).absolute?
120
+ raise ArgumentError, "Dot segments are not allowed." if path.split("/").any? { |segment| segment == "." || segment == ".." }
121
+ end
122
+
123
+ private
124
+
125
+ def require_value(value, label)
126
+ raise ArgumentError, "Missing #{label}." if value.nil? || value.empty?
127
+ value
128
+ end
129
+ end
130
+
131
+ class ResourceClient
132
+ def initialize(transport, base_path)
133
+ @transport = transport
134
+ @base_path = base_path
135
+ end
136
+
137
+ def list = @transport.request("get", @base_path)
138
+ def get(id) = @transport.request("get", "#{@base_path}/#{URI.encode_www_form_component(id)}")
139
+ def create(body) = @transport.request("post", @base_path, body: body)
140
+ def post(suffix, body) = @transport.request("post", "#{@base_path}/#{URI.encode_www_form_component(suffix.sub(%r{\A/+}, ""))}", body: body)
141
+ end
142
+
143
+ class GovernedActions
144
+ def initialize(transport)
145
+ @transport = transport
146
+ end
147
+
148
+ def authorize_action_or_throw(body, idempotency_key: nil, correlation_id: nil)
149
+ headers = {}
150
+ headers["Idempotency-Key"] = idempotency_key if idempotency_key
151
+ headers["x-correlation-id"] = correlation_id if correlation_id
152
+ result = @transport.request("post", "/v1/actions/authorize", body: body, headers: headers)
153
+ raise "GlobiGuard blocked the governed action." if result["decision"] == "BLOCK"
154
+ result
155
+ end
156
+ end
157
+
158
+ module TrustWebhook
159
+ module_function
160
+
161
+ def verify(headers, raw_body, signing_secret, tolerance_seconds: 300)
162
+ normalized = headers.transform_keys { |key| key.to_s.downcase }
163
+ delivery = normalized["x-globiguard-delivery-id"]
164
+ timestamp = normalized["x-globiguard-timestamp"]
165
+ event_type = normalized["x-globiguard-event-type"]
166
+ signature = normalized["x-globiguard-signature"]
167
+ return { ok: false, error: "Missing required webhook headers." } unless delivery && timestamp && event_type && signature
168
+ return { ok: false, error: "Webhook timestamp is outside the replay window." } if (Time.now.to_i - timestamp.to_i).abs > tolerance_seconds
169
+
170
+ signed = "globiguard-hmac-sha256-v1.#{delivery}.#{timestamp}.#{event_type}.#{raw_body}"
171
+ expected = "v1=#{OpenSSL::HMAC.hexdigest("SHA256", signing_secret, signed)}"
172
+ return { ok: false, error: "Invalid webhook signature." } unless secure_compare(expected, signature)
173
+
174
+ { ok: true, envelope: JSON.parse(raw_body) }
175
+ end
176
+
177
+ def secure_compare(left, right)
178
+ return false unless left.bytesize == right.bytesize
179
+ left.bytes.zip(right.bytes).reduce(0) { |memo, pair| memo | (pair[0] ^ pair[1]) }.zero?
180
+ end
181
+ end
182
+
183
+ module Bootstrap
184
+ module_function
185
+
186
+ def install_registration(profile, package_name:, package_version:, integration_kind:, runtime_kind:)
187
+ validate_profile(profile)
188
+ {
189
+ environment: profile.fetch(:environment),
190
+ deploymentMode: profile.fetch(:deploymentMode),
191
+ issuerMode: profile.fetch(:issuerMode),
192
+ installReporting: profile.fetch(:installReporting),
193
+ installLabel: profile[:installLabel],
194
+ package: { name: package_name, version: package_version },
195
+ integration: { kind: integration_kind, runtime: runtime_kind }
196
+ }
197
+ end
198
+
199
+ def validate_profile(profile)
200
+ raise ArgumentError, "Invalid environment." unless ENVIRONMENTS.include?(profile[:environment])
201
+ if profile[:deploymentMode] == "hosted" && profile[:issuerMode] != "globiguard_issued"
202
+ raise ArgumentError, "Hosted deployments require globiguard_issued issuer mode."
203
+ end
204
+ return unless %w[self_hosted sovereign].include?(profile[:deploymentMode])
205
+
206
+ raise ArgumentError, "Self-hosted and sovereign deployments require customer_issued issuer mode." unless profile[:issuerMode] == "customer_issued"
207
+ raise ArgumentError, "Self-hosted and sovereign install reporting must be opt_in or disabled." unless %w[opt_in disabled].include?(profile[:installReporting])
208
+ end
209
+ end
210
+
211
+ module Entitlements
212
+ module_function
213
+
214
+ def verify_signed_manifest(compact_jws, public_keys_by_id:)
215
+ parts = compact_jws.split(".")
216
+ raise ArgumentError, "Entitlement manifest must be compact JWS." unless parts.length == 3
217
+ header = JSON.parse(base64url_decode(parts[0]))
218
+ raise ArgumentError, "Entitlement manifest must use EdDSA." unless header["alg"] == "EdDSA"
219
+ public_key = public_keys_by_id.fetch(header.fetch("kid"))
220
+ signing_input = "#{parts[0]}.#{parts[1]}"
221
+ signature = base64url_decode(parts[2])
222
+ verify_ed25519!(base64url_decode(public_key), signing_input, signature)
223
+ payload = JSON.parse(base64url_decode(parts[1]))
224
+ raise ArgumentError, "Unsupported entitlement manifest schema." unless payload["schema"] == "globiguard.entitlement_manifest.v1"
225
+ now = Time.now.to_i
226
+ raise ArgumentError, "Entitlement manifest is not active yet." if payload["nbf"] && payload["nbf"].to_i > now
227
+ raise ArgumentError, "Entitlement manifest is expired." if payload["exp"] && payload["exp"].to_i <= now
228
+ payload
229
+ end
230
+
231
+ def verify_ed25519!(raw_public_key, signing_input, signature)
232
+ prefix = [48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0].pack("C*")
233
+ key = OpenSSL::PKey.read(prefix + raw_public_key)
234
+ verified = key.verify(nil, signature, signing_input)
235
+ raise OpenSSL::PKey::PKeyError, "Invalid entitlement manifest signature." unless verified
236
+ rescue NoMethodError, OpenSSL::PKey::PKeyError => e
237
+ raise OpenSSL::PKey::PKeyError, "Ruby/OpenSSL Ed25519 verification is unavailable or failed: #{e.message}"
238
+ end
239
+
240
+ def base64url_decode(value)
241
+ Base64.urlsafe_decode64(value + ("=" * ((4 - value.length % 4) % 4)))
242
+ end
243
+ end
244
+ end
245
+
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: globiguard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - GlobiGuard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Dependency-minimal Ruby SDK for GlobiGuard trusted access workflows.
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - CHANGELOG.md
20
+ - CONTRIBUTING.md
21
+ - LICENSE
22
+ - README.md
23
+ - SECURITY.md
24
+ - lib/globiguard.rb
25
+ homepage: https://globiguard.com
26
+ licenses:
27
+ - Apache-2.0
28
+ metadata:
29
+ homepage_uri: https://globiguard.com
30
+ source_code_uri: https://github.com/globiguard/globiguard-ruby
31
+ bug_tracker_uri: https://github.com/globiguard/globiguard-ruby/issues
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.1'
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.4.19
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Official dependency-minimal Ruby SDK for GlobiGuard.
51
+ test_files: []