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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +62 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +35 -0
  6. data/CODE_OF_CONDUCT.md +17 -0
  7. data/CONTRIBUTION.md +283 -0
  8. data/Gemfile +26 -0
  9. data/Gemfile.lock +186 -0
  10. data/LICENSE +201 -0
  11. data/README.md +159 -0
  12. data/ROADMAP.md +54 -0
  13. data/Rakefile +26 -0
  14. data/docs/.gitignore +5 -0
  15. data/docs/404.html +25 -0
  16. data/docs/Gemfile +37 -0
  17. data/docs/Gemfile.lock +195 -0
  18. data/docs/_config.yml +103 -0
  19. data/docs/_includes/footer_custom.html +6 -0
  20. data/docs/_includes/head_custom.html +14 -0
  21. data/docs/_sass/custom/custom.scss +108 -0
  22. data/docs/adr/ADR-001-activesupport-dependency.md +50 -0
  23. data/docs/adr/ADR-002-facade-and-forwardable.md +79 -0
  24. data/docs/adr/ADR-003-protocol-vs-client-type.md +67 -0
  25. data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +59 -0
  26. data/docs/adr/ADR-005-per-client-http-ownership.md +58 -0
  27. data/docs/adr/ADR-006-lazy-discovery.md +83 -0
  28. data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +59 -0
  29. data/docs/adr/ADR-008-warn-return-false-for-compliance-validation.md +74 -0
  30. data/docs/adr/index.md +22 -0
  31. data/docs/advanced.md +284 -0
  32. data/docs/configuration/client-setup.md +158 -0
  33. data/docs/configuration/index.md +60 -0
  34. data/docs/configuration/logging.md +86 -0
  35. data/docs/index.md +64 -0
  36. data/docs/installation.md +96 -0
  37. data/docs/security.md +256 -0
  38. data/docs/smart-on-fhir/confidential-asymmetric/authorization.md +72 -0
  39. data/docs/smart-on-fhir/confidential-asymmetric/index.md +162 -0
  40. data/docs/smart-on-fhir/confidential-asymmetric/token-exchange.md +250 -0
  41. data/docs/smart-on-fhir/confidential-symmetric/authorization.md +75 -0
  42. data/docs/smart-on-fhir/confidential-symmetric/index.md +69 -0
  43. data/docs/smart-on-fhir/confidential-symmetric/token-exchange.md +215 -0
  44. data/docs/smart-on-fhir/discovery/capability-checks.md +142 -0
  45. data/docs/smart-on-fhir/discovery/index.md +96 -0
  46. data/docs/smart-on-fhir/discovery/metadata.md +147 -0
  47. data/docs/smart-on-fhir/index.md +72 -0
  48. data/docs/smart-on-fhir/post-based-authorization.md +190 -0
  49. data/docs/smart-on-fhir/public-client/authorization.md +112 -0
  50. data/docs/smart-on-fhir/public-client/index.md +80 -0
  51. data/docs/smart-on-fhir/public-client/token-exchange.md +249 -0
  52. data/docs/troubleshooting/auth-errors.md +124 -0
  53. data/docs/troubleshooting/client-errors.md +130 -0
  54. data/docs/troubleshooting/index.md +99 -0
  55. data/docs/troubleshooting/token-errors.md +99 -0
  56. data/docs/udap.md +78 -0
  57. data/lib/safire/client.rb +195 -0
  58. data/lib/safire/client_config.rb +169 -0
  59. data/lib/safire/client_config_builder.rb +72 -0
  60. data/lib/safire/entity.rb +26 -0
  61. data/lib/safire/errors.rb +247 -0
  62. data/lib/safire/http_client.rb +87 -0
  63. data/lib/safire/jwt_assertion.rb +237 -0
  64. data/lib/safire/middleware/https_only_redirects.rb +39 -0
  65. data/lib/safire/pkce.rb +39 -0
  66. data/lib/safire/protocols/behaviours.rb +54 -0
  67. data/lib/safire/protocols/smart.rb +378 -0
  68. data/lib/safire/protocols/smart_metadata.rb +231 -0
  69. data/lib/safire/version.rb +4 -0
  70. data/lib/safire.rb +54 -0
  71. data/safire.gemspec +36 -0
  72. 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 &copy; 2025 Safire Contributors. Licensed under Apache 2.0.</p>
4
+ {% else %}
5
+ <p class="footer-copyright">Copyright &copy; 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`