envless-sdk 0.0.1

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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +171 -0
  4. data/lib/envless/sdk/api_error.rb +54 -0
  5. data/lib/envless/sdk/bodies.rb +49 -0
  6. data/lib/envless/sdk/client.rb +39 -0
  7. data/lib/envless/sdk/clock.rb +24 -0
  8. data/lib/envless/sdk/connection_pool.rb +83 -0
  9. data/lib/envless/sdk/connector.rb +26 -0
  10. data/lib/envless/sdk/constants.rb +262 -0
  11. data/lib/envless/sdk/crypto/cipher.rb +58 -0
  12. data/lib/envless/sdk/crypto/codec.rb +23 -0
  13. data/lib/envless/sdk/crypto/envelope.rb +41 -0
  14. data/lib/envless/sdk/crypto/keys.rb +42 -0
  15. data/lib/envless/sdk/crypto/passphrase.rb +38 -0
  16. data/lib/envless/sdk/crypto/passphrase_rules.rb +42 -0
  17. data/lib/envless/sdk/crypto/values.rb +31 -0
  18. data/lib/envless/sdk/crypto/workspace_key.rb +40 -0
  19. data/lib/envless/sdk/crypto_messages.rb +31 -0
  20. data/lib/envless/sdk/default_client.rb +55 -0
  21. data/lib/envless/sdk/defaults.rb +32 -0
  22. data/lib/envless/sdk/envelope.rb +48 -0
  23. data/lib/envless/sdk/environment.rb +42 -0
  24. data/lib/envless/sdk/errors.rb +23 -0
  25. data/lib/envless/sdk/exchange.rb +27 -0
  26. data/lib/envless/sdk/helpers.rb +49 -0
  27. data/lib/envless/sdk/http.rb +46 -0
  28. data/lib/envless/sdk/listing.rb +54 -0
  29. data/lib/envless/sdk/messages.rb +43 -0
  30. data/lib/envless/sdk/net_http_adapter.rb +84 -0
  31. data/lib/envless/sdk/numbers.rb +44 -0
  32. data/lib/envless/sdk/options.rb +56 -0
  33. data/lib/envless/sdk/pagination.rb +31 -0
  34. data/lib/envless/sdk/paths.rb +70 -0
  35. data/lib/envless/sdk/received.rb +28 -0
  36. data/lib/envless/sdk/redirects.rb +56 -0
  37. data/lib/envless/sdk/resource.rb +50 -0
  38. data/lib/envless/sdk/resources/environments.rb +61 -0
  39. data/lib/envless/sdk/resources/invites.rb +28 -0
  40. data/lib/envless/sdk/resources/keys.rb +36 -0
  41. data/lib/envless/sdk/resources/me.rb +11 -0
  42. data/lib/envless/sdk/resources/members.rb +36 -0
  43. data/lib/envless/sdk/resources/product_access.rb +30 -0
  44. data/lib/envless/sdk/resources/products.rb +62 -0
  45. data/lib/envless/sdk/resources/projects.rb +42 -0
  46. data/lib/envless/sdk/resources/roles.rb +40 -0
  47. data/lib/envless/sdk/resources/variable_changes.rb +57 -0
  48. data/lib/envless/sdk/resources/variables.rb +49 -0
  49. data/lib/envless/sdk/resources/versions.rb +60 -0
  50. data/lib/envless/sdk/resources/webhook_deliveries.rb +33 -0
  51. data/lib/envless/sdk/resources/webhooks.rb +44 -0
  52. data/lib/envless/sdk/resources/workspace.rb +26 -0
  53. data/lib/envless/sdk/resources/workspace_domain.rb +31 -0
  54. data/lib/envless/sdk/resources/workspaces.rb +24 -0
  55. data/lib/envless/sdk/results.rb +43 -0
  56. data/lib/envless/sdk/retry_policy.rb +36 -0
  57. data/lib/envless/sdk/rotation/entry_rotator.rb +61 -0
  58. data/lib/envless/sdk/rotation/key_ring.rb +34 -0
  59. data/lib/envless/sdk/rotation/ledger.rb +36 -0
  60. data/lib/envless/sdk/rotation/runner.rb +45 -0
  61. data/lib/envless/sdk/rotation/variable_rotator.rb +38 -0
  62. data/lib/envless/sdk/text.rb +71 -0
  63. data/lib/envless/sdk/transport.rb +62 -0
  64. data/lib/envless/sdk/update_notice.rb +66 -0
  65. data/lib/envless/sdk/urls.rb +70 -0
  66. data/lib/envless/sdk/variable_names.rb +55 -0
  67. data/lib/envless/sdk/version.rb +5 -0
  68. data/lib/envless/sdk/webhooks/headers.rb +49 -0
  69. data/lib/envless/sdk/webhooks/verify.rb +58 -0
  70. data/lib/envless/sdk/wire.rb +77 -0
  71. data/lib/envless/sdk.rb +77 -0
  72. data/sig/envless/sdk/adapter.rbs +60 -0
  73. data/sig/envless/sdk/client.rbs +31 -0
  74. data/sig/envless/sdk/crypto.rbs +92 -0
  75. data/sig/envless/sdk/errors.rbs +37 -0
  76. data/sig/envless/sdk/http.rbs +31 -0
  77. data/sig/envless/sdk/pagination.rbs +81 -0
  78. data/sig/envless/sdk/results.rbs +62 -0
  79. data/sig/envless/sdk/rotation.rbs +86 -0
  80. data/sig/envless/sdk/settings.rbs +81 -0
  81. data/sig/envless/sdk/stdlib.rbs +8 -0
  82. data/sig/envless/sdk/text.rbs +52 -0
  83. data/sig/envless/sdk/transport.rbs +79 -0
  84. data/sig/envless/sdk/urls.rbs +73 -0
  85. data/sig/envless/sdk/webhooks.rbs +23 -0
  86. data/sig/envless/sdk.rbs +36 -0
  87. data/sig/generated/constants.rbs +57 -0
  88. data/sig/generated/resources.rbs +209 -0
  89. data/sig/generated/types.rbs +466 -0
  90. data/sig/manifest.yaml +3 -0
  91. metadata +134 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b6f191fde8f917a2a05741ffc41fdb3f0f5555c6cd40f77ef62eee17f4ebe79
4
+ data.tar.gz: 9a9b3a4001d28af65d39ea62e59339d5965c82775d7674ce3478ab737328d8f4
5
+ SHA512:
6
+ metadata.gz: ee19e6d5b3d1c39afb10f480c61b1f0402c785f645fbb9f07831968ba23804d79a4c8957471fe8a81d2f3e189451a66bbd7353f63687ba1c0e1bf25c9d2b82db
7
+ data.tar.gz: 30dfed76e0191bcf6fb253252bfbf75082d2637dd1716da8d7c81ba5197c4bfe1aaec09b8c8b147a718cab4b492a4a9a2917eea363708f013c152611df80a847
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1
4
+
5
+ The first release of the Ruby gem. It covers the whole TypeScript SDK as it stands in this repository: 113 methods across 13 namespaces, each under its TypeScript name in snake_case. `rake parity` proves that every one of them sends exactly the request its TypeScript twin sends, with no options, with every option set and with the body passed as keywords, and retries, fails and returns the same way. It also proves the encryption, the validators, webhook verification and passphrase rotation agree with TypeScript on the same inputs, and that ciphertext written by either one is read by the other.
6
+
7
+ ### Added
8
+
9
+ `Envless::Client` has a reader for every namespace, from `variables` and `versions` to `workspace.domain` and `products.access`. `listAll` is `list_all`, `getById` is `get_by_id` and `reencryptEntries` is `reencrypt_entries`. A request body goes in as keyword arguments or as one Hash, its keys keep the API's camelCase names, the options of a call are snake_case keywords (`product:`, `limit:`, `offset:`, `search:`, `order:`, `status:`, `reattempt:`), and a response comes back as a Hash with Symbol keys.
10
+
11
+ client = Envless::Client.new(token: ENV.fetch("ENVLESS_TOKEN"))
12
+ client.variables.create("api", "production", name: "API_KEY", value: ciphertext)
13
+
14
+ Every paginated resource has `list`, which returns one `Envless::Page` with `items` and `pagination`, `list_all`, which returns every item as an Array, and `iterate`, which yields each item to a block or returns an Enumerator that fetches the next page only when it gets there.
15
+
16
+ An API refusal raises `Envless::ApiError`, which carries `status`, `code`, `resource`, `field`, `request_id` and `retry_after_seconds`, and answers `auth?`, `scope_missing?`, `validation?`, `not_found?`, `conflict?`, `rate_limited?` and `needs_upgrade?`. No response at all raises `Envless::NetworkError`, with `timeout?` when the deadline passed. A value that cannot be decrypted raises `Envless::DecryptionError`, a delivery that fails verification raises `Envless::WebhookVerificationError`, and all of them inherit `Envless::Error`.
17
+
18
+ `encrypt_value`, `decrypt_value`, `encrypt_with_key`, `decrypt_with_key`, `derive_workspace_key`, `import_workspace_key`, `ciphertext?`, `passphrase_strength` and `ENCRYPTION_PARAMETERS` produce and read the same `v1:` and `v2:` envelopes as the TypeScript and Python SDKs, the CLI and the dashboard: AES-256-GCM under a PBKDF2-SHA-256 key with 200,000 iterations and the workspace salt. `rotate_workspace_passphrase` re-encrypts an environment and its version history under a new passphrase, and lists in `failures` any version entry whose encrypted default it cannot rewrite because the entry's own value is not ciphertext. `variable_name_validation` and `verify_webhook_signature` behave as their TypeScript twins. Plaintext, passphrases and workspace ids must be valid UTF-8, and a raw key goes in only as 32 binary bytes, so binary data or a passphrase passed by mistake raises `ArgumentError` instead of producing ciphertext nothing else can read.
19
+
20
+ `Envless.variables` and every other namespace reach a shared client, built from `ENVLESS_TOKEN` and `ENVLESS_API_ENDPOINT` the first time one is touched, and `Envless.init(...)` configures it once at startup. `client.raw.request` reaches an endpoint no method wraps yet, with the client's key, base URL, timeout and retries.
21
+
22
+ `GET`, `PUT` and `DELETE` requests are retried on 408, 429, 500, 502, 503 and 504, with exponential backoff or the wait `Retry-After` asks for, capped at 30 seconds. `timeout` bounds each attempt, redirects and the response body included. Every request carries `User-Agent: envless-sdk-ruby/0.0.1`.
23
+
24
+ The gem ships RBS signatures for every method, body and response, with a manifest that declares the standard libraries they use, so `library "envless-sdk"` in a Steepfile is all a project needs. It needs Ruby 3.2 or newer and has no runtime dependencies.
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ <div align='center'>
2
+ <a href='https://envless.cloud'>
3
+ <img
4
+ src='https://cdn-prod.envless.cloud/assets/npm/logo.svg'
5
+ alt='Envless Logo'
6
+ width='180'
7
+ />
8
+ </a>
9
+
10
+ <br />
11
+ </div>
12
+
13
+ <p align='center'>
14
+ End-to-end encrypted environment variables for teams. Manage workspaces, projects, environments and secrets from code.
15
+ </p>
16
+
17
+ <p align='center'>
18
+ <a href='https://envless.cloud'>
19
+ <b>
20
+ Website
21
+ </b>
22
+ </a>
23
+ •
24
+ <a href='https://envless.cloud/dashboard'>
25
+ <b>
26
+ Dashboard
27
+ </b>
28
+ </a>
29
+ •
30
+ <a href='https://envless.cloud/docs/ruby/overview'>
31
+ <b>
32
+ Documentation
33
+ </b>
34
+ </a>
35
+ •
36
+ <a href='https://status.envless.cloud'>
37
+ <b>
38
+ Services Status
39
+ </b>
40
+ </a>
41
+ </p>
42
+
43
+ <br />
44
+
45
+ ## Intro to the Ruby Gem
46
+
47
+ The official Ruby client for the Envless API. It has every method of the TypeScript SDK, all 113 across 13 namespaces, under the same names in snake_case, and ships RBS signatures for all of them. It runs on Ruby 3.2 and newer and has no runtime dependencies: HTTP is `Net::HTTP` and the encryption is `OpenSSL`, both from the standard library, with a pool of kept-alive connections that one client shares safely across threads and rebuilds after a fork.
48
+
49
+ It carries an API key with write access, so it belongs on a server, in a job or in CI.
50
+
51
+ ### Installing
52
+ ```bash
53
+ bundle add envless-sdk
54
+ ```
55
+
56
+ Or `gem install envless-sdk` without Bundler. You require it as `envless/sdk` and use it as `Envless`. The `envless` gem on RubyGems is an unrelated project, so `gem install envless` installs something else.
57
+
58
+ ### Using
59
+ ```ruby
60
+ require "envless/sdk"
61
+
62
+ passphrase = ENV.fetch("ENVLESS_PASSPHRASE")
63
+ workspace_id = Envless.me.get[:workspaceId]
64
+
65
+ Envless.variables.create("api", "production",
66
+ name: "STRIPE_SECRET_KEY",
67
+ value: Envless.encrypt_value("sk_live_51H...", passphrase, workspace_id))
68
+ ```
69
+
70
+ `Envless.variables`, `Envless.me` and every other namespace use a shared client that reads `ENVLESS_TOKEN` the first time one is touched. Call `Envless.init(...)` once at startup to configure it, or build your own with `Envless::Client.new`, which reads the same variable when you pass no token.
71
+
72
+ Values are encrypted on your machine, never by the server, so a plaintext value is refused. A request body goes in as keyword arguments or as one Hash, and its keys keep the API's own names, so `defaultValue:` and `projectIds:` read exactly as they do in the API reference. The options of a call are snake_case keywords: `product:`, `limit:`, `offset:`, `search:`, `order:`, `status:`, `reattempt:` and `timeout:`. A response comes back as the parsed JSON, a Hash with Symbol keys, read with `variable[:updatedAt]`.
73
+
74
+ The gem ships RBS signatures in `sig/`, with a record type for every body and response, so Steep and editors that read RBS check the keys and the types of each call. `library "envless-sdk"` in your Steepfile loads them, along with the standard libraries they use.
75
+
76
+ ### A client of your own
77
+ ```ruby
78
+ require "envless/sdk"
79
+
80
+ client = Envless::Client.new(token: ENV.fetch("ENVLESS_TOKEN"), timeout: 10, max_retries: 2)
81
+
82
+ client.variables.iterate("api", "production", product: "billing") do |variable|
83
+ puts "#{variable[:name]} #{variable[:updatedAt]}"
84
+ end
85
+
86
+ client.close
87
+ ```
88
+
89
+ A client is frozen once built, is safe to share between threads and closes its idle connections with `close`.
90
+
91
+ ### Pagination
92
+ Every collection has `list` for one page, `list_all` for every page at once as an Array, and `iterate`, which yields each item to a block, or without one returns an Enumerator that fetches the next page only when it gets there, so `first(10)` stops early. A page is an `Envless::Page` with `items` and `pagination`, plus `has_more?` and `total_count`. `products.access.list` returns an `Envless::ProductAccessPage`, which also answers `private?`.
93
+
94
+ ### Encryption
95
+ ```ruby
96
+ require "envless/sdk"
97
+
98
+ workspace_id = Envless.me.get[:workspaceId]
99
+ key = Envless.derive_workspace_key(ENV.fetch("ENVLESS_PASSPHRASE"), workspace_id)
100
+
101
+ secrets = Envless.variables.list_all("api", "production").filter_map do |variable|
102
+ value = variable[:value]
103
+ [variable[:name], Envless.decrypt_with_key(key, value)] if value && Envless.ciphertext?(value)
104
+ end.to_h
105
+ ```
106
+
107
+ `encrypt_value` and `decrypt_value` derive the key on every call, which costs 200,000 rounds of PBKDF2. To work with many values, derive the key once with `derive_workspace_key`, or load the `ENVLESS_KEY` your CI holds with `import_workspace_key`, then use `encrypt_with_key` and `decrypt_with_key`. An `Envless::WorkspaceKey` never prints its bytes, and `to_bytes` hands them over when you need them. The `_with_key` methods also take those 32 bytes as a binary String, but never as text, so a passphrase can't be mistaken for a key. Plaintext, passphrases and workspace ids must be valid UTF-8: binary data raises `ArgumentError` rather than being changed silently, so encode it first, for example as base64. Ciphertext from this gem and from the TypeScript and Python SDKs, the CLI and the dashboard is interchangeable.
108
+
109
+ `ciphertext?`, `passphrase_strength`, `variable_name_validation` and `ENCRYPTION_PARAMETERS` work exactly as they do in TypeScript. The two validators return an `Envless::Validation` with `valid?` and `error`.
110
+
111
+ ### Rotating the passphrase
112
+ ```ruby
113
+ require "envless/sdk"
114
+
115
+ result = Envless.rotate_workspace_passphrase(client: Envless.client, workspace_unique_id: Envless.me.get[:workspaceId],
116
+ from: ENV.fetch("ENVLESS_PASSPHRASE"), to: ENV.fetch("ENVLESS_NEW_PASSPHRASE"), project: "api", environment: "production")
117
+
118
+ puts result.variables, result.version_entries, result.skipped.size, result.failures.size
119
+ ```
120
+
121
+ It re-encrypts an environment and its version history under the new passphrase and refuses one that `passphrase_strength` rejects. Values that are not ciphertext are left alone and listed in `skipped`, and anything that failed to decrypt or that the API refused is listed in `failures`, so a partial run can be retried. A version entry whose own value is not ciphertext lands in `failures` too when its default is, because the API cannot rewrite that default.
122
+
123
+ ### Errors
124
+ ```ruby
125
+ require "envless/sdk"
126
+
127
+ begin
128
+ Envless.variables.get("api", "production", "MISSING")
129
+ rescue Envless::ApiError => error
130
+ raise unless error.not_found?
131
+
132
+ warn [error.code, error.request_id].join(" ")
133
+ end
134
+ ```
135
+
136
+ An API refusal raises `Envless::ApiError`, with `status`, `code`, `resource`, `field`, `request_id` and `retry_after_seconds`, plus `auth?`, `scope_missing?`, `validation?`, `not_found?`, `conflict?`, `rate_limited?` and `needs_upgrade?`. No response at all raises `Envless::NetworkError`, with `timeout?` when the deadline passed. A value that cannot be decrypted raises `Envless::DecryptionError`, and a delivery that fails verification raises `Envless::WebhookVerificationError`. All of them inherit `Envless::Error`. An unusable variable name is refused before anything is sent, with the same `ApiError` the API would answer with, and a mistake in the call itself, such as an empty slug, raises `ArgumentError`.
137
+
138
+ ### Webhooks
139
+ ```ruby
140
+ class EnvlessWebhooksController < ActionController::API
141
+ def create
142
+ event = Envless.verify_webhook_signature(
143
+ payload: request.raw_post,
144
+ headers: request.headers,
145
+ secret: Rails.application.credentials.envless_webhook_secret
146
+ )
147
+
148
+ Rails.logger.info("#{event[:type]} #{event[:data]}")
149
+ head :no_content
150
+ rescue Envless::WebhookVerificationError
151
+ head :bad_request
152
+ end
153
+ end
154
+ ```
155
+
156
+ It checks the signature in constant time and rejects a delivery more than five minutes old, then returns the parsed event. Pass the raw body, since a parsed or re-encoded body no longer matches its signature. The headers can be a Hash in any case, a Rack env or Rails' `request.headers`.
157
+
158
+ ### Configuring
159
+ `Envless::Client.new` takes `token:`, `base_url:`, `timeout:`, `max_retries:`, `adapter:`, `user_agent:` and `disable_update_notice:`. `base_url` also comes from `ENVLESS_API_ENDPOINT`. `timeout` is in seconds, bounds each attempt including redirects and the response body, and `0` turns it off. `GET`, `PUT` and `DELETE` requests are retried on 408, 429, 500, 502, 503 and 504, up to three times with exponential backoff, waiting what `Retry-After` asks for up to 30 seconds. Writes that cannot safely repeat are not retried. Every method also takes `timeout:` for that one call.
160
+
161
+ `adapter:` swaps the HTTP layer for anything that responds to `call(request)`, receives an `Envless::HttpRequest` and returns an object with `status`, `headers` and `body`. The default honours `http_proxy`, `https_proxy` and `no_proxy`.
162
+
163
+ An endpoint no method wraps yet is one `raw.request` away, with the client's key, base URL, timeout and retries applied:
164
+
165
+ ```ruby
166
+ require "envless/sdk"
167
+
168
+ Envless.client.raw.request("/projects", method: :post, body: {name: "Billing", slug: "billing"})
169
+ ```
170
+
171
+ When a newer version is on RubyGems the client says so once on a terminal. `ENVLESS_DISABLE_UPDATE_NOTICE=1` or `disable_update_notice: true` turns that off.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ class ApiError < Error
5
+ attr_reader :status, :code, :resource, :field, :request_id, :retry_after_seconds
6
+
7
+ def initialize(message = nil, status: nil, code: Constants::ERROR_CODES[:SERVER_ERROR], resource: nil, field: nil,
8
+ request_id: nil, retry_after_seconds: nil)
9
+ super(message)
10
+ @status = status
11
+ @code = code
12
+ @resource = resource
13
+ @field = field
14
+ @request_id = request_id
15
+ @retry_after_seconds = retry_after_seconds
16
+ end
17
+
18
+ def detailed_message(highlight: false, **options)
19
+ "#{super} [#{[status, code].compact.join(" ")}#{", request #{request_id}" if request_id}]"
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class.name} status=#{status.inspect} code=#{code.inspect} message=#{message.inspect}>"
24
+ end
25
+
26
+ def auth?
27
+ code.to_s.start_with?("auth.")
28
+ end
29
+
30
+ def scope_missing?
31
+ code == "auth.scope_missing"
32
+ end
33
+
34
+ def validation?
35
+ status == 400
36
+ end
37
+
38
+ def not_found?
39
+ status == 404
40
+ end
41
+
42
+ def conflict?
43
+ status == 409
44
+ end
45
+
46
+ def rate_limited?
47
+ status == 429
48
+ end
49
+
50
+ def needs_upgrade?
51
+ status == 402
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ module Bodies
5
+ module_function
6
+
7
+ def merge(body, fields)
8
+ base = hash_of(body).transform_keys(&:to_s)
9
+ base.merge(fields.to_h { |key, value| [key.to_s, value] })
10
+ end
11
+
12
+ def hash_of(body)
13
+ return {} if body.nil?
14
+
15
+ mapping(body) || raise(ArgumentError, Messages::BODY_SHAPE)
16
+ end
17
+
18
+ def mapping(value)
19
+ return value if value.is_a?(Hash)
20
+ return value.to_hash if value.respond_to?(:to_hash)
21
+
22
+ value.to_h if value.is_a?(Struct) || value.is_a?(Data)
23
+ end
24
+
25
+ def entries(values, name)
26
+ list(values, name).map do |value|
27
+ hash = mapping(value)
28
+ hash ? hash.to_h { |key, item| [key.to_s, item] } : value
29
+ end
30
+ end
31
+
32
+ def list(values, name)
33
+ return values if values.is_a?(Array)
34
+ return values.to_a if values.is_a?(Set)
35
+
36
+ raise ArgumentError, format(Messages::LIST_SHAPE, name:, kind: values.class)
37
+ end
38
+
39
+ def field(hash, key)
40
+ return nil unless hash.is_a?(Hash)
41
+
42
+ hash.key?(key) ? hash[key] : hash[key.to_sym]
43
+ end
44
+
45
+ def field?(hash, key)
46
+ hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ class Client
5
+ attr_reader :raw, :me, :workspace, :workspaces, :products, :projects, :environments, :variables, :versions,
6
+ :members, :invites, :roles, :keys, :webhooks
7
+
8
+ def initialize(token_argument = nil, token: nil, base_url: nil, adapter: nil, max_retries: nil, timeout: nil,
9
+ user_agent: nil, disable_update_notice: false)
10
+ raise ArgumentError, Messages::TOKEN_TWICE unless token_argument.nil? || token.nil?
11
+
12
+ @raw = Transport.new(token: Environment.resolve_token(token.nil? ? token_argument : token),
13
+ base_url: Environment.resolve_base_url(base_url), adapter:, max_retries:, timeout:, user_agent:,
14
+ disable_update_notice:)
15
+ @me = Resources::Me.new(@raw)
16
+ @workspace = Resources::Workspace.new(@raw)
17
+ @workspaces = Resources::Workspaces.new(@raw)
18
+ @products = Resources::Products.new(@raw)
19
+ @projects = Resources::Projects.new(@raw)
20
+ @environments = Resources::Environments.new(@raw)
21
+ @variables = Resources::Variables.new(@raw)
22
+ @versions = Resources::Versions.new(@raw)
23
+ @members = Resources::Members.new(@raw)
24
+ @invites = Resources::Invites.new(@raw)
25
+ @roles = Resources::Roles.new(@raw)
26
+ @keys = Resources::Keys.new(@raw)
27
+ @webhooks = Resources::Webhooks.new(@raw)
28
+ freeze
29
+ end
30
+
31
+ def close
32
+ @raw.close
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name} base_url=#{@raw.base_url.inspect}>"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ module Clock
5
+ module_function
6
+
7
+ def now
8
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
9
+ end
10
+
11
+ def deadline(seconds)
12
+ seconds && (now + seconds)
13
+ end
14
+
15
+ def remaining(deadline)
16
+ return nil if deadline.nil?
17
+
18
+ left = deadline - now
19
+ raise Timeout::Error if left <= 0
20
+
21
+ left
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ class ConnectionPool
5
+ CLOSE_ERRORS = [IOError, SystemCallError, OpenSSL::SSL::SSLError].freeze
6
+
7
+ def initialize(max_idle:, keep_alive_timeout:)
8
+ @max_idle = max_idle
9
+ @keep_alive_timeout = keep_alive_timeout
10
+ @idle = Hash.new { |pools, key| pools[key] = [] }
11
+ @lock = Mutex.new
12
+ @pid = Process.pid
13
+ end
14
+
15
+ def checkout(uri, timeout)
16
+ while (pooled = take(uri))
17
+ return [pooled, true] if usable?(pooled)
18
+
19
+ discard(pooled)
20
+ end
21
+
22
+ [Connector.open(uri, timeout, @keep_alive_timeout), false]
23
+ end
24
+
25
+ def checkin(uri, connection)
26
+ kept = @lock.synchronize do
27
+ room = @pid == Process.pid && @idle[key(uri)].size < @max_idle
28
+ @idle[key(uri)].push(connection) if room
29
+ room
30
+ end
31
+
32
+ discard(connection) unless kept
33
+ end
34
+
35
+ def discard(connection)
36
+ connection.finish if connection.started?
37
+ rescue *CLOSE_ERRORS
38
+ nil
39
+ end
40
+
41
+ def close
42
+ drained = @lock.synchronize do
43
+ connections = @idle.values.flatten
44
+ connections.clear unless @pid == Process.pid
45
+ @idle.clear
46
+ @pid = Process.pid
47
+ connections
48
+ end
49
+
50
+ drained.each { |connection| discard(connection) }
51
+ nil
52
+ end
53
+
54
+ private
55
+
56
+ def take(uri)
57
+ @lock.synchronize do
58
+ forget_inherited
59
+ @idle[key(uri)].pop
60
+ end
61
+ end
62
+
63
+ def usable?(connection)
64
+ io = connection.instance_variable_get(:@socket)&.io
65
+ return true if io.nil?
66
+
67
+ !io.closed? && !io.to_io.wait_readable(0)
68
+ rescue *CLOSE_ERRORS
69
+ false
70
+ end
71
+
72
+ def forget_inherited
73
+ return if @pid == Process.pid
74
+
75
+ @idle.clear
76
+ @pid = Process.pid
77
+ end
78
+
79
+ def key(uri)
80
+ [uri.scheme.to_s.downcase, uri.hostname.to_s.downcase, uri.port]
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envless
4
+ module Connector
5
+ module_function
6
+
7
+ def open(uri, timeout, keep_alive_timeout)
8
+ proxy = uri.find_proxy
9
+ connection = Net::HTTP.new(uri.hostname.to_s, uri.port, proxy&.hostname, proxy&.port, decoded(proxy&.user),
10
+ decoded(proxy&.password))
11
+ connection.use_ssl = uri.scheme.to_s.casecmp?("https")
12
+ connection.min_version = OpenSSL::SSL::TLS1_2_VERSION if connection.use_ssl?
13
+ connection.open_timeout = timeout
14
+ connection.read_timeout = timeout
15
+ connection.write_timeout = timeout
16
+ connection.keep_alive_timeout = keep_alive_timeout
17
+ connection.max_retries = 0
18
+ connection.ignore_eof = false
19
+ connection.start
20
+ end
21
+
22
+ def decoded(text)
23
+ text && URI.decode_uri_component(text)
24
+ end
25
+ end
26
+ end