bullion 0.11.5 → 0.11.6

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: f1e03e0297625063a198db254192ac85e7389a46bd1ebd0be1161f1221300e4d
4
- data.tar.gz: 2c5a0729d0521920a378fa235f928b6c7eb4c16cd3ee9a63bf74fa89eca7168c
3
+ metadata.gz: 44ad24a873adf3bab2e535688588282797d477c7f973835094b52d00cb9c6592
4
+ data.tar.gz: ee339845aaaca5001f5a33f21c75448c8d59b19893191bbc91055d41b13edc66
5
5
  SHA512:
6
- metadata.gz: 06e5aa78438a991e3e2cf9db8519a2c81bb35c3b872f1fc511ca7ea6d397088e3fce4642631d3dd7d77e9105e9fb4b808dfdc2b185fc121d3deebf8f3614e0d2
7
- data.tar.gz: e3809f863bd43ff203a580b65a36e6d1f8f088cf12b1650ab7057cca3220019db00bfb231f7d85bbce983600d6488cd75f2748d3c164692c41646bbc9166c043
6
+ metadata.gz: 2f663b5f7aedb812648c9065fd324ab0ab70a49b4ecb58c2b1160e12c84c93a5e340512be14812ec1e9d6790cce41344182df20e942d4ade2383ff81cf686dae
7
+ data.tar.gz: 7145c2c82d0e710de29bfc97dbe481464696d74a5381ac4dd55e56bd06605dc225dc6bd2a0593df459413d962c9583f5b3e6d133757b1e5ed75654af880b271f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.6](https://github.com/jgnagy/bullion/compare/bullion/v0.11.5...bullion/v0.11.6) (2026-10-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * give each request fiber its own database connection ([c47a277](https://github.com/jgnagy/bullion/commit/c47a2779ab6b115bbb0c9af75559c17010353962))
9
+ * give each request fiber its own database connection ([cca3b31](https://github.com/jgnagy/bullion/commit/cca3b313a6adf04ef9680698ca64f53027b58d3b))
10
+ * return orderNotReady instead of badCSR for non-ready finalize ([2d5fc6f](https://github.com/jgnagy/bullion/commit/2d5fc6f15f98ba39253b294868674073d56e5f81))
11
+
3
12
  ## [0.11.5](https://github.com/jgnagy/bullion/compare/bullion/v0.11.4...bullion/v0.11.5) (2026-09-21)
4
13
 
5
14
 
data/README.md CHANGED
@@ -160,7 +160,7 @@ Whether run locally or via Docker, the following environment variables configure
160
160
  | `CA_CERT_PATH` | `$CA_DIR/tls.crt` | Public cert for Bullion. If Bullion is an intermediate CA, you'll want to include the root CA's public cert in this file as well the signed cert for Bullion. |
161
161
  | `CA_DOMAINS` | `example.com` | A comma-delimited list of domains for which Bullion will sign certificate requests. Subdomains are automatically allowed. Certificates containing other domains will be rejected. |
162
162
  | `CERT_VALIDITY_DURATION` | `7776000` | How long should issued certs be valid (in seconds)? Default is 90 days. |
163
- | `DATABASE_URL` | _None_ | **(Required)** A shorthand for telling Bullion how to connect to a database. Acceptable URLs will either begin with `sqlite3:` or [`trilogy://`](https://github.com/trilogy-libraries/trilogy/tree/main/contrib/ruby). |
163
+ | `DATABASE_URL` | _None_ | **(Required)** A shorthand for telling Bullion how to connect to a database. Acceptable URLs will either begin with `sqlite3:` or [`trilogy://`](https://github.com/trilogy-libraries/trilogy/tree/main/contrib/ruby). ActiveRecord connection options may be passed as query parameters, e.g. `trilogy://user:pass@host:3306/bullion?max_connections=25&checkout_timeout=5`. |
164
164
  | `DNS01_NAMESERVERS` | _None_ | A comma-delimited list of nameservers to use for resolving [DNS-01](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) challenges. Usually you'll want this to be set to your _internal_ nameservers so internal names resolve correctly. When not set, it'll use the host's DNS. |
165
165
  | `LOG_LEVEL` | `warn` | Log level for Bullion. Supported levels (starting with the noisiest) are debug, info, warn, error, and fatal. |
166
166
  | `BULLION_PORT` | `9292` | TCP port Bullion will listen on. |
@@ -169,6 +169,8 @@ Whether run locally or via Docker, the following environment variables configure
169
169
  | `WORKER_MEMORY_LIMIT` | `1024**3` | [Itsi worker memory limit](https://itsi.fyi/options/worker_memory_limit/) for each worker process (in bytes). Default is 1GiB. |
170
170
  | `RACK_ENV` | `production`* | When run via Docker, the default is `production`, when run via `rake local_demo` it is `development`. Used to tell Bullion if it is run in development mode or for testing. |
171
171
 
172
+ Because Bullion serves requests through Itsi's [fiber scheduler](https://itsi.fyi/options/fiber_scheduler/), each in-flight request leases its own database connection. ActiveRecord's connection pool defaults to 5 connections, so raise `max_connections` on `DATABASE_URL` if you expect more concurrent requests. `checkout_timeout` (default 5 seconds) controls how long a request waits for a connection before failing.
173
+
172
174
  ### Integrating
173
175
 
174
176
  Any client that speaks the [ACMEv2](https://tools.ietf.org/html/rfc8555) protocol can be pointed at `/acme/directory` and everything else can be auto-discovered.
data/db/schema.rb CHANGED
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[8.1].define(version: 2025_08_23_073342) do
13
+ ActiveRecord::Schema[8.1].define(version: 2026_07_16_000000) do
14
14
  create_table "accounts", force: :cascade do |t|
15
15
  t.text "contacts", null: false
16
16
  t.datetime "created_at", null: false
@@ -40,6 +40,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_08_23_073342) do
40
40
  t.string "csr_fingerprint", null: false
41
41
  t.text "data", null: false
42
42
  t.string "requester"
43
+ t.integer "revocation_reason"
44
+ t.boolean "revoked", default: false, null: false
45
+ t.datetime "revoked_at"
43
46
  t.bigint "serial", null: false
44
47
  t.string "subject", null: false
45
48
  t.datetime "updated_at", null: false
@@ -64,6 +64,11 @@ module Bullion
64
64
  def acme_type = "malformed"
65
65
  end
66
66
 
67
+ # ACME exception for orders that aren't ready to be finalized
68
+ class OrderNotReady < Bullion::Acme::Error
69
+ def acme_type = "orderNotReady"
70
+ end
71
+
67
72
  # ACME exception for unsupported contacts in accounts
68
73
  class UnsupportedContact < Bullion::Acme::Error
69
74
  def acme_type = "unsupportedContact"
@@ -125,7 +125,6 @@ module Bullion
125
125
  # Make sure the CSR has a valid public key
126
126
  raise Bullion::Acme::Errors::BadCsr unless csr.verify(csr.public_key)
127
127
 
128
- return false unless order.ready_status?
129
128
  raise Bullion::Acme::Errors::BadCsr if csr_cn && !csr_domains.include?(csr_cn)
130
129
  raise Bullion::Acme::Errors::BadCsr unless csr_domains.sort == order.domains.sort
131
130
 
@@ -192,6 +191,24 @@ module Bullion
192
191
  Bullion.config.ca.domains.none? { domain["value"].end_with?(it) }
193
192
  end
194
193
  end
194
+
195
+ # Builds the ACMEv2 order resource representation
196
+ # @see https://tools.ietf.org/html/rfc8555#section-7.1.3
197
+ def order_data(order)
198
+ data = {
199
+ status: order.status,
200
+ expires: order.expires,
201
+ notBefore: order.not_before,
202
+ notAfter: order.not_after,
203
+ identifiers: order.identifiers,
204
+ authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
205
+ finalize: uri("/orders/#{order.id}/finalize")
206
+ }
207
+
208
+ data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
209
+
210
+ data
211
+ end
195
212
  end
196
213
  end
197
214
  end
@@ -214,15 +214,7 @@ module Bullion
214
214
  content_type "application/json"
215
215
  add_acme_headers @new_nonce, additional: { "Location" => uri("/orders/#{order.id}") }
216
216
 
217
- halt 201, {
218
- status: order.status,
219
- expires: order.expires,
220
- notBefore: order.not_before,
221
- notAfter: order.not_after,
222
- identifiers: order.identifiers,
223
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
224
- finalize: uri("/orders/#{order.id}/finalize")
225
- }.to_json
217
+ halt 201, order_data(order).to_json
226
218
  rescue Bullion::Acme::Error => e
227
219
  content_type "application/problem+json"
228
220
  halt 400, { type: e.acme_error, detail: e.message }.to_json
@@ -237,19 +229,7 @@ module Bullion
237
229
 
238
230
  order = Models::Order.find(params[:id])
239
231
 
240
- data = {
241
- status: order.status,
242
- expires: order.expires,
243
- notBefore: order.not_before,
244
- notAfter: order.not_after,
245
- identifiers: order.identifiers,
246
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
247
- finalize: uri("/orders/#{order.id}/finalize")
248
- }
249
-
250
- data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
251
-
252
- data.to_json
232
+ order_data(order).to_json
253
233
  rescue Bullion::Acme::Error => e
254
234
  content_type "application/problem+json"
255
235
  halt 400, { type: e.acme_error, detail: e.message }.to_json
@@ -267,6 +247,17 @@ module Bullion
267
247
 
268
248
  order_csr = Models::OrderCsr.from_acme_request(order, @payload_data["csr"])
269
249
 
250
+ unless order.ready_status?
251
+ # Finalize is idempotent for orders that have already been finalized
252
+ halt 200, order_data(order).to_json if %w[processing valid].include?(order.status)
253
+
254
+ content_type "application/problem+json"
255
+ halt 403, {
256
+ type: Bullion::Acme::Errors::OrderNotReady.new.acme_error,
257
+ detail: "Order is not ready to be finalized"
258
+ }.to_json
259
+ end
260
+
270
261
  unless acme_csr_valid?(order_csr)
271
262
  content_type "application/problem+json"
272
263
  halt 400, {
@@ -281,19 +272,7 @@ module Bullion
281
272
  order.status = "valid"
282
273
  order.save
283
274
 
284
- data = {
285
- status: order.status,
286
- expires: order.expires,
287
- notBefore: order.not_before,
288
- notAfter: order.not_after,
289
- identifiers: order.identifiers,
290
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
291
- finalize: uri("/orders/#{order.id}/finalize")
292
- }
293
-
294
- data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
295
-
296
- data.to_json
275
+ order_data(order).to_json
297
276
  rescue Bullion::Acme::Error => e
298
277
  content_type "application/problem+json"
299
278
  halt 422, { type: e.acme_error, detail: e.message }.to_json
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullion
4
- VERSION = "0.11.5"
4
+ VERSION = "0.11.6"
5
5
  end
data/lib/bullion.rb CHANGED
@@ -140,6 +140,19 @@ Bullion.configure do |config|
140
140
  config.nameservers = nameservers if nameservers
141
141
  end
142
142
 
143
+ # Itsi processes each request inside its own Fiber, but all fibers on a thread
144
+ # share an ActiveSupport execution context by default. ActiveRecord leases a
145
+ # database connection per execution context, so without fiber isolation every
146
+ # concurrent request fiber on a thread shares (and races on) the same
147
+ # connection. Trilogy detects this and raises Trilogy::SynchronizationError.
148
+ # Mirror Itsi's own scheduler_mode hook (which itsi-server 0.2.27 never loads)
149
+ # and the Rails 8.1+ default by opting into fiber isolation, honoring Itsi's
150
+ # opt-out.
151
+ # @see https://github.com/jgnagy/bullion/issues/116
152
+ unless ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
153
+ ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
154
+ end
155
+
143
156
  # Internal requirements
144
157
  require "bullion/version"
145
158
  require "bullion/acme/error"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.5
4
+ version: 0.11.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy