safire 0.2.0 → 0.4.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/release-safire/SKILL.md +153 -0
  3. data/.rubocop.yml +11 -1
  4. data/.tool-versions +1 -1
  5. data/CHANGELOG.md +107 -1
  6. data/CONTRIBUTION.md +6 -1
  7. data/Gemfile +1 -2
  8. data/Gemfile.lock +47 -37
  9. data/README.md +60 -5
  10. data/ROADMAP.md +61 -12
  11. data/docs/Gemfile.lock +49 -45
  12. data/docs/_config.yml +2 -2
  13. data/docs/adr/ADR-001-activesupport-dependency.md +2 -2
  14. data/docs/adr/ADR-002-facade-and-forwardable.md +21 -6
  15. data/docs/adr/ADR-003-protocol-vs-client-type.md +13 -6
  16. data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +34 -8
  17. data/docs/adr/ADR-006-lazy-discovery.md +64 -5
  18. data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +40 -10
  19. data/docs/adr/ADR-009-oauth-error-hierarchy.md +131 -0
  20. data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +90 -0
  21. data/docs/adr/ADR-011-udap-stu2-discovery-conformance.md +113 -0
  22. data/docs/adr/ADR-012-udap-signed-metadata-validation.md +104 -0
  23. data/docs/adr/ADR-013-udap-registration-request-model.md +106 -0
  24. data/docs/adr/ADR-014-udap-software-statement-signing.md +122 -0
  25. data/docs/adr/index.md +9 -3
  26. data/docs/advanced.md +22 -25
  27. data/docs/configuration/client-setup.md +126 -10
  28. data/docs/configuration/index.md +11 -7
  29. data/docs/index.md +13 -6
  30. data/docs/installation.md +3 -2
  31. data/docs/security.md +44 -5
  32. data/docs/smart-on-fhir/backend-services/index.md +2 -2
  33. data/docs/smart-on-fhir/backend-services/token-request.md +1 -1
  34. data/docs/smart-on-fhir/confidential-asymmetric/index.md +2 -2
  35. data/docs/smart-on-fhir/confidential-symmetric/index.md +1 -1
  36. data/docs/smart-on-fhir/discovery/capability-checks.md +7 -0
  37. data/docs/smart-on-fhir/dynamic-client-registration/index.md +103 -0
  38. data/docs/smart-on-fhir/dynamic-client-registration/registration.md +160 -0
  39. data/docs/smart-on-fhir/dynamic-client-registration/response.md +161 -0
  40. data/docs/smart-on-fhir/index.md +2 -1
  41. data/docs/smart-on-fhir/post-based-authorization.md +1 -1
  42. data/docs/smart-on-fhir/public-client/index.md +1 -1
  43. data/docs/troubleshooting/auth-errors.md +20 -0
  44. data/docs/troubleshooting/client-errors.md +143 -0
  45. data/docs/troubleshooting/index.md +61 -4
  46. data/docs/udap/dynamic-client-registration/index.md +198 -0
  47. data/docs/udap/dynamic-client-registration/lifecycle.md +115 -0
  48. data/docs/udap/dynamic-client-registration/registration-metadata.md +133 -0
  49. data/docs/udap/dynamic-client-registration/software-statement.md +115 -0
  50. data/docs/udap.md +187 -64
  51. data/gemfiles/activesupport_71.gemfile +30 -0
  52. data/gemfiles/activesupport_71.gemfile.lock +291 -0
  53. data/lib/safire/client.rb +141 -44
  54. data/lib/safire/client_config.rb +114 -35
  55. data/lib/safire/client_config_builder.rb +18 -0
  56. data/lib/safire/errors.rb +100 -44
  57. data/lib/safire/http_client.rb +7 -2
  58. data/lib/safire/middleware/https_only_redirects.rb +10 -4
  59. data/lib/safire/protocols/behaviours.rb +12 -1
  60. data/lib/safire/protocols/oauth_response_handling.rb +48 -0
  61. data/lib/safire/protocols/smart.rb +108 -36
  62. data/lib/safire/protocols/smart_metadata.rb +6 -0
  63. data/lib/safire/protocols/udap.rb +534 -0
  64. data/lib/safire/protocols/udap_metadata.rb +429 -0
  65. data/lib/safire/protocols/udap_registration_metadata.rb +368 -0
  66. data/lib/safire/protocols/udap_signed_metadata_validator.rb +349 -0
  67. data/lib/safire/protocols/udap_software_statement.rb +348 -0
  68. data/lib/safire/protocols.rb +6 -0
  69. data/lib/safire/uri_validation.rb +102 -0
  70. data/lib/safire/version.rb +1 -1
  71. data/lib/safire.rb +2 -0
  72. data/safire.gemspec +7 -8
  73. metadata +42 -13
data/docs/udap.md CHANGED
@@ -2,7 +2,9 @@
2
2
  layout: default
3
3
  title: UDAP
4
4
  nav_order: 5
5
- description: "Overview of planned UDAP Security protocol support in Safire, including UDAP discovery, dynamic client registration, JWT client authentication, and tiered OAuth for healthcare data exchange."
5
+ permalink: /udap/
6
+ description: "UDAP Security STU2 discovery and Dynamic Client Registration in Safire."
7
+ has_children: true
6
8
  ---
7
9
 
8
10
  # UDAP
@@ -10,70 +12,191 @@ description: "Overview of planned UDAP Security protocol support in Safire, incl
10
12
  {: .no_toc }
11
13
 
12
14
  <div class="code-example" markdown="1">
13
- **Status:** Planned — see [ROADMAP.md](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md) for timeline and progress.
15
+ Safire implements UDAP Security STU2 discovery, signed metadata validation,
16
+ community scoping, and certificate-backed Dynamic Client Registration for new
17
+ registration, modification, and cancellation. JWT client authentication and
18
+ Tiered OAuth remain planned.
14
19
  </div>
15
20
 
16
- ## Table of contents
17
- {: .no_toc .text-delta }
18
-
19
- 1. TOC
20
- {:toc}
21
-
22
- ---
23
-
24
21
  ## Overview
25
22
 
26
- UDAP (Unified Data Access Profiles) is a security framework for healthcare data exchange defined by the [UDAP Security Implementation Guide](https://hl7.org/fhir/us/udap-security/). It extends standard OAuth 2.0 with X.509 certificate-based identity, dynamic client registration, and trust community models — designed primarily for backend system-to-system integration and cross-organizational data access.
27
-
28
- UDAP is a separate protocol from SMART. In Safire, it is selected via `protocol: :udap` rather than a `client_type:`. Watch the [GitHub repository](https://github.com/vanessuniq/safire) for release announcements.
29
-
30
- ---
31
-
32
- ## Planned Features
33
-
34
- ### Discovery
35
-
36
- - **UDAP Discovery** (`/.well-known/udap`) — fetch server metadata and trust anchors
37
-
38
- ### Client Flows
39
-
40
- - **Dynamic Client Registration (DCR)** — one-time registration using a signed software statement to obtain a `client_id`; required only when the client has not previously registered with the server and if the server supports DCR
41
- - **JWT Client Authentication** — authenticate on every request using a signed JWT assertion (Authentication Token, AnT) with an X.509 certificate chain in the `x5c` header; the registered `client_id` is reused as `iss` and `sub` in each assertion
42
- - **Tiered OAuth** — delegated authorization for multi-system access per the UDAP Security IG
43
- - **Pushed Authorization Requests (RFC 9126)** — PAR support for pre-registering authorization requests
44
-
45
- ### Trust Framework
46
-
47
- - **Certificate Validation** — verify server and client certificates against trust anchors
48
- - **Trust Community Support** — integration with UDAP trust communities (e.g. Carequality, CommonWell)
49
-
50
- ---
51
-
52
- ## When to Use UDAP
53
-
54
- | Scenario | Why UDAP |
55
- |----------|----------|
56
- | **Backend / B2B Integration** | Server-to-server flows without user interaction; certificate-based identity replaces pre-shared secrets |
57
- | **Dynamic Client Registration** | Clients can register programmatically without manual server-side approval |
58
- | **Cross-Organization Access** | Trust communities allow clients to be recognized across participant organizations without per-server registration |
59
- | **High-Assurance Identity** | X.509 certificates provide stronger identity guarantees than client secrets |
60
-
61
- ---
62
-
63
- ## Comparison with SMART
64
-
65
- | Feature | SMART | UDAP |
66
- |---------|---------------|------|
67
- | Primary use case | User-facing apps, EHR launch | B2B, backend services, cross-org access |
68
- | Client registration | Pre-registered per server, optional DCR (recommended) | Dynamic (DCR) or pre-registered |
69
- | Authentication | Client secrets or `private_key_jwt` | Signed JWT assertions (AnT) with X.509 `x5c` chain |
70
- | Trust model | Per-server registration | Certificate-based trust communities |
71
- | Safire selection | `client_type: :public / :confidential_symmetric / :confidential_asymmetric` | `protocol: :udap` (planned) |
72
-
73
- ### Resources
74
-
75
- - [UDAP Security IG](https://hl7.org/fhir/us/udap-security/) — HL7 Implementation Guide
76
- - [UDAP JWT Client Auth](https://www.udap.org/udap-jwt-client-auth.html) — JWT assertion specification
77
- - [UDAP Dynamic Client Registration](https://www.udap.org/udap-dynamic-client-registration.html) — DCR specification
78
- - [RFC 9126 — Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126)
79
- - [UDAP Tiered OAuth](https://hl7.org/fhir/us/udap-security/b2b.html) — Delegated authorization
23
+ UDAP (Unified Data Access Profiles) is a security framework for healthcare data
24
+ exchange defined by the
25
+ [UDAP Security STU2 / v2.0.0 Implementation Guide](https://hl7.org/fhir/us/udap-security/STU2/index.html).
26
+ It adds X.509-based identity, signed metadata, dynamic registration, and trust
27
+ community semantics to OAuth-based workflows.
28
+
29
+ Select UDAP independently from SMART client authentication types:
30
+
31
+ ```ruby
32
+ client = Safire::Client.new(
33
+ { base_url: 'https://fhir.example.com' },
34
+ protocol: :udap
35
+ )
36
+ ```
37
+
38
+ `client_type:` is not applicable to UDAP. Passing one explicitly, or assigning
39
+ `client.client_type = ...` on a UDAP client, raises
40
+ `Safire::Errors::ConfigurationError`.
41
+
42
+ ## Discovery
43
+
44
+ `server_metadata` fetches `/.well-known/udap`, validates the required
45
+ `signed_metadata` JWT, and merges authoritative signed endpoint claims over
46
+ their unsigned JSON counterparts before returning `UdapMetadata`.
47
+
48
+ ### Server trust and revocation
49
+
50
+ Production discovery requires caller-supplied trust anchors plus an explicit
51
+ revocation policy. Use CRLs or a custom checker:
52
+
53
+ ```ruby
54
+ ca_cert = OpenSSL::X509::Certificate.new(File.read('udap-ca.pem'))
55
+ ca_crl = OpenSSL::X509::CRL.new(File.read('udap-ca.crl'))
56
+
57
+ metadata = client.server_metadata(
58
+ trusted_anchors: [ca_cert],
59
+ crls: [ca_crl]
60
+ )
61
+ ```
62
+
63
+ Safire verifies the JWT signature, certificate chain and revocation status,
64
+ issuer/SAN relationship, time claims, `jti`, and signed endpoint claims. Results
65
+ are cached per community and trust policy, but cache hits revalidate the signed
66
+ metadata before reuse. An expired or newly untrusted entry is discarded and
67
+ refetched.
68
+
69
+ {: .warning }
70
+ > `verify_chain: false` skips X.509 chain and revocation validation. Use it only
71
+ > for development and tests. Local HTTP loopback servers separately require
72
+ > `allow_insecure_localhost: true` in `ClientConfig`; this permits HTTP only on
73
+ > `localhost` and `127.0.0.1` and is not production UDAP conformance.
74
+
75
+ ### Community-scoped discovery
76
+
77
+ Pass a trust-community URI when a server supports multiple communities:
78
+
79
+ ```ruby
80
+ metadata = client.server_metadata(
81
+ community: 'https://udap.example.org/community1',
82
+ trusted_anchors: [ca_cert],
83
+ crls: [ca_crl]
84
+ )
85
+ ```
86
+
87
+ Different community, trust-anchor, CRL, revocation-checker, or chain-verification
88
+ arguments use separate cache entries.
89
+
90
+ ### Structural and capability helpers
91
+
92
+ `UdapMetadata#valid?` checks required fields, exact STU2 fixed values, array and
93
+ string types, conditional fields, subset rules, and endpoint URL shape. It logs
94
+ each violation and returns `false`; it does not repeat cryptographic validation.
95
+
96
+ ```ruby
97
+ unless metadata.valid?
98
+ # Safire.logger.warn has already recorded each structural violation.
99
+ raise 'UDAP metadata is structurally non-conformant'
100
+ end
101
+ ```
102
+
103
+ Profile-only helpers inspect raw profile advertisements. Capability helpers add
104
+ the endpoint or grant preconditions Safire can verify locally:
105
+
106
+ | Capability helper | Checks |
107
+ |-------------------|--------|
108
+ | `supports_dynamic_registration?` | `udap_dcr` profile plus a valid `registration_endpoint` |
109
+ | `supports_jwt_client_auth?` | `udap_authn` profile plus a valid `token_endpoint` |
110
+ | `supports_client_authorization?` | `udap_authz`, `client_credentials`, and a valid `token_endpoint` |
111
+ | `supports_authorization_code?` | `authorization_code` grant advertisement |
112
+ | `supports_refresh_token?` | `refresh_token` grant advertisement |
113
+ | `supports_tiered_oauth?` | `udap_to` profile advertisement |
114
+ | `supports_signed_metadata?` | Compact-JWS-shaped `signed_metadata` value |
115
+
116
+ The corresponding profile helpers are `dynamic_registration_profile?`,
117
+ `jwt_client_auth_profile?`, `client_authorization_profile?`, and
118
+ `tiered_oauth_profile?`.
119
+
120
+ Use `signed_metadata_valid?(base_url:, ...)` only when explicitly revalidating
121
+ an existing metadata object against another trust policy. Metadata returned by
122
+ `server_metadata` is already cryptographically validated.
123
+
124
+ ### Discovery failures
125
+
126
+ | Condition | Safire behavior |
127
+ |-----------|-----------------|
128
+ | `404 Not Found` | Raises `DiscoveryError`; clients treat the server as not supporting UDAP workflows |
129
+ | `204 No Content` | Raises `DiscoveryError` before body parsing; no UDAP workflow is available for that community |
130
+ | Invalid `signed_metadata` | Raises `DiscoveryError` after validation warnings |
131
+ | Malformed DER in JOSE `x5c` | Raises `CertificateError` |
132
+ | Invalid `community:` | Raises `ConfigurationError` before HTTP |
133
+ | Connection, TLS, timeout, or blocked redirect failure | Raises `NetworkError` |
134
+
135
+ ## Dynamic Client Registration
136
+
137
+ UDAP DCR is discovery-bound. Safire validates discovery metadata, signs
138
+ caller-controlled registration metadata into a short-lived X.509-backed
139
+ `software_statement`, and POSTs the fixed UDAP envelope to the authoritative
140
+ signed `registration_endpoint`.
141
+
142
+ ```ruby
143
+ client = Safire::Client.new(
144
+ {
145
+ base_url: 'https://fhir.example.com',
146
+ private_key: File.read('client-key.pem'),
147
+ certificate_chain: [File.read('client-cert.pem')]
148
+ },
149
+ protocol: :udap
150
+ )
151
+
152
+ registration = client.register_client(
153
+ {
154
+ client_name: 'Example Backend Service',
155
+ contacts: ['mailto:security@example.com'],
156
+ grant_types: ['client_credentials'],
157
+ scope: 'system/Patient.rs'
158
+ },
159
+ client_uri: 'https://client.example.com',
160
+ trusted_anchors: [ca_cert],
161
+ crls: [ca_crl]
162
+ )
163
+ ```
164
+
165
+ Use `register_client` again with the same client URI and community to request a
166
+ modification. Use `cancel_registration` to send a fresh software statement with
167
+ an empty `grant_types` array. Safire accepts cancellation only when a successful
168
+ response contains a non-blank `client_id` and an empty `grant_types` array.
169
+
170
+ Pass `certifications:` when required by community policy. Certification and
171
+ endorsement JWTs may be signed by the client operator or a third party. Safire
172
+ validates their compact-JWS shape but leaves issuer, signature, claims, and
173
+ policy evaluation to the authorization server.
174
+
175
+ See [Dynamic Client Registration]({% link udap/dynamic-client-registration/index.md %})
176
+ for prerequisites and public API usage, [Registration Metadata]({% link udap/dynamic-client-registration/registration-metadata.md %})
177
+ for input rules, [Software Statements]({% link udap/dynamic-client-registration/software-statement.md %})
178
+ for signing behavior, and [Registration Lifecycle]({% link udap/dynamic-client-registration/lifecycle.md %})
179
+ for modification and cancellation semantics.
180
+
181
+ ## Scope and Protocol Choice
182
+
183
+ Safire does not yet implement UDAP JWT client authentication, token acquisition,
184
+ authorization-code handling, or Tiered OAuth. Those methods continue to raise
185
+ `NotImplementedError` on a UDAP client. Dynamic registration obtains a
186
+ `client_id`, but the later UDAP authentication and authorization flows remain
187
+ separate roadmap work.
188
+
189
+ | Feature | SMART App Launch in Safire | UDAP Security in Safire |
190
+ |---------|----------------------------|--------------------------|
191
+ | Discovery | SMART configuration JSON | Signed UDAP metadata with X.509 trust validation |
192
+ | Dynamic registration | RFC 7591 metadata request | STU2 X.509-backed software statement |
193
+ | Implemented client flows | App Launch and Backend Services | Registration lifecycle only |
194
+ | Client selection | `client_type:` | `protocol: :udap`; no `client_type:` |
195
+ | Trust model | Per-server client registration and registered keys/secrets | Certificate trust communities and signed metadata |
196
+
197
+ Resources:
198
+
199
+ - [UDAP Security STU2 / v2.0.0 IG](https://hl7.org/fhir/us/udap-security/STU2/index.html)
200
+ - [UDAP Dynamic Client Registration STU1](https://www.udap.org/udap-dynamic-client-registration-stu1.html)
201
+ - [UDAP Certifications and Endorsements STU1](https://www.udap.org/udap-certifications-and-endorsements-stu1.html)
202
+ - [Safire roadmap](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'activesupport', '~> 7.1.0'
6
+
7
+ gemspec path: '..'
8
+
9
+ group :development, :test do
10
+ gem 'bundler-audit', '~> 0.9'
11
+ gem 'debug'
12
+ gem 'pry'
13
+ gem 'pry-byebug'
14
+ gem 'rspec', '~> 3.12'
15
+ gem 'rubocop', '~> 1.57'
16
+ gem 'rubocop-rspec', '~> 3.0', require: false
17
+ gem 'timecop', '~> 0.9'
18
+ end
19
+
20
+ group :development do
21
+ gem 'yard', '~> 0.9'
22
+ end
23
+
24
+ group :test do
25
+ gem 'dotenv', '~> 3.0'
26
+ gem 'simplecov', require: false
27
+ gem 'simplecov-cobertura', require: false
28
+ gem 'sinatra', '~> 4.0'
29
+ gem 'webmock', '~> 3.18'
30
+ end
@@ -0,0 +1,291 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ safire (0.3.0)
5
+ activesupport (>= 7.1, < 9)
6
+ addressable (~> 2.8)
7
+ faraday (~> 2.14)
8
+ faraday-follow_redirects (~> 0.4)
9
+ jwt (~> 2.8)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ activesupport (7.1.6)
15
+ base64
16
+ benchmark (>= 0.3)
17
+ bigdecimal
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ connection_pool (>= 2.2.5)
20
+ drb
21
+ i18n (>= 1.6, < 2)
22
+ logger (>= 1.4.2)
23
+ minitest (>= 5.1)
24
+ mutex_m
25
+ securerandom (>= 0.3)
26
+ tzinfo (~> 2.0)
27
+ addressable (2.9.0)
28
+ public_suffix (>= 2.0.2, < 8.0)
29
+ ast (2.4.3)
30
+ base64 (0.3.0)
31
+ benchmark (0.5.0)
32
+ bigdecimal (4.1.2)
33
+ bundler-audit (0.9.3)
34
+ bundler (>= 1.2.0)
35
+ thor (~> 1.0)
36
+ byebug (13.0.0)
37
+ reline (>= 0.6.0)
38
+ coderay (1.1.3)
39
+ concurrent-ruby (1.3.7)
40
+ connection_pool (3.0.2)
41
+ crack (1.0.1)
42
+ bigdecimal
43
+ rexml
44
+ date (3.5.1)
45
+ debug (1.11.1)
46
+ irb (~> 1.10)
47
+ reline (>= 0.3.8)
48
+ diff-lcs (1.6.2)
49
+ docile (1.4.1)
50
+ dotenv (3.2.0)
51
+ drb (2.2.3)
52
+ erb (6.0.4)
53
+ faraday (2.14.3)
54
+ faraday-net_http (>= 2.0, < 3.5)
55
+ json
56
+ logger
57
+ faraday-follow_redirects (0.5.0)
58
+ faraday (>= 1, < 3)
59
+ faraday-net_http (3.4.4)
60
+ net-http (~> 0.5)
61
+ hashdiff (1.2.1)
62
+ i18n (1.15.0)
63
+ concurrent-ruby (~> 1.0)
64
+ io-console (0.8.2)
65
+ irb (1.18.0)
66
+ pp (>= 0.6.0)
67
+ prism (>= 1.3.0)
68
+ rdoc (>= 4.0.0)
69
+ reline (>= 0.4.2)
70
+ json (2.19.9)
71
+ jwt (2.10.3)
72
+ base64
73
+ language_server-protocol (3.17.0.5)
74
+ lint_roller (1.1.0)
75
+ logger (1.7.0)
76
+ method_source (1.1.0)
77
+ minitest (5.27.0)
78
+ mustermann (3.1.1)
79
+ mutex_m (0.3.0)
80
+ net-http (0.9.1)
81
+ uri (>= 0.11.1)
82
+ parallel (2.1.0)
83
+ parser (3.3.11.1)
84
+ ast (~> 2.4.1)
85
+ racc
86
+ pp (0.6.3)
87
+ prettyprint
88
+ prettyprint (0.2.0)
89
+ prism (1.9.0)
90
+ pry (0.16.0)
91
+ coderay (~> 1.1)
92
+ method_source (~> 1.0)
93
+ reline (>= 0.6.0)
94
+ pry-byebug (3.12.0)
95
+ byebug (~> 13.0)
96
+ pry (>= 0.13, < 0.17)
97
+ psych (5.4.0)
98
+ date
99
+ stringio
100
+ public_suffix (7.0.5)
101
+ racc (1.8.1)
102
+ rack (3.2.6)
103
+ rack-protection (4.2.1)
104
+ base64 (>= 0.1.0)
105
+ logger (>= 1.6.0)
106
+ rack (>= 3.0.0, < 4)
107
+ rack-session (2.1.2)
108
+ base64 (>= 0.1.0)
109
+ rack (>= 3.0.0)
110
+ rainbow (3.1.1)
111
+ rdoc (7.2.0)
112
+ erb
113
+ psych (>= 4.0.0)
114
+ tsort
115
+ regexp_parser (2.12.0)
116
+ reline (0.6.3)
117
+ io-console (~> 0.5)
118
+ rexml (3.4.4)
119
+ rspec (3.13.2)
120
+ rspec-core (~> 3.13.0)
121
+ rspec-expectations (~> 3.13.0)
122
+ rspec-mocks (~> 3.13.0)
123
+ rspec-core (3.13.6)
124
+ rspec-support (~> 3.13.0)
125
+ rspec-expectations (3.13.5)
126
+ diff-lcs (>= 1.2.0, < 2.0)
127
+ rspec-support (~> 3.13.0)
128
+ rspec-mocks (3.13.8)
129
+ diff-lcs (>= 1.2.0, < 2.0)
130
+ rspec-support (~> 3.13.0)
131
+ rspec-support (3.13.7)
132
+ rubocop (1.88.0)
133
+ json (~> 2.3)
134
+ language_server-protocol (~> 3.17.0.2)
135
+ lint_roller (~> 1.1.0)
136
+ parallel (>= 1.10)
137
+ parser (>= 3.3.0.2)
138
+ rainbow (>= 2.2.2, < 4.0)
139
+ regexp_parser (>= 2.9.3, < 3.0)
140
+ rubocop-ast (>= 1.49.0, < 2.0)
141
+ ruby-progressbar (~> 1.7)
142
+ unicode-display_width (>= 2.4.0, < 4.0)
143
+ rubocop-ast (1.49.1)
144
+ parser (>= 3.3.7.2)
145
+ prism (~> 1.7)
146
+ rubocop-rspec (3.10.2)
147
+ lint_roller (~> 1.1)
148
+ regexp_parser (>= 2.0)
149
+ rubocop (~> 1.86, >= 1.86.2)
150
+ ruby-progressbar (1.13.0)
151
+ securerandom (0.4.1)
152
+ simplecov (0.22.0)
153
+ docile (~> 1.1)
154
+ simplecov-html (~> 0.11)
155
+ simplecov_json_formatter (~> 0.1)
156
+ simplecov-cobertura (3.2.0)
157
+ rexml
158
+ simplecov (~> 0.19)
159
+ simplecov-html (0.13.2)
160
+ simplecov_json_formatter (0.1.4)
161
+ sinatra (4.2.1)
162
+ logger (>= 1.6.0)
163
+ mustermann (~> 3.0)
164
+ rack (>= 3.0.0, < 4)
165
+ rack-protection (= 4.2.1)
166
+ rack-session (>= 2.0.0, < 3)
167
+ tilt (~> 2.0)
168
+ stringio (3.2.0)
169
+ thor (1.5.0)
170
+ tilt (2.7.0)
171
+ timecop (0.9.11)
172
+ tsort (0.2.0)
173
+ tzinfo (2.0.6)
174
+ concurrent-ruby (~> 1.0)
175
+ unicode-display_width (3.2.0)
176
+ unicode-emoji (~> 4.1)
177
+ unicode-emoji (4.2.0)
178
+ uri (1.1.1)
179
+ webmock (3.26.2)
180
+ addressable (>= 2.8.0)
181
+ crack (>= 0.3.2)
182
+ hashdiff (>= 0.4.0, < 2.0.0)
183
+ yard (0.9.44)
184
+
185
+ PLATFORMS
186
+ ruby
187
+ x86_64-linux
188
+
189
+ DEPENDENCIES
190
+ activesupport (~> 7.1.0)
191
+ bundler-audit (~> 0.9)
192
+ debug
193
+ dotenv (~> 3.0)
194
+ pry
195
+ pry-byebug
196
+ rspec (~> 3.12)
197
+ rubocop (~> 1.57)
198
+ rubocop-rspec (~> 3.0)
199
+ safire!
200
+ simplecov
201
+ simplecov-cobertura
202
+ sinatra (~> 4.0)
203
+ timecop (~> 0.9)
204
+ webmock (~> 3.18)
205
+ yard (~> 0.9)
206
+
207
+ CHECKSUMS
208
+ activesupport (7.1.6) sha256=7f12140a813b1c4922a322663e547129aef1840fc512fa262378f6d7e7fd3a7c
209
+ addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
210
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
211
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
212
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
213
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
214
+ bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
215
+ byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
216
+ coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
217
+ concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
218
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
219
+ crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
220
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
221
+ debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
222
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
223
+ docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
224
+ dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
225
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
226
+ erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
227
+ faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278
228
+ faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
229
+ faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588
230
+ hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
231
+ i18n (1.15.0) sha256=ed20037ed7ca75dd36ec486b5fb9696e8e0cfe48f4f80980017932d93e70527f
232
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
233
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
234
+ json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
235
+ jwt (2.10.3) sha256=e4d9352fbc7309b1a7448c7dd713dfe4d8c47077af80759cdbed8f878ea0b484
236
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
237
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
238
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
239
+ method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
240
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
241
+ mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
242
+ mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
243
+ net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
244
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
245
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
246
+ pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
247
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
248
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
249
+ pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
250
+ pry-byebug (3.12.0) sha256=594e094ae8a8390a7ad4c7b36ae36e13304ed02664c67417d108dc5f7213d1b7
251
+ psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7
252
+ public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
253
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
254
+ rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
255
+ rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
256
+ rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
257
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
258
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
259
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
260
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
261
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
262
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
263
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
264
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
265
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
266
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
267
+ rubocop (1.88.0) sha256=e420ddf1662d0ef34bc8a2910ac4b396a7ddda0b51a708264405241734b08e0b
268
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
269
+ rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2
270
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
271
+ safire (0.3.0)
272
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
273
+ simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
274
+ simplecov-cobertura (3.2.0) sha256=70d702658677fcb20e5aceb6915ccf8bc62ff2ccd38b62b3ad5c9db5c0888740
275
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
276
+ simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
277
+ sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
278
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
279
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
280
+ tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3
281
+ timecop (0.9.11) sha256=41284dc6e5041f2184f781ace766f942108c842f8d8c1386a26e6343decc7542
282
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
283
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
284
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
285
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
286
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
287
+ webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
288
+ yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28
289
+
290
+ BUNDLED WITH
291
+ 4.0.10