safire 0.1.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 +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +62 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +35 -0
- data/CODE_OF_CONDUCT.md +17 -0
- data/CONTRIBUTION.md +283 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +186 -0
- data/LICENSE +201 -0
- data/README.md +159 -0
- data/ROADMAP.md +54 -0
- data/Rakefile +26 -0
- data/docs/.gitignore +5 -0
- data/docs/404.html +25 -0
- data/docs/Gemfile +37 -0
- data/docs/Gemfile.lock +195 -0
- data/docs/_config.yml +103 -0
- data/docs/_includes/footer_custom.html +6 -0
- data/docs/_includes/head_custom.html +14 -0
- data/docs/_sass/custom/custom.scss +108 -0
- data/docs/adr/ADR-001-activesupport-dependency.md +50 -0
- data/docs/adr/ADR-002-facade-and-forwardable.md +79 -0
- data/docs/adr/ADR-003-protocol-vs-client-type.md +67 -0
- data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +59 -0
- data/docs/adr/ADR-005-per-client-http-ownership.md +58 -0
- data/docs/adr/ADR-006-lazy-discovery.md +83 -0
- data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +59 -0
- data/docs/adr/ADR-008-warn-return-false-for-compliance-validation.md +74 -0
- data/docs/adr/index.md +22 -0
- data/docs/advanced.md +284 -0
- data/docs/configuration/client-setup.md +158 -0
- data/docs/configuration/index.md +60 -0
- data/docs/configuration/logging.md +86 -0
- data/docs/index.md +64 -0
- data/docs/installation.md +96 -0
- data/docs/security.md +256 -0
- data/docs/smart-on-fhir/confidential-asymmetric/authorization.md +72 -0
- data/docs/smart-on-fhir/confidential-asymmetric/index.md +162 -0
- data/docs/smart-on-fhir/confidential-asymmetric/token-exchange.md +250 -0
- data/docs/smart-on-fhir/confidential-symmetric/authorization.md +75 -0
- data/docs/smart-on-fhir/confidential-symmetric/index.md +69 -0
- data/docs/smart-on-fhir/confidential-symmetric/token-exchange.md +215 -0
- data/docs/smart-on-fhir/discovery/capability-checks.md +142 -0
- data/docs/smart-on-fhir/discovery/index.md +96 -0
- data/docs/smart-on-fhir/discovery/metadata.md +147 -0
- data/docs/smart-on-fhir/index.md +72 -0
- data/docs/smart-on-fhir/post-based-authorization.md +190 -0
- data/docs/smart-on-fhir/public-client/authorization.md +112 -0
- data/docs/smart-on-fhir/public-client/index.md +80 -0
- data/docs/smart-on-fhir/public-client/token-exchange.md +249 -0
- data/docs/troubleshooting/auth-errors.md +124 -0
- data/docs/troubleshooting/client-errors.md +130 -0
- data/docs/troubleshooting/index.md +99 -0
- data/docs/troubleshooting/token-errors.md +99 -0
- data/docs/udap.md +78 -0
- data/lib/safire/client.rb +195 -0
- data/lib/safire/client_config.rb +169 -0
- data/lib/safire/client_config_builder.rb +72 -0
- data/lib/safire/entity.rb +26 -0
- data/lib/safire/errors.rb +247 -0
- data/lib/safire/http_client.rb +87 -0
- data/lib/safire/jwt_assertion.rb +237 -0
- data/lib/safire/middleware/https_only_redirects.rb +39 -0
- data/lib/safire/pkce.rb +39 -0
- data/lib/safire/protocols/behaviours.rb +54 -0
- data/lib/safire/protocols/smart.rb +378 -0
- data/lib/safire/protocols/smart_metadata.rb +231 -0
- data/lib/safire/version.rb +4 -0
- data/lib/safire.rb +54 -0
- data/safire.gemspec +36 -0
- metadata +184 -0
data/docs/Gemfile.lock
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
addressable (2.8.8)
|
|
5
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
6
|
+
base64 (0.3.0)
|
|
7
|
+
bigdecimal (4.0.1)
|
|
8
|
+
colorator (1.1.0)
|
|
9
|
+
concurrent-ruby (1.3.6)
|
|
10
|
+
csv (3.3.5)
|
|
11
|
+
em-websocket (0.5.3)
|
|
12
|
+
eventmachine (>= 0.12.9)
|
|
13
|
+
http_parser.rb (~> 0)
|
|
14
|
+
eventmachine (1.2.7)
|
|
15
|
+
ffi (1.17.3)
|
|
16
|
+
ffi (1.17.3-arm-linux-gnu)
|
|
17
|
+
ffi (1.17.3-arm-linux-musl)
|
|
18
|
+
ffi (1.17.3-x86_64-linux-gnu)
|
|
19
|
+
forwardable-extended (2.6.0)
|
|
20
|
+
google-protobuf (4.33.4)
|
|
21
|
+
bigdecimal
|
|
22
|
+
rake (>= 13)
|
|
23
|
+
http_parser.rb (0.8.1)
|
|
24
|
+
i18n (1.14.8)
|
|
25
|
+
concurrent-ruby (~> 1.0)
|
|
26
|
+
jekyll (4.4.1)
|
|
27
|
+
addressable (~> 2.4)
|
|
28
|
+
base64 (~> 0.2)
|
|
29
|
+
colorator (~> 1.0)
|
|
30
|
+
csv (~> 3.0)
|
|
31
|
+
em-websocket (~> 0.5)
|
|
32
|
+
i18n (~> 1.0)
|
|
33
|
+
jekyll-sass-converter (>= 2.0, < 4.0)
|
|
34
|
+
jekyll-watch (~> 2.0)
|
|
35
|
+
json (~> 2.6)
|
|
36
|
+
kramdown (~> 2.3, >= 2.3.1)
|
|
37
|
+
kramdown-parser-gfm (~> 1.0)
|
|
38
|
+
liquid (~> 4.0)
|
|
39
|
+
mercenary (~> 0.3, >= 0.3.6)
|
|
40
|
+
pathutil (~> 0.9)
|
|
41
|
+
rouge (>= 3.0, < 5.0)
|
|
42
|
+
safe_yaml (~> 1.0)
|
|
43
|
+
terminal-table (>= 1.8, < 4.0)
|
|
44
|
+
webrick (~> 1.7)
|
|
45
|
+
jekyll-feed (0.17.0)
|
|
46
|
+
jekyll (>= 3.7, < 5.0)
|
|
47
|
+
jekyll-include-cache (0.2.1)
|
|
48
|
+
jekyll (>= 3.7, < 5.0)
|
|
49
|
+
jekyll-sass-converter (3.1.0)
|
|
50
|
+
sass-embedded (~> 1.75)
|
|
51
|
+
jekyll-seo-tag (2.8.0)
|
|
52
|
+
jekyll (>= 3.8, < 5.0)
|
|
53
|
+
jekyll-sitemap (1.4.0)
|
|
54
|
+
jekyll (>= 3.7, < 5.0)
|
|
55
|
+
jekyll-watch (2.2.1)
|
|
56
|
+
listen (~> 3.0)
|
|
57
|
+
json (2.18.0)
|
|
58
|
+
just-the-docs (0.12.0)
|
|
59
|
+
jekyll (>= 3.8.5)
|
|
60
|
+
jekyll-include-cache
|
|
61
|
+
jekyll-seo-tag (>= 2.0)
|
|
62
|
+
rake (>= 12.3.1)
|
|
63
|
+
kramdown (2.5.2)
|
|
64
|
+
rexml (>= 3.4.4)
|
|
65
|
+
kramdown-parser-gfm (1.1.0)
|
|
66
|
+
kramdown (~> 2.0)
|
|
67
|
+
liquid (4.0.4)
|
|
68
|
+
listen (3.10.0)
|
|
69
|
+
logger
|
|
70
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
71
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
|
72
|
+
logger (1.7.0)
|
|
73
|
+
mercenary (0.4.0)
|
|
74
|
+
pathutil (0.16.2)
|
|
75
|
+
forwardable-extended (~> 2.6)
|
|
76
|
+
public_suffix (7.0.2)
|
|
77
|
+
rake (13.3.1)
|
|
78
|
+
rb-fsevent (0.11.2)
|
|
79
|
+
rb-inotify (0.11.1)
|
|
80
|
+
ffi (~> 1.0)
|
|
81
|
+
rexml (3.4.4)
|
|
82
|
+
rouge (4.7.0)
|
|
83
|
+
safe_yaml (1.0.5)
|
|
84
|
+
sass-embedded (1.97.3)
|
|
85
|
+
google-protobuf (~> 4.31)
|
|
86
|
+
rake (>= 13)
|
|
87
|
+
sass-embedded (1.97.3-aarch64-linux-android)
|
|
88
|
+
google-protobuf (~> 4.31)
|
|
89
|
+
sass-embedded (1.97.3-arm-linux-androideabi)
|
|
90
|
+
google-protobuf (~> 4.31)
|
|
91
|
+
sass-embedded (1.97.3-arm-linux-gnueabihf)
|
|
92
|
+
google-protobuf (~> 4.31)
|
|
93
|
+
sass-embedded (1.97.3-arm-linux-musleabihf)
|
|
94
|
+
google-protobuf (~> 4.31)
|
|
95
|
+
sass-embedded (1.97.3-riscv64-linux-android)
|
|
96
|
+
google-protobuf (~> 4.31)
|
|
97
|
+
sass-embedded (1.97.3-riscv64-linux-gnu)
|
|
98
|
+
google-protobuf (~> 4.31)
|
|
99
|
+
sass-embedded (1.97.3-riscv64-linux-musl)
|
|
100
|
+
google-protobuf (~> 4.31)
|
|
101
|
+
sass-embedded (1.97.3-x86_64-linux-android)
|
|
102
|
+
google-protobuf (~> 4.31)
|
|
103
|
+
sass-embedded (1.97.3-x86_64-linux-gnu)
|
|
104
|
+
google-protobuf (~> 4.31)
|
|
105
|
+
terminal-table (3.0.2)
|
|
106
|
+
unicode-display_width (>= 1.1.1, < 3)
|
|
107
|
+
unicode-display_width (2.6.0)
|
|
108
|
+
webrick (1.9.2)
|
|
109
|
+
|
|
110
|
+
PLATFORMS
|
|
111
|
+
aarch64-linux-android
|
|
112
|
+
arm-linux-androideabi
|
|
113
|
+
arm-linux-gnu
|
|
114
|
+
arm-linux-gnueabihf
|
|
115
|
+
arm-linux-musl
|
|
116
|
+
arm-linux-musleabihf
|
|
117
|
+
riscv64-linux-android
|
|
118
|
+
riscv64-linux-gnu
|
|
119
|
+
riscv64-linux-musl
|
|
120
|
+
ruby
|
|
121
|
+
x86_64-linux
|
|
122
|
+
x86_64-linux-android
|
|
123
|
+
|
|
124
|
+
DEPENDENCIES
|
|
125
|
+
base64
|
|
126
|
+
bigdecimal
|
|
127
|
+
csv
|
|
128
|
+
http_parser.rb (~> 0.8.0)
|
|
129
|
+
jekyll (~> 4.4)
|
|
130
|
+
jekyll-feed (~> 0.17)
|
|
131
|
+
jekyll-seo-tag
|
|
132
|
+
jekyll-sitemap
|
|
133
|
+
just-the-docs
|
|
134
|
+
logger
|
|
135
|
+
tzinfo (>= 1, < 3)
|
|
136
|
+
tzinfo-data
|
|
137
|
+
wdm (~> 0.1)
|
|
138
|
+
webrick
|
|
139
|
+
|
|
140
|
+
CHECKSUMS
|
|
141
|
+
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
|
|
142
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
143
|
+
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
|
144
|
+
colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38
|
|
145
|
+
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
146
|
+
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
|
147
|
+
em-websocket (0.5.3) sha256=f56a92bde4e6cb879256d58ee31f124181f68f8887bd14d53d5d9a292758c6a8
|
|
148
|
+
eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972
|
|
149
|
+
ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c
|
|
150
|
+
ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668
|
|
151
|
+
ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053
|
|
152
|
+
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
|
153
|
+
forwardable-extended (2.6.0) sha256=1bec948c469bbddfadeb3bd90eb8c85f6e627a412a3e852acfd7eaedbac3ec97
|
|
154
|
+
google-protobuf (4.33.4) sha256=86921935b023ed0d872d6e84382e79016c91689be0520d614c74426778f13c16
|
|
155
|
+
http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a
|
|
156
|
+
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
|
157
|
+
jekyll (4.4.1) sha256=4c1144d857a5b2b80d45b8cf5138289579a9f8136aadfa6dd684b31fe2bc18c1
|
|
158
|
+
jekyll-feed (0.17.0) sha256=689aab16c877949bb9e7a5c436de6278318a51ecb974792232fd94d8b3acfcc3
|
|
159
|
+
jekyll-include-cache (0.2.1) sha256=c7d4b9e551732a27442cb2ce853ba36a2f69c66603694b8c1184c99ab1a1a205
|
|
160
|
+
jekyll-sass-converter (3.1.0) sha256=83925d84f1d134410c11d0c6643b0093e82e3a3cf127e90757a85294a3862443
|
|
161
|
+
jekyll-seo-tag (2.8.0) sha256=3f2ed1916d56f14ebfa38e24acde9b7c946df70cb183af2cb5f0598f21ae6818
|
|
162
|
+
jekyll-sitemap (1.4.0) sha256=0de08c5debc185ea5a8f980e1025c7cd3f8e0c35c8b6ef592f15c46235cf4218
|
|
163
|
+
jekyll-watch (2.2.1) sha256=bc44ed43f5e0a552836245a54dbff3ea7421ecc2856707e8a1ee203a8387a7e1
|
|
164
|
+
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
|
|
165
|
+
just-the-docs (0.12.0) sha256=15f2839ac9082898d60f33b978aa6f8e46fc50ba8fac20ae7a7f0e1fb295523e
|
|
166
|
+
kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa
|
|
167
|
+
kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729
|
|
168
|
+
liquid (4.0.4) sha256=4fcfebb1a045e47918388dbb7a0925e7c3893e58d2bd6c3b3c73ec17a2d8fdb3
|
|
169
|
+
listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2
|
|
170
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
171
|
+
mercenary (0.4.0) sha256=b25a1e4a59adca88665e08e24acf0af30da5b5d859f7d8f38fba52c28f405138
|
|
172
|
+
pathutil (0.16.2) sha256=e43b74365631cab4f6d5e4228f812927efc9cb2c71e62976edcb252ee948d589
|
|
173
|
+
public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857
|
|
174
|
+
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
175
|
+
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
|
176
|
+
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
|
177
|
+
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
178
|
+
rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739
|
|
179
|
+
safe_yaml (1.0.5) sha256=a6ac2d64b7eb027bdeeca1851fe7e7af0d668e133e8a88066a0c6f7087d9f848
|
|
180
|
+
sass-embedded (1.97.3) sha256=c4136da69ae3acfa7b0809f4ec10891125edf57df214a2d1ab570f721f96f7a6
|
|
181
|
+
sass-embedded (1.97.3-aarch64-linux-android) sha256=623b2f52fed6e3696c6445406e4efaef57b54442cc35604bfffbb82aef7d5c45
|
|
182
|
+
sass-embedded (1.97.3-arm-linux-androideabi) sha256=e2ef33b187066e09374023e58e72cf3b5baabe6b77ecd74356fe9b4892a1c6e1
|
|
183
|
+
sass-embedded (1.97.3-arm-linux-gnueabihf) sha256=ce443b57f3d7f03740267cf0f2cdff13e8055dd5938488967746f29f230222da
|
|
184
|
+
sass-embedded (1.97.3-arm-linux-musleabihf) sha256=be3972424616f916ce1f4f41228266d57339e490dfd7ca0cea5588579564d4c0
|
|
185
|
+
sass-embedded (1.97.3-riscv64-linux-android) sha256=201426b3e58611aa8cf34a7574df51905ec42fefb5a69982cc8497ac7fb26a6b
|
|
186
|
+
sass-embedded (1.97.3-riscv64-linux-gnu) sha256=d7bac32f4de55c589a036da13ac4482bf5b7dfac980b4c0203d31a1bd9f07622
|
|
187
|
+
sass-embedded (1.97.3-riscv64-linux-musl) sha256=621d981d700e2b8d0459b5ea696fff746dfa07d6b6bbc70cd982905214b07888
|
|
188
|
+
sass-embedded (1.97.3-x86_64-linux-android) sha256=8f5e179bee8610be432499f228ea4e53ab362b1db0da1ae3cd3e76b114712372
|
|
189
|
+
sass-embedded (1.97.3-x86_64-linux-gnu) sha256=173a4d0dbe2fffdf7482bd3e82fb597dfc658c18d1e8fd746aa7d5077ed4e850
|
|
190
|
+
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
|
|
191
|
+
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
|
|
192
|
+
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
|
193
|
+
|
|
194
|
+
BUNDLED WITH
|
|
195
|
+
4.0.3
|
data/docs/_config.yml
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Welcome to Jekyll!
|
|
2
|
+
#
|
|
3
|
+
# This config file is meant for settings that affect your whole blog, values
|
|
4
|
+
# which you are expected to set up once and rarely edit after that. If you find
|
|
5
|
+
# yourself editing this file very often, consider using Jekyll's data files
|
|
6
|
+
# feature for the data you need to update frequently.
|
|
7
|
+
#
|
|
8
|
+
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
|
9
|
+
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
|
|
10
|
+
#
|
|
11
|
+
# If you need help with YAML syntax, here are some quick references for you:
|
|
12
|
+
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
|
|
13
|
+
# https://learnxinyminutes.com/docs/yaml/
|
|
14
|
+
#
|
|
15
|
+
# Site settings
|
|
16
|
+
# These are used to personalize your new site. If you look in the HTML files,
|
|
17
|
+
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
|
18
|
+
# You can create any custom variable you would like, and they will be accessible
|
|
19
|
+
# in the templates via {{ site.myvariable }}.
|
|
20
|
+
|
|
21
|
+
title: Safire Documentation
|
|
22
|
+
description: SMART on FHIR and UDAP implementation library for Ruby
|
|
23
|
+
baseurl: /safire
|
|
24
|
+
url: https://vanessuniq.github.io
|
|
25
|
+
|
|
26
|
+
# Build settings
|
|
27
|
+
markdown: kramdown
|
|
28
|
+
highlighter: rouge
|
|
29
|
+
theme: just-the-docs
|
|
30
|
+
plugins:
|
|
31
|
+
- jekyll-feed
|
|
32
|
+
- jekyll-seo-tag
|
|
33
|
+
- jekyll-sitemap
|
|
34
|
+
|
|
35
|
+
# Just the Docs theme configuration
|
|
36
|
+
color_scheme: light
|
|
37
|
+
search_enabled: true
|
|
38
|
+
search:
|
|
39
|
+
previews: 2
|
|
40
|
+
preview_words_before: 3
|
|
41
|
+
preview_words_after: 5
|
|
42
|
+
tokenizer_separator: /[\s\-/]+/
|
|
43
|
+
nav_sort: case_insensitive
|
|
44
|
+
footer_content: ""
|
|
45
|
+
|
|
46
|
+
# Markdown Processors
|
|
47
|
+
sass:
|
|
48
|
+
quiet_deps: true
|
|
49
|
+
silence_deprecations:
|
|
50
|
+
- import
|
|
51
|
+
|
|
52
|
+
kramdown:
|
|
53
|
+
syntax_highlighter: rouge
|
|
54
|
+
syntax_highlighter_opts:
|
|
55
|
+
css_class: highlight
|
|
56
|
+
span:
|
|
57
|
+
line_numbers: false
|
|
58
|
+
block:
|
|
59
|
+
line_numbers: false
|
|
60
|
+
start_line: 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
defaults:
|
|
64
|
+
- scope:
|
|
65
|
+
path: ""
|
|
66
|
+
type: pages
|
|
67
|
+
values:
|
|
68
|
+
layout: default
|
|
69
|
+
toc: true
|
|
70
|
+
|
|
71
|
+
aux_links:
|
|
72
|
+
Safire API Docs:
|
|
73
|
+
- /safire/api
|
|
74
|
+
GitHub:
|
|
75
|
+
- https://github.com/vanessuniq/safire
|
|
76
|
+
|
|
77
|
+
aux_links_new_tab: true
|
|
78
|
+
|
|
79
|
+
back_to_top: true
|
|
80
|
+
back_to_top_text: Back to Top ↑
|
|
81
|
+
|
|
82
|
+
# Mermaid diagrams
|
|
83
|
+
mermaid:
|
|
84
|
+
version: "10.9.0"
|
|
85
|
+
# Exclude from processing.
|
|
86
|
+
# The following items will not be processed, by default.
|
|
87
|
+
# Any item listed under the `exclude:` key here will be automatically added to
|
|
88
|
+
# the internal "default list".
|
|
89
|
+
#
|
|
90
|
+
# Excluded items can be processed by explicitly listing the directories or
|
|
91
|
+
# their entries' file path in the `include:` list.
|
|
92
|
+
#
|
|
93
|
+
# exclude:
|
|
94
|
+
# - .sass-cache/
|
|
95
|
+
# - .jekyll-cache/
|
|
96
|
+
# - gemfiles/
|
|
97
|
+
# - Gemfile
|
|
98
|
+
# - Gemfile.lock
|
|
99
|
+
# - node_modules/
|
|
100
|
+
# - vendor/bundle/
|
|
101
|
+
# - vendor/cache/
|
|
102
|
+
# - vendor/gems/
|
|
103
|
+
# - vendor/ruby/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{% assign current_year = site.time | date: '%Y' %}
|
|
2
|
+
{% if current_year == '2025' %}
|
|
3
|
+
<p class="footer-copyright">Copyright © 2025 Safire Contributors. Licensed under Apache 2.0.</p>
|
|
4
|
+
{% else %}
|
|
5
|
+
<p class="footer-copyright">Copyright © 2025-{{ current_year }} Safire Contributors. Licensed under Apache 2.0.</p>
|
|
6
|
+
{% endif %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
2
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
3
|
+
<link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,300;0,400;1,300&display=swap" rel="stylesheet">
|
|
4
|
+
<style>
|
|
5
|
+
.footer-copyright {
|
|
6
|
+
font-family: 'Raleway', sans-serif;
|
|
7
|
+
font-size: 0.72rem;
|
|
8
|
+
font-weight: 700;
|
|
9
|
+
letter-spacing: 0.06em;
|
|
10
|
+
text-align: center;
|
|
11
|
+
opacity: 0.75;
|
|
12
|
+
margin-top: 0.5rem;
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Custom styles for Safire documentation
|
|
2
|
+
|
|
3
|
+
// Sticky footer - ensure footer stays at bottom of viewport
|
|
4
|
+
html {
|
|
5
|
+
height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.side-bar {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.main {
|
|
20
|
+
flex: 1 0 auto;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.main-content-wrap {
|
|
26
|
+
flex: 1 0 auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Footer styling
|
|
30
|
+
.site-footer {
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
padding: 1rem;
|
|
33
|
+
margin-top: auto;
|
|
34
|
+
border-top: 1px solid #e1e4e8;
|
|
35
|
+
background: #f6f8fa;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Grid layout for cards
|
|
39
|
+
.grid {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
42
|
+
gap: 1rem;
|
|
43
|
+
margin: 2rem 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.grid-item {
|
|
47
|
+
padding: 1.5rem;
|
|
48
|
+
border: 1px solid #e1e4e8;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
background: #f6f8fa;
|
|
51
|
+
|
|
52
|
+
h3 {
|
|
53
|
+
margin-top: 0;
|
|
54
|
+
|
|
55
|
+
a {
|
|
56
|
+
color: #0366d6;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
color: #93116c;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Status badges
|
|
67
|
+
.status-complete {
|
|
68
|
+
color: #28a745;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.status-planned {
|
|
72
|
+
color: #6f42c1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.status-progress {
|
|
76
|
+
color: #fd7e14;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Code highlighting improvements
|
|
80
|
+
.highlight {
|
|
81
|
+
margin: 1rem 0;
|
|
82
|
+
|
|
83
|
+
pre {
|
|
84
|
+
padding: 1rem;
|
|
85
|
+
overflow-x: auto;
|
|
86
|
+
font-size: 0.875rem;
|
|
87
|
+
line-height: 1.45;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Warning and info boxes
|
|
92
|
+
.warning {
|
|
93
|
+
padding: 1rem;
|
|
94
|
+
margin: 1rem 0;
|
|
95
|
+
background-color: #fff3cd;
|
|
96
|
+
border: 1px solid #ffeaa7;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
color: #856404;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.info {
|
|
102
|
+
padding: 1rem;
|
|
103
|
+
margin: 1rem 0;
|
|
104
|
+
background-color: #d1ecf1;
|
|
105
|
+
border: 1px solid #bee5eb;
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
color: #0c5460;
|
|
108
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "ADR-001: ActiveSupport as a runtime dependency"
|
|
4
|
+
parent: Architecture Decision Records
|
|
5
|
+
nav_order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ADR-001: ActiveSupport as a runtime dependency
|
|
9
|
+
|
|
10
|
+
**Status:** Accepted
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
Safire needs several utility methods throughout its codebase: presence checks (`present?`, `blank?`), string/array utilities, and safe object handling. There are three options:
|
|
17
|
+
|
|
18
|
+
**Option A — Implement utilities inline:** write custom `present?`, `blank?`, and other helpers inside the `Safire` module.
|
|
19
|
+
|
|
20
|
+
**Option B — Require individual ActiveSupport components:** use `require 'active_support/core_ext/object/blank'` and similar targeted requires to pull in only what is needed.
|
|
21
|
+
|
|
22
|
+
**Option C — `require 'active_support/all'`:** load the entire ActiveSupport library at once.
|
|
23
|
+
|
|
24
|
+
Option A introduces maintenance burden and drift from well-tested, community-maintained implementations. Option B requires tracking which AS components are used and updating requires when new utilities are adopted — a form of accidental complexity with no real benefit.
|
|
25
|
+
|
|
26
|
+
The key fact about ActiveSupport is that it uses Ruby's `autoload` mechanism internally. Even though `require 'active_support/all'` loads the autoload registry for all AS modules, the actual code for each module is only loaded when that module is first referenced. The memory overhead of `require 'active_support/all'` is therefore close to the overhead of targeted requires — the difference is measured in milliseconds at startup, not in runtime memory.
|
|
27
|
+
|
|
28
|
+
Safire is also commonly used alongside Rails applications, where ActiveSupport is already loaded. In that context, `require 'active_support/all'` is effectively a no-op.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
Use `require 'active_support/all'` and treat ActiveSupport as a first-class runtime dependency (`spec.add_dependency 'activesupport', '~> 8.0.0'`).
|
|
35
|
+
|
|
36
|
+
ActiveSupport utilities (`present?`, `blank?`, `fetch`, safe navigation, etc.) may be used freely throughout the codebase without needing to track individual requires.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Consequences
|
|
41
|
+
|
|
42
|
+
**Benefits:**
|
|
43
|
+
- No custom utility reimplementations to maintain
|
|
44
|
+
- Any ActiveSupport method is available anywhere in the codebase without an explicit require
|
|
45
|
+
- No startup overhead in Rails applications (AS already loaded)
|
|
46
|
+
- Minimal overhead in non-Rails applications due to AS autoloading
|
|
47
|
+
|
|
48
|
+
**Trade-offs:**
|
|
49
|
+
- `activesupport` is pinned to `~> 8.0.0` — non-Rails Ruby applications must accept this dependency; a future major AS version bump requires a Safire dependency update
|
|
50
|
+
- Developers unfamiliar with AS may not recognise AS methods as external — mitigated by the fact that AS conventions (`present?`, `blank?`) are widely known in the Ruby ecosystem
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "ADR-002: Facade pattern — Client delegates to protocol implementations via Forwardable"
|
|
4
|
+
parent: Architecture Decision Records
|
|
5
|
+
nav_order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ADR-002: Facade pattern — `Client` delegates to protocol implementations via `Forwardable`
|
|
9
|
+
|
|
10
|
+
**Status:** Accepted
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
Safire must support multiple authorization protocols (SMART on FHIR, UDAP) from a single public entry point. There are several ways to structure this:
|
|
17
|
+
|
|
18
|
+
**Option A — Monolithic `Client`:** implement all protocol logic directly inside `Safire::Client`. Simple at first, but grows unbounded as each protocol adds methods, and makes it impossible to test protocol logic in isolation.
|
|
19
|
+
|
|
20
|
+
**Option B — Inheritance:** `Safire::Client` is an abstract base class; `SmartClient` and `UdapClient` subclass it. Callers would instantiate the concrete subclass. This leaks implementation details to callers (they must know which subclass to pick) and makes the `protocol:` keyword redundant.
|
|
21
|
+
|
|
22
|
+
**Option C — Strategy pattern via instance variable:** `Client` holds a `@protocol_client` strategy object and calls it manually in every method. Works, but every delegated method requires a boilerplate wrapper with the same `def method_name(...); @protocol_client.method_name(...); end` pattern.
|
|
23
|
+
|
|
24
|
+
**Option D — Facade with `Forwardable`:** `Client` is a thin facade. It resolves configuration, validates `protocol:` and `client_type:`, constructs the appropriate protocol implementation lazily, and then delegates all public methods to it using Ruby's `Forwardable` module.
|
|
25
|
+
|
|
26
|
+
The core requirement that drives the choice is: **callers must use a single, stable class (`Safire::Client`) regardless of which protocol they need.** Adding a new protocol must not change the public API or require callers to change their code.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Decision
|
|
31
|
+
|
|
32
|
+
`Safire::Client` is a facade. It owns:
|
|
33
|
+
- Configuration resolution (hash → `ClientConfig`)
|
|
34
|
+
- Protocol and client type validation
|
|
35
|
+
- Lazy construction of the protocol implementation (`@protocol_client`)
|
|
36
|
+
- Delegation of all public protocol methods via `Forwardable`
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
class Client
|
|
40
|
+
extend Forwardable
|
|
41
|
+
|
|
42
|
+
def_delegators :protocol_client,
|
|
43
|
+
:server_metadata, :authorization_url,
|
|
44
|
+
:request_access_token, :refresh_token,
|
|
45
|
+
:token_response_valid?, :register_client
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def protocol_client
|
|
50
|
+
@protocol_client ||= PROTOCOL_CLASSES.fetch(@protocol).new(config, client_type:)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Protocol implementations (`Protocols::Smart`, future `Protocols::Udap`) include `Protocols::Behaviours` to declare the required interface. Adding a new protocol requires:
|
|
56
|
+
1. Implementing the `Behaviours` interface in a new class
|
|
57
|
+
2. Adding the class to `PROTOCOL_CLASSES`
|
|
58
|
+
3. Adding its valid client types to `PROTOCOL_CLIENT_TYPES`
|
|
59
|
+
|
|
60
|
+
No changes to `Client` itself.
|
|
61
|
+
|
|
62
|
+
**Why `Forwardable` over `method_missing`:** `Forwardable` is explicit — the delegated method list is visible in the class body, easy to grep, and YARD-documented. `method_missing` is implicit, difficult to introspect, and catches typos silently.
|
|
63
|
+
|
|
64
|
+
**Why `Forwardable` over manual wrappers:** manual wrappers require writing the same boilerplate for every method and must be updated whenever a method signature changes. `def_delegators` is a single declaration.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Consequences
|
|
69
|
+
|
|
70
|
+
**Benefits:**
|
|
71
|
+
- Public API (`Safire::Client`) is stable — callers never need to change when a new protocol is added
|
|
72
|
+
- Protocol implementations are independently testable
|
|
73
|
+
- `Forwardable` delegation is explicit and greppable
|
|
74
|
+
- The `protocol:` keyword cleanly selects the implementation class without leaking subclass names to callers
|
|
75
|
+
- `client_type=` mutation works naturally — the facade updates `@protocol_client` in place (see [ADR-006]({% link adr/ADR-006-lazy-discovery.md %}) for why this preserves cached discovery)
|
|
76
|
+
|
|
77
|
+
**Trade-offs:**
|
|
78
|
+
- `Client` itself has no runtime behaviour — all logic lives in protocol classes; contributors must know to look in `Protocols::Smart` for SMART logic, not in `Client`
|
|
79
|
+
- `def_delegators` does not forward keyword arguments transparently in all Ruby versions — method signatures in `Behaviours` must be compatible with delegation
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "ADR-003: protocol: and client_type: as orthogonal dimensions"
|
|
4
|
+
parent: Architecture Decision Records
|
|
5
|
+
nav_order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ADR-003: `protocol:` and `client_type:` as orthogonal dimensions
|
|
9
|
+
|
|
10
|
+
**Status:** Accepted
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
`Safire::Client` needs to support multiple healthcare authorization protocols (SMART on FHIR, UDAP) and, within SMART, multiple client authentication methods (public, confidential symmetric, confidential asymmetric). There are two ways to model this:
|
|
17
|
+
|
|
18
|
+
**Option A — flat enum:** a single parameter enumerating every combination.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
Safire::Client.new(config, auth: :smart_public)
|
|
22
|
+
Safire::Client.new(config, auth: :smart_confidential_symmetric)
|
|
23
|
+
Safire::Client.new(config, auth: :udap_b2b)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Option B — two orthogonal keyword arguments:** one for the protocol, one for the client type within that protocol.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
Safire::Client.new(config, protocol: :smart, client_type: :public)
|
|
30
|
+
Safire::Client.new(config, protocol: :smart, client_type: :confidential_symmetric)
|
|
31
|
+
Safire::Client.new(config, protocol: :udap) # client_type not applicable
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The key structural difference: SMART has three client authentication methods; UDAP has none — UDAP always authenticates via signed JWT assertions (AnT) with an X.509 certificate chain, and this is not user-configurable. Mixing them into one flat enum would create invalid combinations (`:udap_public`, `:udap_confidential_symmetric`) and make `client_type=` mutation impossible to express cleanly.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Decision
|
|
39
|
+
|
|
40
|
+
Two orthogonal keyword arguments: `protocol:` selects the protocol implementation class; `client_type:` is a SMART-specific parameter that controls the token endpoint authentication method.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
VALID_PROTOCOLS = %i[smart udap].freeze
|
|
44
|
+
|
|
45
|
+
PROTOCOL_CLIENT_TYPES = {
|
|
46
|
+
smart: %i[public confidential_symmetric confidential_asymmetric],
|
|
47
|
+
udap: nil # not user-configurable; AnT with x5c always used
|
|
48
|
+
}.freeze
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `protocol:` is validated against `VALID_PROTOCOLS`; an unknown protocol raises `ConfigurationError`
|
|
52
|
+
- `client_type:` is validated against `PROTOCOL_CLIENT_TYPES[@protocol]`; if `nil` (UDAP), validation is skipped and the setter logs a warning and no-ops rather than raising
|
|
53
|
+
- Changing `client_type=` on a SMART client updates the underlying protocol client in place — already-fetched server metadata is preserved and no re-discovery occurs
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
**Benefits:**
|
|
60
|
+
- No invalid combinations — UDAP has no client type choices at all; this is enforced at the type level, not with runtime checks
|
|
61
|
+
- `client_type=` mutation is clean and natural for the "discover first, then select client type" pattern
|
|
62
|
+
- Adding a new SMART client type requires only adding a symbol to `PROTOCOL_CLIENT_TYPES[:smart]`
|
|
63
|
+
- Adding a new protocol requires adding a class to `PROTOCOL_CLASSES` and an entry to `PROTOCOL_CLIENT_TYPES`
|
|
64
|
+
|
|
65
|
+
**Trade-offs:**
|
|
66
|
+
- Two keyword args instead of one — a caller needs to know which dimension belongs to which kwarg; mitigated by clear documentation and validation errors that name the invalid parameter
|
|
67
|
+
- `client_type:` defaults to `:public` even when `protocol: :udap` — the value is ignored for UDAP, but setting it is technically a no-op with a warning rather than an error; this is intentional for resilience in generic caller code
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: "ADR-004: ClientConfig immutability and Entity sensitive attribute masking"
|
|
4
|
+
parent: Architecture Decision Records
|
|
5
|
+
nav_order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ADR-004: `ClientConfig` immutability and `Entity` sensitive attribute masking
|
|
9
|
+
|
|
10
|
+
**Status:** Accepted
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
`ClientConfig` holds all credentials and endpoints for a Safire client — including `client_secret` and `private_key`. Two separate concerns need to be addressed:
|
|
17
|
+
|
|
18
|
+
**Concern 1 — Mutability:** should `ClientConfig` allow attributes to be changed after construction?
|
|
19
|
+
|
|
20
|
+
Mutable configuration creates subtle bugs in concurrent environments: a `ClientConfig` instance shared across threads could have its `client_secret` changed mid-request. It also makes it impossible to reason about the state of a client at any point after construction, because any attribute may have been changed.
|
|
21
|
+
|
|
22
|
+
**Concern 2 — Sensitive data leakage:** Ruby's default `inspect` and `to_s` output every instance variable. In a Rails application, an unhandled exception containing a `ClientConfig` would dump `client_secret` and `private_key` into error logs, exception trackers (Sentry, Datadog), and any other logging middleware.
|
|
23
|
+
|
|
24
|
+
These two concerns are related: if `ClientConfig` is mutable, masking is harder to guarantee (a new value could be assigned via a setter without going through the masking layer).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
**`ClientConfig` is immutable after construction.** All attributes are `attr_reader` only — no setters. Validation runs once at construction. After `initialize` returns, the object's state cannot change.
|
|
31
|
+
|
|
32
|
+
**Sensitive attributes are masked at two layers** via the `Entity` base class:
|
|
33
|
+
|
|
34
|
+
**Layer 1 — `#to_hash`:** the `sensitive_attributes` hook (overridden in `ClientConfig` to return `[:client_secret, :private_key]`) causes those values to appear as `'[FILTERED]'` in any hash serialisation.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
def to_hash
|
|
38
|
+
ATTRIBUTES.each_with_object({}) do |attr, hash|
|
|
39
|
+
value = send(attr)
|
|
40
|
+
hash[attr] = sensitive_attributes.include?(attr) ? '[FILTERED]' : value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Layer 2 — `#inspect`:** `ClientConfig` overrides `inspect` directly, emitting `[FILTERED]` for sensitive attributes. This prevents credential leakage in exception backtraces, IRB/pry sessions, and logging middleware that calls `inspect` on objects.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
**Benefits:**
|
|
52
|
+
- Thread-safe by default — a `ClientConfig` shared across threads has no mutable state
|
|
53
|
+
- Credentials cannot leak through `inspect`, `to_s`, exception trackers, or log output
|
|
54
|
+
- Validation at construction means invalid configs are caught early, before any network calls
|
|
55
|
+
- The `sensitive_attributes` hook is extensible — subclasses can add fields without modifying `Entity`
|
|
56
|
+
|
|
57
|
+
**Trade-offs:**
|
|
58
|
+
- Callers cannot modify a `ClientConfig` in place — they must construct a new one; this is intentional and makes state changes explicit
|
|
59
|
+
- `private_key` masking means the key object itself is not serialisable via `to_hash` — callers needing to inspect or store the key must access it directly via `config.private_key`
|