nonnative 3.17.0 → 3.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfb0d140b1fe5374f9d34b8e6accf2fc21ded8f8bba720cea25a166897b2886e
4
- data.tar.gz: '0409f68ad074bc744aac66c324d5c5c1d38fa13b2085485562c1c2039435866f'
3
+ metadata.gz: f5b12a0346b56caafe5c5c3b569be5d413f549881aaff7b11a5f32790fcdb9a8
4
+ data.tar.gz: 34c07a08b757d56c2a03180f988c90227f1a4fa2b5a3e4f98dc0d893fd18eaa9
5
5
  SHA512:
6
- metadata.gz: f1ba38459a5762fd599ff0a44e5dcc4e2be6caad32f7006fd0d1c4a68c1970652929649021cfa735f64550aa14224c25a0bfafdddc3e210573549249590a31ad
7
- data.tar.gz: 2a0ace60a99ad96a97d7cf65e3282ee3fbe6e66b58c222bf101588fde85b52eca517d9e55bd7e8be81fd523c38f03cf415434539b91428a78d71e75802e7e8ad
6
+ metadata.gz: 1bcbc6b9874a9ee048b4aac19382fc6d241f6d1ff616c787fede90c8957a8e51180dc8101aa41574e980098d1805937d7475e15212402b8f4cfb5a71d509091c
7
+ data.tar.gz: 9efff6226af6557162861d81f43976bfcde15991aa736715e140f7bc22cae1a8a28a65ec18d332a5f09997fc95fb755d7418a9e4156118ba7152c8bc3fb6be39
data/.circleci/config.yml CHANGED
@@ -14,10 +14,10 @@ commands:
14
14
  executors:
15
15
  ruby:
16
16
  docker:
17
- - image: alexfalkowski/ruby:3.19
17
+ - image: alexfalkowski/ruby:3.20
18
18
  release:
19
19
  docker:
20
- - image: alexfalkowski/release:8.21
20
+ - image: alexfalkowski/release:8.22
21
21
  alpine:
22
22
  docker:
23
23
  - image: alpine:latest
data/.claude/skills ADDED
@@ -0,0 +1 @@
1
+ ../bin/skills
data/AGENTS.md CHANGED
@@ -111,7 +111,7 @@ of dependencies.
111
111
  Public entry point: `lib/nonnative.rb`.
112
112
 
113
113
  Main API: `configure`, `start`, `stop`, `clear`, `reset`, `pool`,
114
- `go_argv`, `go_command`.
114
+ `go_argv`, `go_command`, `token`.
115
115
 
116
116
  Configuration is `Nonnative::Configuration`, built with
117
117
  `config.process`, `config.server`, `config.service`, or
@@ -127,6 +127,16 @@ Runners:
127
127
  reverse. Readiness and shutdown checks are TCP-only via
128
128
  `Nonnative::Port#open?` and `#closed?`.
129
129
 
130
+ Token generation: `Nonnative.token(kind:, issuer:, key:, private_key:, expiration:)`
131
+ returns a `Nonnative::Token` whose `generate(aud:, sub:)` produces a signed token for
132
+ authenticating against services under test; it feeds `Nonnative::Header.auth_bearer`.
133
+ Kinds are `jwt` (EdDSA, `kid` header), `paseto` (v4.public, `kid` footer), and `ssh`
134
+ (go-service style raw-Ed25519 `base64(claims).base64(signature)`). All Ed25519 and
135
+ generation-only. `jwt`/`paseto` take a PKCS#8 PEM key; `ssh` takes an OpenSSH-format
136
+ key. PASETO needs system libsodium (via `rbnacl`), required lazily so `require
137
+ 'nonnative'` works without it until a PASETO token is generated.
138
+ `Nonnative::Token.http_audience` / `grpc_audience` build the endpoint-scoped `aud`.
139
+
130
140
  ## Cucumber Surface
131
141
 
132
142
  `lib/nonnative/cucumber.rb` is public compatibility surface. Do not remove or
@@ -192,6 +202,7 @@ Limitations:
192
202
  - Readiness/timeouts: `lib/nonnative/port.rb`, `lib/nonnative/timeout.rb`
193
203
  - Process lifecycle: `lib/nonnative/process.rb`
194
204
  - Go executable command/argv building: `lib/nonnative/go_executable.rb`
205
+ - Token generation: `lib/nonnative/token.rb`, `lib/nonnative/jwt_token.rb`, `lib/nonnative/paseto_token.rb`, `lib/nonnative/ssh_token.rb`, `lib/nonnative/ed25519_key.rb`
195
206
  - Proxies: `lib/nonnative/fault_injection_proxy.rb`, `lib/nonnative/socket_pair_factory.rb`
196
207
  - Cucumber: `lib/nonnative/cucumber.rb`, `lib/nonnative/startup.rb`, `features/support/env.rb`
197
208
  - Config loading: `lib/nonnative/configuration.rb`, `lib/nonnative/configuration_file.rb`, `lib/nonnative/configuration_runner.rb`, `lib/nonnative/configuration_proxy.rb`
data/CLAUDE.md ADDED
@@ -0,0 +1,4 @@
1
+ <!-- BEGIN bin claude-init -->
2
+ @AGENTS.md
3
+ @bin/AGENTS.md
4
+ <!-- END bin claude-init -->
data/Gemfile.lock CHANGED
@@ -1,20 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nonnative (3.17.0)
4
+ nonnative (3.18.0)
5
5
  concurrent-ruby (>= 1, < 2)
6
6
  config (>= 5, < 6)
7
7
  cucumber (>= 7, < 12)
8
8
  cucumber-cucumber-expressions (< 19)
9
+ ed25519 (>= 1, < 2)
9
10
  get_process_mem (>= 1, < 2)
10
11
  grpc (>= 1, < 2)
12
+ jwt (>= 3, < 4)
13
+ jwt-eddsa (>= 0, < 1)
11
14
  puma (>= 7, < 8)
15
+ rbnacl (>= 7, < 8)
12
16
  rest-client (>= 2, < 3)
13
17
  retriable (>= 3, < 4)
14
18
  rspec-benchmark (>= 0, < 1)
15
19
  rspec-expectations (>= 3, < 4)
16
20
  rspec-wait (>= 1, < 2)
21
+ ruby-paseto (>= 0, < 1)
17
22
  sinatra (>= 4, < 5)
23
+ ssh_data (>= 1, < 2)
18
24
 
19
25
  GEM
20
26
  remote: https://rubygems.org/
@@ -30,7 +36,7 @@ GEM
30
36
  config (5.6.1)
31
37
  deep_merge (~> 1.2, >= 1.2.1)
32
38
  ostruct
33
- cucumber (11.1.0)
39
+ cucumber (11.1.1)
34
40
  base64 (~> 0.2)
35
41
  builder (~> 3.2)
36
42
  cucumber-ci-environment (> 9, < 12)
@@ -59,6 +65,7 @@ GEM
59
65
  diff-lcs (1.6.2)
60
66
  docile (1.4.1)
61
67
  domain_name (0.6.20240107)
68
+ ed25519 (1.4.0)
62
69
  ffi (1.17.4-x86_64-darwin)
63
70
  ffi (1.17.4-x86_64-linux-gnu)
64
71
  get_process_mem (1.0.0)
@@ -72,29 +79,37 @@ GEM
72
79
  rake (~> 13.3)
73
80
  googleapis-common-protos-types (1.23.0)
74
81
  google-protobuf (~> 4.26)
75
- grpc (1.81.1-x86_64-darwin)
82
+ grpc (1.82.0-x86_64-darwin)
76
83
  google-protobuf (>= 3.25, < 5.0)
77
84
  googleapis-common-protos-types (~> 1.0)
78
- grpc (1.81.1-x86_64-linux-gnu)
85
+ grpc (1.82.0-x86_64-linux-gnu)
79
86
  google-protobuf (>= 3.25, < 5.0)
80
87
  googleapis-common-protos-types (~> 1.0)
81
88
  http-accept (1.7.0)
82
89
  http-cookie (1.1.6)
83
90
  domain_name (~> 0.5)
84
91
  json (2.20.0)
85
- language_server-protocol (3.17.0.5)
92
+ jwt (3.2.0)
93
+ base64
94
+ jwt-eddsa (0.9.0)
95
+ base64
96
+ ed25519
97
+ jwt (>= 2.9.0)
98
+ language_server-protocol (3.17.0.6)
86
99
  lint_roller (1.1.0)
87
100
  logger (1.7.0)
88
101
  memoist3 (1.0.0)
89
102
  mime-types (3.7.0)
90
103
  logger
91
104
  mime-types-data (~> 3.2025, >= 3.2025.0507)
92
- mime-types-data (3.2026.0414)
105
+ mime-types-data (3.2026.0701)
93
106
  mini_mime (1.1.5)
107
+ multi_json (1.15.0)
94
108
  multi_test (1.1.0)
95
109
  mustermann (3.1.1)
96
110
  netrc (0.11.0)
97
111
  nio4r (2.7.5)
112
+ openssl (3.3.3)
98
113
  ostruct (0.6.3)
99
114
  parallel (2.1.0)
100
115
  parser (3.3.11.1)
@@ -114,6 +129,8 @@ GEM
114
129
  rack (>= 3.0.0)
115
130
  rainbow (3.1.1)
116
131
  rake (13.4.2)
132
+ rbnacl (7.1.2)
133
+ ffi (~> 1)
117
134
  rbs (4.0.3)
118
135
  logger
119
136
  prism (>= 1.6.0)
@@ -146,7 +163,7 @@ GEM
146
163
  rspec-support (3.13.7)
147
164
  rspec-wait (1.0.2)
148
165
  rspec (>= 3.4)
149
- rubocop (1.88.0)
166
+ rubocop (1.88.1)
150
167
  json (~> 2.3)
151
168
  language_server-protocol (~> 3.17.0.2)
152
169
  lint_roller (~> 1.1.0)
@@ -157,13 +174,18 @@ GEM
157
174
  rubocop-ast (>= 1.49.0, < 2.0)
158
175
  ruby-progressbar (~> 1.7)
159
176
  unicode-display_width (>= 2.4.0, < 4.0)
160
- rubocop-ast (1.49.1)
177
+ rubocop-ast (1.50.0)
161
178
  parser (>= 3.3.7.2)
162
179
  prism (~> 1.7)
163
180
  ruby-lsp (0.26.9)
164
181
  language_server-protocol (~> 3.17.0)
165
182
  prism (>= 1.2, < 2.0)
166
183
  rbs (>= 3, < 5)
184
+ ruby-paseto (0.2.0)
185
+ multi_json (~> 1.15.0)
186
+ openssl (~> 3.3)
187
+ sorbet-runtime
188
+ zeitwerk
167
189
  ruby-progressbar (1.13.0)
168
190
  simplecov (0.22.0)
169
191
  docile (~> 1.1)
@@ -181,14 +203,17 @@ GEM
181
203
  rack-protection (= 4.2.1)
182
204
  rack-session (>= 2.0.0, < 3)
183
205
  tilt (~> 2.0)
206
+ sorbet-runtime (0.6.13323)
207
+ ssh_data (1.3.0)
184
208
  sys-uname (1.5.1)
185
209
  ffi (~> 1.1)
186
210
  memoist3 (~> 1.0.0)
187
- tilt (2.7.0)
211
+ tilt (2.8.0)
188
212
  tsort (0.2.0)
189
213
  unicode-display_width (3.2.0)
190
214
  unicode-emoji (~> 4.1)
191
215
  unicode-emoji (4.2.0)
216
+ zeitwerk (2.8.2)
192
217
 
193
218
  PLATFORMS
194
219
  x86_64-darwin-21
data/Makefile CHANGED
@@ -1,3 +1,4 @@
1
1
  include bin/build/make/help.mak
2
2
  include bin/build/make/ruby.mak
3
3
  include bin/build/make/git.mak
4
+ include bin/build/make/claude.mak
data/README.md CHANGED
@@ -180,6 +180,37 @@ health = Nonnative.grpc_health(
180
180
  expect(health.serving?).to eq(true)
181
181
  ```
182
182
 
183
+ ### 🔑 Tokens
184
+
185
+ `Nonnative.token` builds a signer for authenticating requests against a service under test. You pass the signing parameters directly (parsed from your own configuration); it is not coupled to any service's config format. The generated string is ready for `Nonnative::Header.auth_bearer`.
186
+
187
+ ```ruby
188
+ token = Nonnative.token(
189
+ kind: 'jwt',
190
+ issuer: 'iss',
191
+ key: 'key-1',
192
+ private_key: 'config/ed25519.pem',
193
+ expiration: 3600
194
+ )
195
+
196
+ headers = Nonnative::Header.auth_bearer(
197
+ token.generate(aud: Nonnative::Token.http_audience('GET', '/v1/things'), sub: 'user-1')
198
+ )
199
+ ```
200
+
201
+ Supported `kind` values (all Ed25519, generation only):
202
+
203
+ - `jwt`: EdDSA JWT with the key id in the `kid` header. `private_key` is a PKCS#8 PEM file.
204
+ - `paseto`: PASETO v4.public with the key id in a `{"kid":"..."}` footer. `private_key` is a PKCS#8 PEM file. Requires system **libsodium** (via `rbnacl`); it loads lazily, so `require 'nonnative'` works without libsodium until you generate a PASETO token.
205
+ - `ssh`: go-service style `base64(claims).base64(signature)` with a raw Ed25519 signature over `v1` claims. `private_key` is an **OpenSSH-format** key. `issuer` and `sub` are ignored (the subject is the key id).
206
+
207
+ The audience is endpoint-scoped; build it with the helpers:
208
+
209
+ ```ruby
210
+ Nonnative::Token.http_audience('GET', '/v1/things') # => "GET /v1/things"
211
+ Nonnative::Token.grpc_audience('/health.v1.Health/Check') # => "/health.v1.Health/Check"
212
+ ```
213
+
183
214
  ### 🔁 Lifecycle strategies (Cucumber integration)
184
215
 
185
216
  Nonnative ships Cucumber hooks (when loaded) that support these tags/strategies:
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Loads an Ed25519 private key from a PEM file for signing tokens.
5
+ #
6
+ # Verifiers such as go-service use Ed25519 keys encoded as PKCS#8 PEM. This reads the PEM once and
7
+ # exposes it in the shapes the token backends need: the raw PEM for {Nonnative::PasetoToken} and the
8
+ # 32-byte seed for {Nonnative::JwtToken}.
9
+ #
10
+ # @example
11
+ # key = Nonnative::Ed25519Key.new('config/ed25519.pem')
12
+ # key.pem # => "-----BEGIN PRIVATE KEY-----\n..."
13
+ # key.seed # => 32-byte binary String
14
+ class Ed25519Key
15
+ # @param path [String] path to a PKCS#8 Ed25519 private key PEM file
16
+ def initialize(path)
17
+ @pem = File.read(path)
18
+ end
19
+
20
+ # @return [String] the PEM contents (PKCS#8 Ed25519 private key)
21
+ attr_reader :pem
22
+
23
+ # Extracts the raw 32-byte Ed25519 seed from the PEM.
24
+ #
25
+ # @return [String] the raw 32-byte Ed25519 seed
26
+ def seed
27
+ OpenSSL::PKey.read(@pem).raw_private_key
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Generates Ed25519 (EdDSA) JWT tokens for authenticating against services under test.
5
+ #
6
+ # The key id is set in the JWT `kid` header, and the claims match the common
7
+ # iss/aud/sub/iat/nbf/exp/jti set expected by verifiers such as go-service.
8
+ #
9
+ # @example
10
+ # jwt = Nonnative::JwtToken.new(issuer: 'iss', key: 'key-1', private_key: 'config/ed25519.pem', expiration: 3600)
11
+ # jwt.generate(aud: 'GET /v1/things', sub: 'user-1')
12
+ #
13
+ # @see https://github.com/jwt/ruby-jwt-eddsa jwt-eddsa
14
+ class JwtToken
15
+ # @param issuer [String] the `iss` claim
16
+ # @param key [String] the key id set in the JWT `kid` header
17
+ # @param private_key [String] path to a PKCS#8 Ed25519 private key PEM file
18
+ # @param expiration [Integer] token lifetime in seconds (drives `exp`)
19
+ def initialize(issuer:, key:, private_key:, expiration:)
20
+ @issuer = issuer
21
+ @key = key
22
+ @ed25519 = Nonnative::Ed25519Key.new(private_key)
23
+ @expiration = expiration
24
+ end
25
+
26
+ # Generates a signed EdDSA JWT.
27
+ #
28
+ # @param aud [String] the `aud` claim (for example `"GET /v1/things"` or a gRPC full method)
29
+ # @param sub [String] the `sub` claim
30
+ # @return [String] the signed JWT
31
+ def generate(aud:, sub:)
32
+ now = Time.now.to_i
33
+ payload = {
34
+ iss: @issuer,
35
+ aud: aud,
36
+ sub: sub,
37
+ iat: now,
38
+ nbf: now,
39
+ exp: now + @expiration,
40
+ jti: SecureRandom.uuid
41
+ }
42
+
43
+ JWT.encode(payload, Ed25519::SigningKey.new(@ed25519.seed), 'EdDSA', { kid: @key })
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Generates Ed25519 PASETO v4.public tokens for authenticating against services under test.
5
+ #
6
+ # The key id is carried in a JSON footer (`{"kid":"..."}`), and the claims match the common
7
+ # iss/aud/sub/iat/nbf/exp/jti set expected by verifiers such as go-service. The signing library is
8
+ # required lazily so that requiring `nonnative` does not depend on `rbnacl`/libsodium being present.
9
+ #
10
+ # @example
11
+ # paseto = Nonnative::PasetoToken.new(issuer: 'iss', key: 'key-1', private_key: 'config/ed25519.pem', expiration: 3600)
12
+ # paseto.generate(aud: 'GET /v1/things', sub: 'user-1')
13
+ #
14
+ # @see https://github.com/bannable/paseto ruby-paseto
15
+ class PasetoToken
16
+ # @param issuer [String] the `iss` claim
17
+ # @param key [String] the key id carried in the `kid` footer
18
+ # @param private_key [String] path to a PKCS#8 Ed25519 private key PEM file
19
+ # @param expiration [Integer] token lifetime in seconds (drives `exp`)
20
+ def initialize(issuer:, key:, private_key:, expiration:)
21
+ @issuer = issuer
22
+ @key = key
23
+ @ed25519 = Nonnative::Ed25519Key.new(private_key)
24
+ @expiration = expiration
25
+ end
26
+
27
+ # Generates a signed PASETO v4.public token.
28
+ #
29
+ # @param aud [String] the `aud` claim (for example `"GET /v1/things"` or a gRPC full method)
30
+ # @param sub [String] the `sub` claim
31
+ # @return [String] the signed PASETO token
32
+ def generate(aud:, sub:)
33
+ load_dependencies!
34
+
35
+ now = Time.now.utc
36
+ claims = {
37
+ 'iss' => @issuer,
38
+ 'aud' => aud,
39
+ 'sub' => sub,
40
+ 'iat' => now.iso8601,
41
+ 'nbf' => now.iso8601,
42
+ 'exp' => (now + @expiration).iso8601,
43
+ 'jti' => SecureRandom.uuid
44
+ }
45
+
46
+ Paseto::V4::Public.new(@ed25519.pem).encode!(claims, footer: { 'kid' => @key }.to_json)
47
+ end
48
+
49
+ private
50
+
51
+ # Loads the PASETO signing libraries lazily and raises a clear error if they are unavailable.
52
+ # rbnacl loads system libsodium at runtime, which the gemspec cannot guarantee, so requiring it
53
+ # here keeps `require 'nonnative'` working without libsodium for consumers that do not use PASETO.
54
+ def load_dependencies!
55
+ require 'rbnacl'
56
+ require 'paseto'
57
+ rescue LoadError => e
58
+ raise Nonnative::Error, "PASETO token generation requires libsodium and the rbnacl and ruby-paseto gems (#{e.message})"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Generates go-service style SSH tokens for authenticating against services under test.
5
+ #
6
+ # Despite the name this is not an SSH protocol signature: the key is loaded from an OpenSSH-format
7
+ # Ed25519 private key and used as a raw Ed25519 key, matching go-service's SSH token scheme. The token
8
+ # is `<base64(claims)>.<base64(signature)>`, where the signature is a raw Ed25519 signature over the
9
+ # claims JSON. Claims are ver/kid/sub/aud/iat/exp, with `sub == kid == key` and `iat`/`exp` in Unix
10
+ # nanoseconds. `issuer` and `sub` are accepted for a uniform interface with the other kinds but unused:
11
+ # SSH tokens have no issuer, and the subject is always the key id.
12
+ #
13
+ # @example
14
+ # ssh = Nonnative::SshToken.new(key: 'key-1', private_key: 'config/id_ed25519', expiration: 3600)
15
+ # ssh.generate(aud: 'GET /v1/things', sub: 'user-1')
16
+ #
17
+ # @see https://github.com/github/ssh_data ssh_data
18
+ class SshToken
19
+ # The go-service SSH token format version.
20
+ TOKEN_VERSION = 'v1'
21
+
22
+ # @param key [String] the key id; also used as the subject
23
+ # @param private_key [String] path to an OpenSSH-format Ed25519 private key file
24
+ # @param expiration [Integer] token lifetime in seconds (drives `exp`)
25
+ def initialize(key:, private_key:, expiration:, **)
26
+ @key = key
27
+ @private_key = private_key
28
+ @expiration = expiration
29
+ end
30
+
31
+ # Generates a signed SSH token.
32
+ #
33
+ # @param aud [String] the `aud` claim (for example `"GET /v1/things"` or a gRPC full method)
34
+ # @return [String] the token, `"<base64(claims)>.<base64(signature)>"`
35
+ def generate(aud:, **)
36
+ now = Time.now
37
+ issued_at = (now.to_i * 1_000_000_000) + now.nsec
38
+ claims = {
39
+ ver: TOKEN_VERSION,
40
+ kid: @key,
41
+ sub: @key,
42
+ aud: aud,
43
+ iat: issued_at,
44
+ exp: issued_at + (@expiration * 1_000_000_000)
45
+ }.to_json
46
+
47
+ signature = Ed25519::SigningKey.new(seed).sign(claims)
48
+
49
+ "#{Base64.strict_encode64(claims)}.#{Base64.strict_encode64(signature)}"
50
+ end
51
+
52
+ private
53
+
54
+ def seed
55
+ SSHData::PrivateKey.parse_openssh(File.read(@private_key)).first.sk[0, 32]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nonnative
4
+ # Builds signed tokens (JWT, PASETO, or SSH) for authenticating against services under test.
5
+ #
6
+ # The consumer passes in the signing parameters they parsed from their own configuration; this class
7
+ # is not coupled to any particular service's configuration format. The generated token string is
8
+ # ready to pass to {Nonnative::Header.auth_bearer}.
9
+ #
10
+ # @example JWT
11
+ # token = Nonnative::Token.new(kind: 'jwt', issuer: 'iss', key: 'key-1',
12
+ # private_key: 'config/ed25519.pem', expiration: 3600)
13
+ # header = Nonnative::Header.auth_bearer(token.generate(aud: 'GET /v1/things', sub: 'user-1'))
14
+ #
15
+ # @example PASETO
16
+ # token = Nonnative::Token.new(kind: 'paseto', issuer: 'iss', key: 'key-1',
17
+ # private_key: 'config/ed25519.pem', expiration: 3600)
18
+ # token.generate(aud: Nonnative::Token.grpc_audience('/health.v1.Health/Check'), sub: 'user-1')
19
+ #
20
+ # @see Nonnative::Header.auth_bearer
21
+ class Token
22
+ # Supported token kinds mapped to their implementation.
23
+ KINDS = { 'jwt' => Nonnative::JwtToken, 'paseto' => Nonnative::PasetoToken, 'ssh' => Nonnative::SshToken }.freeze
24
+
25
+ class << self
26
+ # Builds the audience string for an HTTP endpoint.
27
+ #
28
+ # @param method [String] HTTP method (for example `"GET"`)
29
+ # @param path [String] request path (for example `"/v1/things"`)
30
+ # @return [String] the audience string (for example `"GET /v1/things"`)
31
+ def http_audience(method, path)
32
+ "#{method} #{path}"
33
+ end
34
+
35
+ # Builds the audience string for a gRPC endpoint.
36
+ #
37
+ # @param full_method [String] the gRPC full method (for example `"/health.v1.Health/Check"`)
38
+ # @return [String] the audience string
39
+ def grpc_audience(full_method)
40
+ full_method
41
+ end
42
+ end
43
+
44
+ # @param kind [String] token kind, one of `"jwt"`, `"paseto"`, or `"ssh"`
45
+ # @param issuer [String] the `iss` claim (unused by the `ssh` kind)
46
+ # @param key [String] the key id (JWT `kid` header, PASETO `kid` footer, or SSH `kid` claim)
47
+ # @param private_key [String] path to the Ed25519 private key file (PKCS#8 PEM for `jwt`/`paseto`, OpenSSH format for `ssh`)
48
+ # @param expiration [Integer] token lifetime in seconds (drives `exp`)
49
+ # @raise [ArgumentError] if the kind is not supported
50
+ def initialize(kind:, issuer:, key:, private_key:, expiration:)
51
+ klass = KINDS.fetch(kind) { raise ArgumentError, "Unsupported token kind '#{kind}'" }
52
+ @token = klass.new(issuer: issuer, key: key, private_key: private_key, expiration: expiration)
53
+ end
54
+
55
+ # Generates a signed token.
56
+ #
57
+ # @param aud [String] the `aud` claim
58
+ # @param sub [String] the `sub` claim
59
+ # @return [String] the signed token
60
+ def generate(aud:, sub:)
61
+ @token.generate(aud: aud, sub: sub)
62
+ end
63
+ end
64
+ end
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.17.0'
7
+ VERSION = '3.18.0'
8
8
  end
data/lib/nonnative.rb CHANGED
@@ -49,6 +49,10 @@ require 'open3'
49
49
  require 'securerandom'
50
50
  require 'shellwords'
51
51
  require 'uri'
52
+ require 'openssl'
53
+ require 'json'
54
+ require 'time'
55
+ require 'singleton' # ruby-paseto depends on this stdlib, which Ruby no longer auto-loads
52
56
 
53
57
  require 'grpc'
54
58
  require 'grpc/health/v1/health_services_pb'
@@ -64,6 +68,11 @@ require 'rspec/wait'
64
68
  require 'puma'
65
69
  require 'puma/server'
66
70
 
71
+ # jwt-eddsa (with the ed25519 gem) and ssh_data are pure Ruby and need no system library, so they load
72
+ # here. PASETO's rbnacl needs system libsodium, so Nonnative::PasetoToken requires it lazily instead.
73
+ require 'jwt/eddsa'
74
+ require 'ssh_data'
75
+
67
76
  require 'nonnative/version'
68
77
  require 'nonnative/error'
69
78
  require 'nonnative/start_error'
@@ -109,6 +118,11 @@ require 'nonnative/socket_pair_factory'
109
118
  require 'nonnative/go_executable'
110
119
  require 'nonnative/cucumber'
111
120
  require 'nonnative/header'
121
+ require 'nonnative/ed25519_key'
122
+ require 'nonnative/jwt_token'
123
+ require 'nonnative/paseto_token'
124
+ require 'nonnative/ssh_token'
125
+ require 'nonnative/token'
112
126
 
113
127
  # The main namespace for the gem.
114
128
  #
@@ -194,6 +208,23 @@ module Nonnative
194
208
  Nonnative::GoExecutable.new(tools, exec, output).argv(cmd, *params)
195
209
  end
196
210
 
211
+ # Builds a token generator for authenticating against a service under test.
212
+ #
213
+ # The signing parameters are passed in directly; this is not coupled to any service's
214
+ # configuration format. The generated token string is ready for {Nonnative::Header.auth_bearer}.
215
+ #
216
+ # @param kind [String] token kind, one of `"jwt"`, `"paseto"`, or `"ssh"`
217
+ # @param issuer [String] the `iss` claim (unused by the `ssh` kind)
218
+ # @param key [String] the key id (JWT `kid` header, PASETO `kid` footer, or SSH `kid` claim)
219
+ # @param private_key [String] path to the Ed25519 private key file (PKCS#8 PEM for `jwt`/`paseto`, OpenSSH format for `ssh`)
220
+ # @param expiration [Integer] token lifetime in seconds (drives `exp`)
221
+ # @return [Nonnative::Token]
222
+ #
223
+ # @example
224
+ # token = Nonnative.token(kind: 'jwt', issuer: 'iss', key: 'key-1', private_key: 'config/ed25519.pem', expiration: 3600)
225
+ # Nonnative::Header.auth_bearer(token.generate(aud: 'GET /v1/things', sub: 'user-1'))
226
+ def token(kind:, issuer:, key:, private_key:, expiration:) = Nonnative::Token.new(kind:, issuer:, key:, private_key:, expiration:)
227
+
197
228
  # Returns an HTTP client for common health/readiness endpoints.
198
229
  #
199
230
  # @return [Nonnative::Observability]
data/nonnative.gemspec CHANGED
@@ -28,13 +28,19 @@ Gem::Specification.new do |spec|
28
28
  spec.add_dependency 'config', '>= 5', '< 6'
29
29
  spec.add_dependency 'cucumber', '>= 7', '< 12'
30
30
  spec.add_dependency 'cucumber-cucumber-expressions', '< 19'
31
+ spec.add_dependency 'ed25519', '>= 1', '< 2'
31
32
  spec.add_dependency 'get_process_mem', '>= 1', '< 2'
32
33
  spec.add_dependency 'grpc', '>= 1', '< 2'
34
+ spec.add_dependency 'jwt', '>= 3', '< 4'
35
+ spec.add_dependency 'jwt-eddsa', '>= 0', '< 1'
33
36
  spec.add_dependency 'puma', '>= 7', '< 8'
37
+ spec.add_dependency 'rbnacl', '>= 7', '< 8'
34
38
  spec.add_dependency 'rest-client', '>= 2', '< 3'
35
39
  spec.add_dependency 'retriable', '>= 3', '< 4'
36
40
  spec.add_dependency 'rspec-benchmark', '>= 0', '< 1'
37
41
  spec.add_dependency 'rspec-expectations', '>= 3', '< 4'
38
42
  spec.add_dependency 'rspec-wait', '>= 1', '< 2'
43
+ spec.add_dependency 'ruby-paseto', '>= 0', '< 1'
39
44
  spec.add_dependency 'sinatra', '>= 4', '< 5'
45
+ spec.add_dependency 'ssh_data', '>= 1', '< 2'
40
46
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nonnative
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.17.0
4
+ version: 3.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alejandro Falkowski
@@ -83,6 +83,26 @@ dependencies:
83
83
  - - "<"
84
84
  - !ruby/object:Gem::Version
85
85
  version: '19'
86
+ - !ruby/object:Gem::Dependency
87
+ name: ed25519
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '1'
93
+ - - "<"
94
+ - !ruby/object:Gem::Version
95
+ version: '2'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '1'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '2'
86
106
  - !ruby/object:Gem::Dependency
87
107
  name: get_process_mem
88
108
  requirement: !ruby/object:Gem::Requirement
@@ -123,6 +143,46 @@ dependencies:
123
143
  - - "<"
124
144
  - !ruby/object:Gem::Version
125
145
  version: '2'
146
+ - !ruby/object:Gem::Dependency
147
+ name: jwt
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '3'
153
+ - - "<"
154
+ - !ruby/object:Gem::Version
155
+ version: '4'
156
+ type: :runtime
157
+ prerelease: false
158
+ version_requirements: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '3'
163
+ - - "<"
164
+ - !ruby/object:Gem::Version
165
+ version: '4'
166
+ - !ruby/object:Gem::Dependency
167
+ name: jwt-eddsa
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ - - "<"
174
+ - !ruby/object:Gem::Version
175
+ version: '1'
176
+ type: :runtime
177
+ prerelease: false
178
+ version_requirements: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ - - "<"
184
+ - !ruby/object:Gem::Version
185
+ version: '1'
126
186
  - !ruby/object:Gem::Dependency
127
187
  name: puma
128
188
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +203,26 @@ dependencies:
143
203
  - - "<"
144
204
  - !ruby/object:Gem::Version
145
205
  version: '8'
206
+ - !ruby/object:Gem::Dependency
207
+ name: rbnacl
208
+ requirement: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '7'
213
+ - - "<"
214
+ - !ruby/object:Gem::Version
215
+ version: '8'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '7'
223
+ - - "<"
224
+ - !ruby/object:Gem::Version
225
+ version: '8'
146
226
  - !ruby/object:Gem::Dependency
147
227
  name: rest-client
148
228
  requirement: !ruby/object:Gem::Requirement
@@ -243,6 +323,26 @@ dependencies:
243
323
  - - "<"
244
324
  - !ruby/object:Gem::Version
245
325
  version: '2'
326
+ - !ruby/object:Gem::Dependency
327
+ name: ruby-paseto
328
+ requirement: !ruby/object:Gem::Requirement
329
+ requirements:
330
+ - - ">="
331
+ - !ruby/object:Gem::Version
332
+ version: '0'
333
+ - - "<"
334
+ - !ruby/object:Gem::Version
335
+ version: '1'
336
+ type: :runtime
337
+ prerelease: false
338
+ version_requirements: !ruby/object:Gem::Requirement
339
+ requirements:
340
+ - - ">="
341
+ - !ruby/object:Gem::Version
342
+ version: '0'
343
+ - - "<"
344
+ - !ruby/object:Gem::Version
345
+ version: '1'
246
346
  - !ruby/object:Gem::Dependency
247
347
  name: sinatra
248
348
  requirement: !ruby/object:Gem::Requirement
@@ -263,6 +363,26 @@ dependencies:
263
363
  - - "<"
264
364
  - !ruby/object:Gem::Version
265
365
  version: '5'
366
+ - !ruby/object:Gem::Dependency
367
+ name: ssh_data
368
+ requirement: !ruby/object:Gem::Requirement
369
+ requirements:
370
+ - - ">="
371
+ - !ruby/object:Gem::Version
372
+ version: '1'
373
+ - - "<"
374
+ - !ruby/object:Gem::Version
375
+ version: '2'
376
+ type: :runtime
377
+ prerelease: false
378
+ version_requirements: !ruby/object:Gem::Requirement
379
+ requirements:
380
+ - - ">="
381
+ - !ruby/object:Gem::Version
382
+ version: '1'
383
+ - - "<"
384
+ - !ruby/object:Gem::Version
385
+ version: '2'
266
386
  description: Starts OS processes, in-process Ruby servers, and proxy-only services
267
387
  with TCP readiness checks and fault-injection proxies.
268
388
  email:
@@ -272,6 +392,7 @@ extensions: []
272
392
  extra_rdoc_files: []
273
393
  files:
274
394
  - ".circleci/config.yml"
395
+ - ".claude/skills"
275
396
  - ".codecov.yml"
276
397
  - ".config/cucumber.yml"
277
398
  - ".editorconfig"
@@ -281,6 +402,7 @@ files:
281
402
  - ".goreleaser.yml"
282
403
  - ".rubocop.yml"
283
404
  - AGENTS.md
405
+ - CLAUDE.md
284
406
  - Gemfile
285
407
  - Gemfile.lock
286
408
  - LICENSE
@@ -299,6 +421,7 @@ files:
299
421
  - lib/nonnative/configuration_service_readiness.rb
300
422
  - lib/nonnative/cucumber.rb
301
423
  - lib/nonnative/delay_socket_pair.rb
424
+ - lib/nonnative/ed25519_key.rb
302
425
  - lib/nonnative/error.rb
303
426
  - lib/nonnative/fault_injection_proxy.rb
304
427
  - lib/nonnative/go_executable.rb
@@ -312,9 +435,11 @@ files:
312
435
  - lib/nonnative/http_server.rb
313
436
  - lib/nonnative/http_service.rb
314
437
  - lib/nonnative/invalid_data_socket_pair.rb
438
+ - lib/nonnative/jwt_token.rb
315
439
  - lib/nonnative/no_proxy.rb
316
440
  - lib/nonnative/not_found_error.rb
317
441
  - lib/nonnative/observability.rb
442
+ - lib/nonnative/paseto_token.rb
318
443
  - lib/nonnative/pool.rb
319
444
  - lib/nonnative/port.rb
320
445
  - lib/nonnative/ports.rb
@@ -326,12 +451,14 @@ files:
326
451
  - lib/nonnative/service.rb
327
452
  - lib/nonnative/socket_pair.rb
328
453
  - lib/nonnative/socket_pair_factory.rb
454
+ - lib/nonnative/ssh_token.rb
329
455
  - lib/nonnative/start_error.rb
330
456
  - lib/nonnative/startup.rb
331
457
  - lib/nonnative/stop_error.rb
332
458
  - lib/nonnative/tcp_probe.rb
333
459
  - lib/nonnative/timeout.rb
334
460
  - lib/nonnative/timeout_socket_pair.rb
461
+ - lib/nonnative/token.rb
335
462
  - lib/nonnative/version.rb
336
463
  - nonnative.gemspec
337
464
  homepage: https://github.com/alexfalkowski/nonnative