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 +4 -4
- data/.circleci/config.yml +2 -2
- data/.claude/skills +1 -0
- data/AGENTS.md +12 -1
- data/CLAUDE.md +4 -0
- data/Gemfile.lock +34 -9
- data/Makefile +1 -0
- data/README.md +31 -0
- data/lib/nonnative/ed25519_key.rb +30 -0
- data/lib/nonnative/jwt_token.rb +46 -0
- data/lib/nonnative/paseto_token.rb +61 -0
- data/lib/nonnative/ssh_token.rb +58 -0
- data/lib/nonnative/token.rb +64 -0
- data/lib/nonnative/version.rb +1 -1
- data/lib/nonnative.rb +31 -0
- data/nonnative.gemspec +6 -0
- metadata +128 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5b12a0346b56caafe5c5c3b569be5d413f549881aaff7b11a5f32790fcdb9a8
|
|
4
|
+
data.tar.gz: 34c07a08b757d56c2a03180f988c90227f1a4fa2b5a3e4f98dc0d893fd18eaa9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
17
|
+
- image: alexfalkowski/ruby:3.20
|
|
18
18
|
release:
|
|
19
19
|
docker:
|
|
20
|
-
- image: alexfalkowski/release:8.
|
|
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
data/Gemfile.lock
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
nonnative (3.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
data/lib/nonnative/version.rb
CHANGED
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.
|
|
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
|