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.
- checksums.yaml +4 -4
- data/.claude/skills/release-safire/SKILL.md +153 -0
- data/.rubocop.yml +11 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +107 -1
- data/CONTRIBUTION.md +6 -1
- data/Gemfile +1 -2
- data/Gemfile.lock +47 -37
- data/README.md +60 -5
- data/ROADMAP.md +61 -12
- data/docs/Gemfile.lock +49 -45
- data/docs/_config.yml +2 -2
- data/docs/adr/ADR-001-activesupport-dependency.md +2 -2
- data/docs/adr/ADR-002-facade-and-forwardable.md +21 -6
- data/docs/adr/ADR-003-protocol-vs-client-type.md +13 -6
- data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +34 -8
- data/docs/adr/ADR-006-lazy-discovery.md +64 -5
- data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +40 -10
- data/docs/adr/ADR-009-oauth-error-hierarchy.md +131 -0
- data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +90 -0
- data/docs/adr/ADR-011-udap-stu2-discovery-conformance.md +113 -0
- data/docs/adr/ADR-012-udap-signed-metadata-validation.md +104 -0
- data/docs/adr/ADR-013-udap-registration-request-model.md +106 -0
- data/docs/adr/ADR-014-udap-software-statement-signing.md +122 -0
- data/docs/adr/index.md +9 -3
- data/docs/advanced.md +22 -25
- data/docs/configuration/client-setup.md +126 -10
- data/docs/configuration/index.md +11 -7
- data/docs/index.md +13 -6
- data/docs/installation.md +3 -2
- data/docs/security.md +44 -5
- data/docs/smart-on-fhir/backend-services/index.md +2 -2
- data/docs/smart-on-fhir/backend-services/token-request.md +1 -1
- data/docs/smart-on-fhir/confidential-asymmetric/index.md +2 -2
- data/docs/smart-on-fhir/confidential-symmetric/index.md +1 -1
- data/docs/smart-on-fhir/discovery/capability-checks.md +7 -0
- data/docs/smart-on-fhir/dynamic-client-registration/index.md +103 -0
- data/docs/smart-on-fhir/dynamic-client-registration/registration.md +160 -0
- data/docs/smart-on-fhir/dynamic-client-registration/response.md +161 -0
- data/docs/smart-on-fhir/index.md +2 -1
- data/docs/smart-on-fhir/post-based-authorization.md +1 -1
- data/docs/smart-on-fhir/public-client/index.md +1 -1
- data/docs/troubleshooting/auth-errors.md +20 -0
- data/docs/troubleshooting/client-errors.md +143 -0
- data/docs/troubleshooting/index.md +61 -4
- data/docs/udap/dynamic-client-registration/index.md +198 -0
- data/docs/udap/dynamic-client-registration/lifecycle.md +115 -0
- data/docs/udap/dynamic-client-registration/registration-metadata.md +133 -0
- data/docs/udap/dynamic-client-registration/software-statement.md +115 -0
- data/docs/udap.md +187 -64
- data/gemfiles/activesupport_71.gemfile +30 -0
- data/gemfiles/activesupport_71.gemfile.lock +291 -0
- data/lib/safire/client.rb +141 -44
- data/lib/safire/client_config.rb +114 -35
- data/lib/safire/client_config_builder.rb +18 -0
- data/lib/safire/errors.rb +100 -44
- data/lib/safire/http_client.rb +7 -2
- data/lib/safire/middleware/https_only_redirects.rb +10 -4
- data/lib/safire/protocols/behaviours.rb +12 -1
- data/lib/safire/protocols/oauth_response_handling.rb +48 -0
- data/lib/safire/protocols/smart.rb +108 -36
- data/lib/safire/protocols/smart_metadata.rb +6 -0
- data/lib/safire/protocols/udap.rb +534 -0
- data/lib/safire/protocols/udap_metadata.rb +429 -0
- data/lib/safire/protocols/udap_registration_metadata.rb +368 -0
- data/lib/safire/protocols/udap_signed_metadata_validator.rb +349 -0
- data/lib/safire/protocols/udap_software_statement.rb +348 -0
- data/lib/safire/protocols.rb +6 -0
- data/lib/safire/uri_validation.rb +102 -0
- data/lib/safire/version.rb +1 -1
- data/lib/safire.rb +2 -0
- data/safire.gemspec +7 -8
- metadata +42 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc7c5bf60b5d43365009f5073aae66d535f8e5345825f8ca08b638fb26587417
|
|
4
|
+
data.tar.gz: f47a4a2c4a84eefa8427e05a59d9e5e2cfca4adbce2a989f43cd53ead47e42bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 625c96ea6424f41590d5396fb9d455ec0d89477474f484cbda236ec55ef13aed76d756ce88e014f76c1556ee8fa8e6e9d92a5a37743af2279e30c59456d74a58
|
|
7
|
+
data.tar.gz: 8c572a8974dfabdad87fd9d672013ca9bf64276a604db1492d11d6ad19e0351b30ecc2485b9262a366e3fce21af8eff656e2d0060077295c1b1127d6315c5ae2
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release-safire
|
|
3
|
+
description: Run the full Safire gem release workflow
|
|
4
|
+
argument-hint: Optional target version (e.g. 0.3.0); omit to auto-determine from CHANGELOG
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Safire Release Workflow
|
|
8
|
+
|
|
9
|
+
You are guiding the user through a complete Safire gem release. Follow each phase in order. **Never modify files or run commands without explicit user approval.** All commits must use `-s` (Signed-off-by) and one-line subjects.
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Before proceeding, gather context by reading these files directly (do not shell out):
|
|
14
|
+
- Read `lib/safire/version.rb` to determine the current version
|
|
15
|
+
- Read `CHANGELOG.md` to find the `## [Unreleased]` section and its entries
|
|
16
|
+
- Run `git branch --show-current` to confirm the current branch
|
|
17
|
+
- Run `git status --short` to check for any uncommitted changes
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Phase 1: Determine Target Version
|
|
22
|
+
|
|
23
|
+
Target version argument: $ARGUMENTS
|
|
24
|
+
|
|
25
|
+
If `$ARGUMENTS` is blank, analyze the [Unreleased] CHANGELOG section and recommend a version bump:
|
|
26
|
+
- **PATCH** (X.Y.Z+1): bug fixes only
|
|
27
|
+
- **MINOR** (X.Y+1.0): new backward-compatible features
|
|
28
|
+
- **MAJOR** (X+1.0.0): breaking changes
|
|
29
|
+
|
|
30
|
+
Present your recommendation with reasoning. Ask the user to confirm or provide a different version before proceeding.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 2: Pre-Release Checks
|
|
35
|
+
|
|
36
|
+
Present this checklist and ask the user to approve running all checks before proceeding:
|
|
37
|
+
|
|
38
|
+
1. `bundle exec rspec` — all tests must pass
|
|
39
|
+
2. `bundle exec rubocop` — zero offenses
|
|
40
|
+
3. `bundle exec bundler-audit check --update` — no known vulnerabilities
|
|
41
|
+
4. `cd docs && bundle exec jekyll build` — docs must build clean
|
|
42
|
+
|
|
43
|
+
Run each check sequentially and report results. If any check fails, stop and clearly describe what needs to be fixed. Do not proceed to Phase 3 until all checks pass.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Phase 3: Create Release Branch
|
|
48
|
+
|
|
49
|
+
Ask the user to approve creating the release branch, then run:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
git checkout main
|
|
53
|
+
git pull origin main
|
|
54
|
+
git checkout -b release-X.Y.Z
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Replace `X.Y.Z` with the confirmed target version.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Phase 4: Update CHANGELOG.md (docs commit)
|
|
62
|
+
|
|
63
|
+
Show the user the exact diff you will make:
|
|
64
|
+
- Rename `## [Unreleased]` → `## [X.Y.Z] - YYYY-MM-DD` (use today's date)
|
|
65
|
+
- Add a fresh empty `## [Unreleased]` section above the new versioned entry
|
|
66
|
+
|
|
67
|
+
Wait for approval, then edit `CHANGELOG.md`.
|
|
68
|
+
|
|
69
|
+
After editing, ask the user to approve this commit:
|
|
70
|
+
```
|
|
71
|
+
git add CHANGELOG.md
|
|
72
|
+
git commit -s -m "Update CHANGELOG for vX.Y.Z"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Stage `CHANGELOG.md` only — no other files.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Phase 5: Bump Version, Update Gemfile.lock and ROADMAP (release commit)
|
|
80
|
+
|
|
81
|
+
Show the user the exact change to `lib/safire/version.rb`:
|
|
82
|
+
```ruby
|
|
83
|
+
VERSION = 'X.Y.Z'.freeze
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Wait for approval, then edit the file and run `bundle install` to regenerate `Gemfile.lock`.
|
|
87
|
+
|
|
88
|
+
Update the `Latest Published Release` in the Roadmap.md file
|
|
89
|
+
|
|
90
|
+
Ask the user to approve this commit:
|
|
91
|
+
```
|
|
92
|
+
git add lib/safire/version.rb Gemfile.lock ROADMAP.md
|
|
93
|
+
git commit -s -m "Bump version to X.Y.Z"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Stage `lib/safire/version.rb`, `Gemfile.lock`, and `ROADMAP.md` only — no other files.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Phase 6: Local Gem Verification
|
|
101
|
+
|
|
102
|
+
Ask the user to approve running local verification (no files will be committed):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
gem build safire.gemspec
|
|
106
|
+
gem install ./safire-X.Y.Z.gem
|
|
107
|
+
ruby -e "require 'safire'; puts Safire::VERSION"
|
|
108
|
+
rm safire-X.Y.Z.gem
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `ruby -e` line must print `X.Y.Z`. If it does not, stop and report the issue. Never commit the `.gem` file.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Phase 7: Push Release Branch
|
|
116
|
+
|
|
117
|
+
Ask the user to approve:
|
|
118
|
+
```
|
|
119
|
+
git push -u origin release-X.Y.Z
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Phase 8: Open Release PR
|
|
125
|
+
|
|
126
|
+
Show the proposed PR and ask for approval before running `gh pr create`:
|
|
127
|
+
|
|
128
|
+
- **Title:** `Release vX.Y.Z`
|
|
129
|
+
- **Body:** the full CHANGELOG entry for this version (the `## [X.Y.Z] - YYYY-MM-DD` block)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Phase 9: Post-Merge Instructions
|
|
134
|
+
|
|
135
|
+
After the PR is created, tell the user the remaining manual steps:
|
|
136
|
+
|
|
137
|
+
1. **Merge the PR** once CI passes and it is approved.
|
|
138
|
+
2. **Create a GitHub Release** after merge:
|
|
139
|
+
- Tag: `vX.Y.Z` on `main`
|
|
140
|
+
- Title: `Safire vX.Y.Z`
|
|
141
|
+
- Notes: the CHANGELOG entry for this version
|
|
142
|
+
3. **Publishing is automated** — `.github/workflows/publish-gem.yml` triggers on release creation.
|
|
143
|
+
4. **Verify** once published: `gem info safire -r`
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Rules (never violate)
|
|
148
|
+
|
|
149
|
+
- All commits use `-s`; subjects are one-line only
|
|
150
|
+
- Two-commit structure on the release branch: docs commit (CHANGELOG) then release commit (version.rb + Gemfile.lock + ROADMAP.md)
|
|
151
|
+
- Never commit a `.gem` file
|
|
152
|
+
- Separate doc changes from code changes into distinct commits
|
|
153
|
+
- Always get explicit user approval before modifying files or running commands
|
data/.rubocop.yml
CHANGED
|
@@ -2,7 +2,7 @@ plugins:
|
|
|
2
2
|
- rubocop-rspec
|
|
3
3
|
|
|
4
4
|
AllCops:
|
|
5
|
-
TargetRubyVersion:
|
|
5
|
+
TargetRubyVersion: 3.2
|
|
6
6
|
NewCops: enable
|
|
7
7
|
Exclude:
|
|
8
8
|
- 'vendor/**/*'
|
|
@@ -32,6 +32,10 @@ Metrics/BlockLength:
|
|
|
32
32
|
- 'spec/**/*'
|
|
33
33
|
- '*.gemspec'
|
|
34
34
|
|
|
35
|
+
Metrics/AbcSize:
|
|
36
|
+
Exclude:
|
|
37
|
+
- 'spec/**/*'
|
|
38
|
+
|
|
35
39
|
Metrics/ParameterLists:
|
|
36
40
|
CountKeywordArgs: false
|
|
37
41
|
|
|
@@ -51,6 +55,12 @@ RSpec/MultipleMemoizedHelpers:
|
|
|
51
55
|
RSpec/NestedGroups:
|
|
52
56
|
Enabled: false
|
|
53
57
|
|
|
58
|
+
RSpec/SpecFilePathFormat:
|
|
59
|
+
CustomTransform:
|
|
60
|
+
OAuthResponseHandling: oauth_response_handling
|
|
61
|
+
RuboCop: rubocop
|
|
62
|
+
RSpec: rspec
|
|
63
|
+
|
|
54
64
|
# Integration tests describe workflows, not classes
|
|
55
65
|
RSpec/DescribeClass:
|
|
56
66
|
Exclude:
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 4.0.
|
|
1
|
+
ruby 4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to the packaged Safire gem will be documented in this file.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Safire::Client#register_client` and `Safire::Client#cancel_registration` now
|
|
15
|
+
support the UDAP Security STU2 Dynamic Client Registration lifecycle when
|
|
16
|
+
initialized with `protocol: :udap`. Safire performs discovery-bound
|
|
17
|
+
registration against authoritative signed endpoints, checks structural DCR
|
|
18
|
+
capability, posts the fixed UDAP envelope with optional certification or
|
|
19
|
+
endorsement JWTs, accepts new-registration `201` and update-style `200`
|
|
20
|
+
responses with a valid `client_id`, and confirms cancellation through a
|
|
21
|
+
successful response containing a valid `client_id` and an empty `grant_types`
|
|
22
|
+
array. OAuth-style failures preserve UDAP error codes such as
|
|
23
|
+
`invalid_software_statement` and `unapproved_software_statement`.
|
|
24
|
+
- `Safire::Protocols::UdapRegistrationMetadata` validates and normalizes
|
|
25
|
+
caller-controlled UDAP Security STU2 registration and cancellation metadata
|
|
26
|
+
before software-statement signing. It enforces exact grant shapes, HTTPS
|
|
27
|
+
redirect and logo URIs, required `mailto:` contact data, protocol-owned
|
|
28
|
+
fields, JSON-compatible extensions, and immutable canonical output. An
|
|
29
|
+
explicit `allow_insecure_localhost: true` option permits development-only
|
|
30
|
+
HTTP loopback URIs without allowing remote HTTP. Registration software
|
|
31
|
+
statements use minimal `alg`/`x5c` headers, exact `iss`/`sub`/`aud` claims, a
|
|
32
|
+
five-minute lifetime, fresh `jti`, key-compatible algorithm negotiation, and
|
|
33
|
+
local certificate/key/SAN checks. `ClientConfig` accepts and masks the
|
|
34
|
+
non-empty, leaf-first `certificate_chain` of PEM strings or
|
|
35
|
+
`OpenSSL::X509::Certificate` instances required for UDAP registration.
|
|
36
|
+
- UDAP Security STU2 discovery is now available with
|
|
37
|
+
`Safire::Client.new(..., protocol: :udap).server_metadata`. Safire fetches
|
|
38
|
+
`/.well-known/udap`, supports community-scoped discovery via `community:`, accepts
|
|
39
|
+
`trusted_anchors:`, `crls:`, `revocation_checker:`, and `verify_chain:` for
|
|
40
|
+
signed metadata trust validation (`verify_chain: false` is for development/test
|
|
41
|
+
only), parses metadata into `Safire::Protocols::UdapMetadata`, and raises
|
|
42
|
+
`DiscoveryError` for HTTP errors, 204 responses, a response body that is not a
|
|
43
|
+
JSON object, or failed signed metadata validation.
|
|
44
|
+
- `Safire::Protocols::UdapMetadata` provides STU2 structural validation and helper
|
|
45
|
+
predicates for advertised UDAP profiles and capabilities.
|
|
46
|
+
- `Safire::Protocols::UdapSignedMetadataValidator` validates the `signed_metadata`
|
|
47
|
+
JWT per UDAP Security STU2, including RS256, `x5c`, JWT signature, certificate
|
|
48
|
+
chain and revocation checks, issuer/subject/time claims, `jti`, and signed
|
|
49
|
+
endpoint claims.
|
|
50
|
+
- UDAP signed endpoint claims are merged over unsigned discovery metadata after
|
|
51
|
+
successful validation. Cached UDAP metadata is revalidated before reuse and
|
|
52
|
+
refetched if the signed JWT, certificate chain, or revocation policy no longer
|
|
53
|
+
validates.
|
|
54
|
+
- `UdapMetadata#signed_metadata_valid?` allows explicit cryptographic re-validation
|
|
55
|
+
against caller-provided trust anchors and revocation material.
|
|
56
|
+
- `Safire::Errors::DiscoveryError` accepts a `label:` keyword argument (default:
|
|
57
|
+
`'SMART configuration'`) and exposes it as a readable attribute so callers can
|
|
58
|
+
identify which protocol's discovery failed.
|
|
59
|
+
|
|
60
|
+
### Breaking Changes
|
|
61
|
+
|
|
62
|
+
- SMART and shared HTTP URI handling now require an explicit
|
|
63
|
+
`allow_insecure_localhost: true` opt-in before accepting HTTP loopback URIs
|
|
64
|
+
or redirects. This aligns SMART local-development behavior with UDAP DCR
|
|
65
|
+
metadata validation while keeping production defaults HTTPS-only.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- SMART Dynamic Client Registration now requires successful RFC 7591 responses
|
|
70
|
+
to contain a non-blank string `client_id`. Malformed identifiers that were
|
|
71
|
+
previously accepted now raise `Safire::Errors::RegistrationError`; valid
|
|
72
|
+
registration responses are unchanged.
|
|
73
|
+
- Ruby requirement relaxed from `>= 4.0.4` to `>= 3.2` to support Rails 7.1+ apps still
|
|
74
|
+
running on Ruby 3.x. The minimum is 3.2 because the gem uses anonymous keyword splat
|
|
75
|
+
forwarding (`**` without a name), which was introduced in Ruby 3.2.
|
|
76
|
+
- ActiveSupport requirement relaxed from `~> 8.0.0` to `>= 7.1, < 9`, resolving the
|
|
77
|
+
bundler conflict that prevented the gem from being used in Rails 8.1 apps or any app
|
|
78
|
+
pinning ActiveSupport 8.1.x.
|
|
79
|
+
- `Safire::Client` now raises `ConfigurationError` when `client_type:` is passed explicitly for
|
|
80
|
+
`protocol: :udap`, both at construction and via `client_type=`; previously the value was
|
|
81
|
+
ignored silently.
|
|
82
|
+
|
|
83
|
+
## [0.3.0] - 2026-04-15
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- `Safire::Client#register_client` implements the OAuth 2.0 Dynamic Client Registration
|
|
88
|
+
Protocol (RFC 7591): POSTs client metadata to the server's registration endpoint and
|
|
89
|
+
returns the response as a Hash containing at minimum a `client_id`
|
|
90
|
+
- Endpoint is resolved from SMART discovery (`registration_endpoint` field) when not
|
|
91
|
+
supplied explicitly via the `registration_endpoint:` keyword argument; HTTPS is
|
|
92
|
+
enforced on the endpoint regardless of source
|
|
93
|
+
- Supports an optional initial access token via the `authorization:` keyword argument
|
|
94
|
+
(full `Authorization` header value including token type prefix)
|
|
95
|
+
- Raises `Safire::Errors::DiscoveryError` when no registration endpoint is available,
|
|
96
|
+
`Safire::Errors::RegistrationError` on server error or a 2xx response missing
|
|
97
|
+
`client_id`, and `Safire::Errors::NetworkError` on transport failure
|
|
98
|
+
- `Safire::Errors::RegistrationError` — new error class for Dynamic Client Registration
|
|
99
|
+
failures; inherits from `Safire::Errors::OAuthError` with `status`, `error_code`,
|
|
100
|
+
`error_description`, and `received_fields` attributes
|
|
101
|
+
- `Safire::Errors::OAuthError` — new shared base class for `RegistrationError`,
|
|
102
|
+
`TokenError`, and `AuthError`; provides `status`, `error_code`, and
|
|
103
|
+
`error_description` attributes and can be used as a single rescue point for any
|
|
104
|
+
server-side OAuth protocol error
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- `client_id` is now optional at `ClientConfig` and `Protocols::Smart` initialization;
|
|
109
|
+
all authorization flows (`authorization_url`, `request_access_token`, `refresh_token`,
|
|
110
|
+
`request_backend_token`) validate its presence at call time and raise
|
|
111
|
+
`Safire::Errors::ConfigurationError` if it is absent
|
|
112
|
+
- `Protocols::Smart#token_endpoint` now raises `Safire::Errors::DiscoveryError` when
|
|
113
|
+
the discovery response does not include a `token_endpoint` field, rather than silently
|
|
114
|
+
passing `nil` to the HTTP client
|
|
115
|
+
|
|
10
116
|
## [0.2.0] - 2026-04-04
|
|
11
117
|
|
|
12
118
|
### Added
|
data/CONTRIBUTION.md
CHANGED
|
@@ -20,6 +20,9 @@ This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participatin
|
|
|
20
20
|
## Getting Started
|
|
21
21
|
|
|
22
22
|
1. Fork the repository on GitHub
|
|
23
|
+
|
|
24
|
+
Note: Codex automated review currently runs only for same-repository branches because it requires repository secrets and reviews untrusted PR content. Fork-based PRs are still welcome, but maintainers will review them manually.
|
|
25
|
+
|
|
23
26
|
2. Clone your fork locally:
|
|
24
27
|
```bash
|
|
25
28
|
git clone https://github.com/YOUR_USERNAME/safire.git
|
|
@@ -34,7 +37,7 @@ This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participatin
|
|
|
34
37
|
|
|
35
38
|
### Requirements
|
|
36
39
|
|
|
37
|
-
- Ruby
|
|
40
|
+
- Ruby 3.2 or later
|
|
38
41
|
- Bundler
|
|
39
42
|
|
|
40
43
|
### Setup
|
|
@@ -231,6 +234,8 @@ end
|
|
|
231
234
|
|
|
232
235
|
2. Open a Pull Request against `main`
|
|
233
236
|
|
|
237
|
+
Codex automated review only runs for same-repository branches. Pull requests from forks will receive maintainer review instead.
|
|
238
|
+
|
|
234
239
|
3. Fill out the PR template with:
|
|
235
240
|
- Summary of changes
|
|
236
241
|
- Related issues
|
data/Gemfile
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
-
ruby '4.0.2'
|
|
4
|
-
|
|
5
3
|
gemspec
|
|
6
4
|
|
|
7
5
|
group :development, :test do
|
|
@@ -23,5 +21,6 @@ group :test do
|
|
|
23
21
|
gem 'dotenv', '~> 3.0'
|
|
24
22
|
gem 'simplecov', require: false
|
|
25
23
|
gem 'simplecov-cobertura', require: false
|
|
24
|
+
gem 'sinatra', '~> 4.0'
|
|
26
25
|
gem 'webmock', '~> 3.18'
|
|
27
26
|
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
safire (0.
|
|
5
|
-
activesupport (
|
|
4
|
+
safire (0.4.0)
|
|
5
|
+
activesupport (>= 7.1, < 9)
|
|
6
6
|
addressable (~> 2.8)
|
|
7
7
|
faraday (~> 2.14)
|
|
8
8
|
faraday-follow_redirects (~> 0.4)
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
activesupport (8.0.
|
|
14
|
+
activesupport (8.0.5)
|
|
15
15
|
base64
|
|
16
16
|
benchmark (>= 0.3)
|
|
17
17
|
bigdecimal
|
|
@@ -24,20 +24,20 @@ GEM
|
|
|
24
24
|
securerandom (>= 0.3)
|
|
25
25
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
26
26
|
uri (>= 0.13.1)
|
|
27
|
-
addressable (2.
|
|
27
|
+
addressable (2.9.0)
|
|
28
28
|
public_suffix (>= 2.0.2, < 8.0)
|
|
29
29
|
ast (2.4.3)
|
|
30
30
|
base64 (0.3.0)
|
|
31
|
-
benchmark (0.
|
|
32
|
-
bigdecimal (
|
|
31
|
+
benchmark (0.5.0)
|
|
32
|
+
bigdecimal (4.1.2)
|
|
33
33
|
bundler-audit (0.9.3)
|
|
34
34
|
bundler (>= 1.2.0)
|
|
35
35
|
thor (~> 1.0)
|
|
36
36
|
byebug (13.0.0)
|
|
37
37
|
reline (>= 0.6.0)
|
|
38
38
|
coderay (1.1.3)
|
|
39
|
-
concurrent-ruby (1.3.
|
|
40
|
-
connection_pool (
|
|
39
|
+
concurrent-ruby (1.3.7)
|
|
40
|
+
connection_pool (3.0.2)
|
|
41
41
|
crack (1.0.1)
|
|
42
42
|
bigdecimal
|
|
43
43
|
rexml
|
|
@@ -46,39 +46,40 @@ GEM
|
|
|
46
46
|
irb (~> 1.10)
|
|
47
47
|
reline (>= 0.3.8)
|
|
48
48
|
diff-lcs (1.6.2)
|
|
49
|
-
docile (1.4.1)
|
|
50
49
|
dotenv (3.2.0)
|
|
51
50
|
drb (2.2.3)
|
|
52
|
-
erb (
|
|
53
|
-
faraday (2.14.
|
|
51
|
+
erb (6.0.4)
|
|
52
|
+
faraday (2.14.3)
|
|
54
53
|
faraday-net_http (>= 2.0, < 3.5)
|
|
55
54
|
json
|
|
56
55
|
logger
|
|
57
56
|
faraday-follow_redirects (0.5.0)
|
|
58
57
|
faraday (>= 1, < 3)
|
|
59
|
-
faraday-net_http (3.4.
|
|
58
|
+
faraday-net_http (3.4.4)
|
|
60
59
|
net-http (~> 0.5)
|
|
61
60
|
hashdiff (1.2.1)
|
|
62
|
-
i18n (1.14.
|
|
61
|
+
i18n (1.14.8)
|
|
63
62
|
concurrent-ruby (~> 1.0)
|
|
64
63
|
io-console (0.8.2)
|
|
65
64
|
irb (1.15.3)
|
|
66
65
|
pp (>= 0.6.0)
|
|
67
66
|
rdoc (>= 4.0.0)
|
|
68
67
|
reline (>= 0.4.2)
|
|
69
|
-
json (2.
|
|
70
|
-
jwt (2.10.
|
|
68
|
+
json (2.21.2)
|
|
69
|
+
jwt (2.10.3)
|
|
71
70
|
base64
|
|
72
|
-
language_server-protocol (3.17.0.
|
|
71
|
+
language_server-protocol (3.17.0.6)
|
|
73
72
|
lint_roller (1.1.0)
|
|
74
73
|
logger (1.7.0)
|
|
75
74
|
method_source (1.1.0)
|
|
76
|
-
minitest (6.0.
|
|
75
|
+
minitest (6.0.6)
|
|
76
|
+
drb (~> 2.0)
|
|
77
77
|
prism (~> 1.5)
|
|
78
|
+
mustermann (3.1.1)
|
|
78
79
|
net-http (0.9.1)
|
|
79
80
|
uri (>= 0.11.1)
|
|
80
|
-
parallel (1.
|
|
81
|
-
parser (3.3.
|
|
81
|
+
parallel (1.28.0)
|
|
82
|
+
parser (3.3.12.0)
|
|
82
83
|
ast (~> 2.4.1)
|
|
83
84
|
racc
|
|
84
85
|
pp (0.6.3)
|
|
@@ -95,13 +96,21 @@ GEM
|
|
|
95
96
|
psych (5.2.6)
|
|
96
97
|
date
|
|
97
98
|
stringio
|
|
98
|
-
public_suffix (
|
|
99
|
+
public_suffix (7.0.5)
|
|
99
100
|
racc (1.8.1)
|
|
101
|
+
rack (3.2.6)
|
|
102
|
+
rack-protection (4.2.1)
|
|
103
|
+
base64 (>= 0.1.0)
|
|
104
|
+
logger (>= 1.6.0)
|
|
105
|
+
rack (>= 3.0.0, < 4)
|
|
106
|
+
rack-session (2.1.2)
|
|
107
|
+
base64 (>= 0.1.0)
|
|
108
|
+
rack (>= 3.0.0)
|
|
100
109
|
rainbow (3.1.1)
|
|
101
110
|
rdoc (6.14.2)
|
|
102
111
|
erb
|
|
103
112
|
psych (>= 4.0.0)
|
|
104
|
-
regexp_parser (2.
|
|
113
|
+
regexp_parser (2.12.0)
|
|
105
114
|
reline (0.6.3)
|
|
106
115
|
io-console (~> 0.5)
|
|
107
116
|
rexml (3.4.4)
|
|
@@ -118,18 +127,18 @@ GEM
|
|
|
118
127
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
119
128
|
rspec-support (~> 3.13.0)
|
|
120
129
|
rspec-support (3.13.6)
|
|
121
|
-
rubocop (1.
|
|
130
|
+
rubocop (1.89.0)
|
|
122
131
|
json (~> 2.3)
|
|
123
132
|
language_server-protocol (~> 3.17.0.2)
|
|
124
133
|
lint_roller (~> 1.1.0)
|
|
125
|
-
parallel (
|
|
134
|
+
parallel (>= 1.10)
|
|
126
135
|
parser (>= 3.3.0.2)
|
|
127
136
|
rainbow (>= 2.2.2, < 4.0)
|
|
128
137
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
129
138
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
130
139
|
ruby-progressbar (~> 1.7)
|
|
131
140
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
132
|
-
rubocop-ast (1.
|
|
141
|
+
rubocop-ast (1.50.0)
|
|
133
142
|
parser (>= 3.3.7.2)
|
|
134
143
|
prism (~> 1.7)
|
|
135
144
|
rubocop-rspec (3.9.0)
|
|
@@ -137,18 +146,21 @@ GEM
|
|
|
137
146
|
rubocop (~> 1.81)
|
|
138
147
|
ruby-progressbar (1.13.0)
|
|
139
148
|
securerandom (0.4.1)
|
|
140
|
-
simplecov (0.
|
|
141
|
-
|
|
142
|
-
simplecov-html (~> 0.11)
|
|
143
|
-
simplecov_json_formatter (~> 0.1)
|
|
144
|
-
simplecov-cobertura (3.1.0)
|
|
149
|
+
simplecov (1.0.3)
|
|
150
|
+
simplecov-cobertura (4.0.0)
|
|
145
151
|
rexml
|
|
146
|
-
simplecov (~> 0
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
simplecov (~> 1.0)
|
|
153
|
+
sinatra (4.2.1)
|
|
154
|
+
logger (>= 1.6.0)
|
|
155
|
+
mustermann (~> 3.0)
|
|
156
|
+
rack (>= 3.0.0, < 4)
|
|
157
|
+
rack-protection (= 4.2.1)
|
|
158
|
+
rack-session (>= 2.0.0, < 3)
|
|
159
|
+
tilt (~> 2.0)
|
|
149
160
|
stringio (3.1.9)
|
|
150
161
|
thor (1.4.0)
|
|
151
|
-
|
|
162
|
+
tilt (2.7.0)
|
|
163
|
+
timecop (0.9.11)
|
|
152
164
|
tzinfo (2.0.6)
|
|
153
165
|
concurrent-ruby (~> 1.0)
|
|
154
166
|
unicode-display_width (3.2.0)
|
|
@@ -159,7 +171,7 @@ GEM
|
|
|
159
171
|
addressable (>= 2.8.0)
|
|
160
172
|
crack (>= 0.3.2)
|
|
161
173
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
162
|
-
yard (0.9.
|
|
174
|
+
yard (0.9.45)
|
|
163
175
|
|
|
164
176
|
PLATFORMS
|
|
165
177
|
ruby
|
|
@@ -177,12 +189,10 @@ DEPENDENCIES
|
|
|
177
189
|
safire!
|
|
178
190
|
simplecov
|
|
179
191
|
simplecov-cobertura
|
|
192
|
+
sinatra (~> 4.0)
|
|
180
193
|
timecop (~> 0.9)
|
|
181
194
|
webmock (~> 3.18)
|
|
182
195
|
yard (~> 0.9)
|
|
183
196
|
|
|
184
|
-
RUBY VERSION
|
|
185
|
-
ruby 4.0.2p0
|
|
186
|
-
|
|
187
197
|
BUNDLED WITH
|
|
188
198
|
2.7.1
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://codecov.io/gh/vanessuniq/safire)
|
|
6
6
|
[](https://vanessuniq.github.io/safire)
|
|
7
7
|
|
|
8
|
-
Safire is a Ruby gem
|
|
8
|
+
Safire is a Ruby gem for healthcare client applications that implements [SMART App Launch 2.2.0](https://hl7.org/fhir/smart-app-launch/) and [UDAP Security STU2 / v2.0.0](https://hl7.org/fhir/us/udap-security/STU2/index.html) discovery plus certificate-backed Dynamic Client Registration. It handles SMART OAuth 2.0 authorization against HL7 FHIR servers, covering PKCE, private key JWT assertions, and the Backend Services system-to-system flow, so you can focus on your application rather than protocol plumbing.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -13,6 +13,7 @@ Safire is a Ruby gem implementing the [SMART App Launch 2.2.0](https://hl7.org/f
|
|
|
13
13
|
|
|
14
14
|
### SMART App Launch (v2.2.0)
|
|
15
15
|
|
|
16
|
+
- Dynamic Client Registration (RFC 7591): obtain a `client_id` at runtime by POSTing client metadata to the server's registration endpoint
|
|
16
17
|
- Discovery (`/.well-known/smart-configuration`)
|
|
17
18
|
- Public Client (PKCE)
|
|
18
19
|
- Confidential Symmetric Client (`client_secret` + HTTP Basic Auth)
|
|
@@ -20,15 +21,69 @@ Safire is a Ruby gem implementing the [SMART App Launch 2.2.0](https://hl7.org/f
|
|
|
20
21
|
- POST-Based Authorization
|
|
21
22
|
- Backend Services (`client_credentials` grant, JWT assertion, no user interaction or PKCE; scope defaults to `system/*.rs`)
|
|
22
23
|
|
|
23
|
-
### UDAP
|
|
24
|
+
### UDAP Security (STU2)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Server metadata discovery is implemented. Pass `protocol: :udap` to fetch `/.well-known/udap`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
client = Safire::Client.new(
|
|
30
|
+
{ base_url: 'https://fhir.example.com' },
|
|
31
|
+
protocol: :udap
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
metadata = client.server_metadata(verify_chain: false) # development/test only
|
|
35
|
+
# => #<Safire::Protocols::UdapMetadata ...>
|
|
36
|
+
|
|
37
|
+
# Community-scoped discovery
|
|
38
|
+
metadata = client.server_metadata(community: 'https://udap.example.org/community1', verify_chain: false)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Production UDAP discovery requires trust anchors plus an explicit certificate revocation policy
|
|
42
|
+
(`crls:` or `revocation_checker:`). Use `verify_chain: false` only for development or tests.
|
|
43
|
+
|
|
44
|
+
Dynamic Client Registration is implemented for certificate-backed UDAP clients:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
udap_client = Safire::Client.new(
|
|
48
|
+
{
|
|
49
|
+
base_url: 'https://fhir.example.com',
|
|
50
|
+
private_key: File.read('client-key.pem'),
|
|
51
|
+
certificate_chain: [File.read('client-cert.pem')]
|
|
52
|
+
},
|
|
53
|
+
protocol: :udap
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
registration = udap_client.register_client(
|
|
57
|
+
{
|
|
58
|
+
client_name: 'Example Backend Service',
|
|
59
|
+
contacts: ['mailto:security@example.com'],
|
|
60
|
+
grant_types: ['client_credentials'],
|
|
61
|
+
scope: 'system/Patient.rs'
|
|
62
|
+
},
|
|
63
|
+
client_uri: 'https://client.example.com',
|
|
64
|
+
trusted_anchors: [ca_cert],
|
|
65
|
+
crls: [ca_crl]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
cancellation = udap_client.cancel_registration(
|
|
69
|
+
{
|
|
70
|
+
client_name: 'Example Backend Service',
|
|
71
|
+
contacts: ['mailto:security@example.com'],
|
|
72
|
+
scope: 'system/Patient.rs'
|
|
73
|
+
},
|
|
74
|
+
client_uri: 'https://client.example.com',
|
|
75
|
+
trusted_anchors: [ca_cert],
|
|
76
|
+
crls: [ca_crl]
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
UDAP JWT client authentication and Tiered OAuth are planned. See [ROADMAP.md](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md) for details.
|
|
26
81
|
|
|
27
82
|
---
|
|
28
83
|
|
|
29
84
|
## Installation
|
|
30
85
|
|
|
31
|
-
Requires Ruby ≥
|
|
86
|
+
Requires Ruby ≥ 3.2.
|
|
32
87
|
|
|
33
88
|
```ruby
|
|
34
89
|
gem 'safire'
|
|
@@ -154,7 +209,7 @@ bin/demo
|
|
|
154
209
|
# Visit http://localhost:4567
|
|
155
210
|
```
|
|
156
211
|
|
|
157
|
-
Demonstrates SMART discovery, all authorization flows, token refresh, and backend services token requests. See [`examples/sinatra_app/README.md`](examples/sinatra_app/README.md) for details.
|
|
212
|
+
Demonstrates SMART Dynamic Client Registration, SMART discovery, UDAP discovery with `signed_metadata` trust validation, UDAP Dynamic Client Registration and cancellation, all authorization flows, token refresh, and backend services token requests. See [`examples/sinatra_app/README.md`](examples/sinatra_app/README.md) for details.
|
|
158
213
|
|
|
159
214
|
---
|
|
160
215
|
|