standard_singpass 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8da1f0678a358ee51cf45c1f7b47f537e465f4d9c2ed738de7cdb2326fd92b49
4
+ data.tar.gz: cd6df85e85fe8d5a0636558b943f1cfadc5a5704eed15c7c52c367c2bf7ba974
5
+ SHA512:
6
+ metadata.gz: 849a2c6e1de1e63c8092f2088ef5e8de12292d3b3e130c6a47831fa244def9dfe7cc854a82eff49d4863757423d4feb788f6d943345aa953c6007a85f91df549
7
+ data.tar.gz: 36da035a4471275f50a3a2e2c974f440ff337c45a34b43a4181da80187a283c40937bcc0724f20a52d8f9a22780d6af277aba3494739bdaa49ebe586a193caee
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-24
11
+
12
+ ### Added
13
+
14
+ - `StandardSingpass::Myinfo::Client` — FAPI 2.0 OAuth client with PKCE, DPoP (RFC 9449), and `private_key_jwt` client assertion. Performs PAR, token exchange, ID-token validation (iss/aud/exp/iat/sub/acr/nonce), and userinfo fetch.
15
+ - `StandardSingpass::Myinfo::Security` — PKCE/DPoP primitives, JWE decryption dispatch, JWS validation with JWKS caching and one-shot key-rotation retry.
16
+ - `StandardSingpass::Myinfo::EcdhJwe` — native ECDH-ES+A128KW / +A256KW JWE implementation covering A128GCM, A256GCM, A128CBC-HS256, A256CBC-HS512. Exists because the `jwt` gem does not support ECDH-ES key agreement.
17
+ - `StandardSingpass::Myinfo::PersonDataParser` — extracts 40+ structured fields (identity, contact, address, pass info, income, employment, assets, housing, vehicles) from FAPI 2.0 v5 userinfo responses, unwrapping the `person_info` envelope.
18
+ - `StandardSingpass::Myinfo::JwksGenerator` — generates and validates the private JWKS document used for signing + ECDH-ES decryption keys. Includes a `standard_singpass:myinfo:generate_jwks` rake task.
19
+ - `StandardSingpass::Myinfo::TestPersonas` — loader for the bundled persona fixture set used by mock-callback flows and RSpec helpers; host can override via `config.personas_path`.
20
+ - `StandardSingpass::Myinfo::Configuration` — block-style config (`StandardSingpass::Myinfo.configure { |c| ... }`). Host passes `client_id`, `redirect_url`, `private_jwks_json`, optional `minimum_acr`, optional `network_wrapper` (e.g. circuit breaker), and `environment` (`:production` / `:staging` — picks Singpass endpoint URLs).
21
+ - `rails generate standard_singpass:install` scaffolds `config/initializers/standard_singpass.rb` with the full configuration surface commented out. Idempotent; `--force` overwrites.
22
+ - Full-flow integration spec at `spec/standard_singpass/myinfo/full_flow_spec.rb` walking PAR → token exchange → userinfo → JWE decrypt → JWS validate → parse end to end. Covers happy path, nonce mismatch, and the `minimum_acr` enforcement branch (LOA-3 floor + LOA-2 token → `AuthenticationError`).
23
+ - `spec/standard_singpass/myinfo/configuration_spec.rb` covering the global `configure` / `public_jwks` paths and the private-JWKS parser (happy path, malformed JSON, public-only-key rejection).
24
+ - Sorbet wired end to end: `bin/tapioca` shim, generated RBIs for runtime deps under `sorbet/rbi/gems/`, hand-edited shims for `OpenSSL::PKey::EC::Point#to_octet_string` and `Faraday.get`, `.github/workflows/typecheck.yml` running `bundle exec srb tc` on every PR, and `bundle exec srb tc` appended to the weekly-maintenance test commands so dependency-update PRs also catch type-sig breakage.
25
+ - `AGENTS.md` — quick-reference doc for AI agents and human contributors. Public surface, error taxonomy, key workflows.
26
+ - Bundled persona fixture at `fixtures/myinfo-personas.json`.
27
+ - Productivity workflows: `.github/workflows/{claude.yml,claude-code-review.yml,weekly-maintenance.yml}` + `.github/dependabot.yml` (matches peer `standard_*` gem setup).
28
+
29
+ ### Notes
30
+
31
+ - No Rails routes, models, or migrations — gem is library-only by design. The host owns persistence (e.g. a MyInfo record model), orchestration (callback handlers), forms, and UI.
32
+ - `lib/standard_singpass/engine.rb` defers its `Rails::Engine` definition behind `if defined?(::Rails::Engine)` so the gem loads cleanly under `tapioca gems` and other no-Rails contexts. The host `Gemfile` should also list `gem "rails"` ahead of `gemspec` so `Bundler.require` loads Rails first.
33
+ - Coverage sits at 95.26% line / 84.29% branch with a 90% line / 75% branch floor enforced via SimpleCov.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Jaryl Sim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # StandardSingpass
2
+
3
+ Singpass MyInfo (FAPI 2.0) client for Rails applications. Packages the OAuth flow, DPoP/PKCE primitives, native ECDH-ES JWE decryption, JWS validation, and person-data parser needed to integrate with Singpass MyInfo.
4
+
5
+ The gem is intentionally **library-only** — it does not own routes, models, migrations, or UI. The host application owns persistence, orchestration, forms, and presentation.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "standard_singpass"
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ ```ruby
18
+ # config/initializers/standard_singpass.rb
19
+ StandardSingpass::Myinfo.configure do |c|
20
+ # Endpoint set. Drives production vs. staging Singpass URLs.
21
+ c.environment = Rails.env.production? ? :production : :staging
22
+
23
+ # Client credentials.
24
+ c.client_id = ENV["MYINFO_CLIENT_ID"]
25
+ c.redirect_url = ENV["MYINFO_REDIRECT_URL"]
26
+
27
+ # Optional: override default scope (defaults to a 36-attribute set covering
28
+ # identity, contact, income, employment, housing, assets, vehicles).
29
+ # c.scope = "openid name email ..."
30
+
31
+ # Required: full private JWKS JSON containing both sig (ES256) and enc
32
+ # (ECDH-ES+A256KW) keys with the private scalar `d`.
33
+ c.private_jwks_json = ENV["MYINFO_PRIVATE_JWKS"]
34
+
35
+ # Optional: enforce minimum Authentication Context Class Reference. Set to
36
+ # e.g. "urn:singpass:authentication:loa:3" to require high-assurance.
37
+ c.minimum_acr = ENV["MYINFO_MIN_ACR"]
38
+
39
+ # Optional: wrap outbound HTTP calls with a circuit breaker / retry layer.
40
+ # Defaults to identity (no wrapper).
41
+ # c.network_wrapper = ->(&block) { StandardCircuit.run(:myinfo, &block) }
42
+
43
+ # Optional: path to a JSON file of test personas (for mock callback flows).
44
+ # Defaults to the gem's bundled fixtures/myinfo-personas.json.
45
+ # c.personas_path = Rails.root.join("e2e/fixtures/myinfo-personas.json")
46
+ end
47
+ ```
48
+
49
+ ## Initiating the flow
50
+
51
+ ```ruby
52
+ pkce = StandardSingpass::Myinfo::Security.generate_pkce_pair
53
+ dpop_key = StandardSingpass::Myinfo::Security.generate_ephemeral_key_pair
54
+ state = SecureRandom.hex(16)
55
+ nonce = SecureRandom.hex(16)
56
+
57
+ client = StandardSingpass::Myinfo::Client.new
58
+ par = client.push_authorization_request(
59
+ code_challenge: pkce[:code_challenge],
60
+ state: state,
61
+ nonce: nonce,
62
+ dpop_key_pair: dpop_key
63
+ )
64
+
65
+ # Persist pkce[:code_verifier], state, nonce, and dpop_key in the user session.
66
+ redirect_to client.build_authorize_redirect(request_uri: par[:request_uri])
67
+ ```
68
+
69
+ ## Handling the callback
70
+
71
+ ```ruby
72
+ result = client.get_person_data(
73
+ auth_code: params[:code],
74
+ code_verifier: session[:myinfo_code_verifier],
75
+ dpop_key_pair: session[:myinfo_dpop_key],
76
+ nonce: session[:myinfo_nonce]
77
+ )
78
+
79
+ parsed = StandardSingpass::Myinfo::PersonDataParser.call(result[:person_data])
80
+ acr = result[:id_token_acr]
81
+
82
+ # `parsed` is a 40+ key hash: nric, name, email, mobile_number,
83
+ # registered_address, cpf_balances, noa, hdb_ownership, etc. Pass it to your
84
+ # host-side persistence / projection layer.
85
+ ```
86
+
87
+ ## Generating and serving JWKS
88
+
89
+ The host application is responsible for serving the public JWKS at
90
+ `/.well-known/jwks.json` (or another endpoint Singpass is configured to fetch).
91
+
92
+ ```ruby
93
+ # Generate a fresh private JWKS (run locally, never in CI):
94
+ bin/rails standard_singpass:myinfo:generate_jwks > private-jwks.json
95
+
96
+ # Serve the public JWKS from a controller:
97
+ render json: StandardSingpass::Myinfo.public_jwks
98
+ ```
99
+
100
+ ## Error classes
101
+
102
+ All errors descend from `StandardSingpass::Myinfo::Error`:
103
+
104
+ - `AuthenticationError` — ID token or token exchange rejected
105
+ - `ApiError` — endpoint reachable but returned a non-2xx response
106
+ - `PARError` — pushed authorization request failed
107
+ - `DecryptionError` — JWE decryption failed
108
+ - `SignatureError` — JWS verification failed
109
+ - `RateLimitError` — Singpass returned HTTP 429
110
+ - `ConfigurationError` — gem is misconfigured (e.g. invalid ACR URN)
111
+
112
+ `DecryptionError` and `SignatureError` indicate a key/cert misconfiguration, not an upstream outage — exclude them from circuit-breaker tracking if you use one.
113
+
114
+ ## License
115
+
116
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,250 @@
1
+ {
2
+ "default": {
3
+ "name": { "value": "TAN AH KOW" },
4
+ "uinfin": { "value": "S9812345A" },
5
+ "aliasname": { "value": "JOHN TAN" },
6
+ "dob": { "value": "1998-03-15" },
7
+ "sex": { "code": "M", "desc": "MALE" },
8
+ "race": { "code": "CN", "desc": "CHINESE" },
9
+ "nationality": { "code": "SG", "desc": "SINGAPORE CITIZEN" },
10
+ "residentialstatus": { "code": "C", "desc": "CITIZEN" },
11
+ "marital": { "code": "2", "desc": "MARRIED" },
12
+ "edulevel": { "code": "7", "desc": "BACHELOR'S OR EQUIVALENT" },
13
+ "email": { "value": "ahkow@example.com" },
14
+ "mobileno": {
15
+ "prefix": { "value": "+" },
16
+ "areacode": { "value": "65" },
17
+ "nbr": { "value": "91234567" }
18
+ },
19
+ "regadd": {
20
+ "type": "SG",
21
+ "block": { "value": "123" },
22
+ "street": { "value": "BEDOK NORTH AVE 1" },
23
+ "floor": { "value": "05" },
24
+ "unit": { "value": "123" },
25
+ "postal": { "value": "460123" },
26
+ "country": { "code": "SG" }
27
+ },
28
+ "hdbtype": { "code": "112", "desc": "4-ROOM FLAT" },
29
+ "housingtype": { "code": "HDB", "desc": "HDB" },
30
+ "employment": { "value": "EMPLOYED" },
31
+ "occupation": { "code": "5223", "desc": "SALES SUPERVISOR" },
32
+ "ownerprivate": { "value": "N" },
33
+ "cpfbalances": {
34
+ "oa": { "value": "12500.00" }
35
+ },
36
+ "cpfcontributions": {
37
+ "history": [
38
+ { "employer": { "value": "ACME PTE LTD" }, "month": { "value": "2026-04" }, "amount": { "value": "1850.00" }, "date": { "value": "2026-04-15" } },
39
+ { "employer": { "value": "ACME PTE LTD" }, "month": { "value": "2026-03" }, "amount": { "value": "1850.00" }, "date": { "value": "2026-03-15" } }
40
+ ]
41
+ },
42
+ "cpfemployers": {
43
+ "history": [
44
+ { "employer": { "value": "ACME PTE LTD" }, "month": { "value": "2026-04" } }
45
+ ]
46
+ },
47
+ "noa-basic": {
48
+ "yearofassessment": { "value": "2025" },
49
+ "amount": { "value": "75000.00" }
50
+ },
51
+ "noahistory-basic": {
52
+ "noas": [
53
+ { "yearofassessment": { "value": "2024" }, "amount": { "value": "70000.00" } },
54
+ { "yearofassessment": { "value": "2023" }, "amount": { "value": "65000.00" } }
55
+ ]
56
+ },
57
+ "hdbownership": [
58
+ {
59
+ "noofowners": { "value": "2" },
60
+ "address": {
61
+ "block": { "value": "123" },
62
+ "street": { "value": "BEDOK NORTH AVE 1" },
63
+ "floor": { "value": "05" },
64
+ "unit": { "value": "123" },
65
+ "postal": { "value": "460123" },
66
+ "country": { "code": "SG" }
67
+ },
68
+ "hdbtype": { "code": "112" },
69
+ "loangranted": { "value": "300000" },
70
+ "balanceloanrepayment": { "value": "15" },
71
+ "outstandingloanbalance": { "value": "240000" },
72
+ "monthlyloaninstalment": { "value": "1800" },
73
+ "outstandinginstalment": { "value": "1800" }
74
+ }
75
+ ],
76
+ "vehicles": [
77
+ { "effectiveownership": { "value": "2024-06-15T00:00:00" } }
78
+ ]
79
+ },
80
+
81
+ "self_employed": {
82
+ "name": { "value": "LIM MEI LING" },
83
+ "uinfin": { "value": "S8567890B" },
84
+ "dob": { "value": "1985-11-22" },
85
+ "sex": { "code": "F", "desc": "FEMALE" },
86
+ "race": { "code": "CN", "desc": "CHINESE" },
87
+ "nationality": { "code": "SG", "desc": "SINGAPORE CITIZEN" },
88
+ "residentialstatus": { "code": "C", "desc": "CITIZEN" },
89
+ "marital": { "code": "1", "desc": "SINGLE" },
90
+ "edulevel": { "code": "5", "desc": "DIPLOMA OR EQUIVALENT" },
91
+ "email": { "value": "meiling@example.com" },
92
+ "mobileno": {
93
+ "prefix": { "value": "+" },
94
+ "areacode": { "value": "65" },
95
+ "nbr": { "value": "98765432" }
96
+ },
97
+ "regadd": {
98
+ "type": "SG",
99
+ "block": { "value": "456" },
100
+ "street": { "value": "TAMPINES ST 21" },
101
+ "floor": { "value": "12" },
102
+ "unit": { "value": "456" },
103
+ "postal": { "value": "520456" },
104
+ "country": { "code": "SG" }
105
+ },
106
+ "employment": { "value": "SELF-EMPLOYED" }
107
+ },
108
+
109
+ "tamil_name": {
110
+ "name": { "value": "RAJ KUMAR S/O THIAGARAJAN" },
111
+ "uinfin": { "value": "S7234567C" },
112
+ "dob": { "value": "1972-06-08" },
113
+ "sex": { "code": "M", "desc": "MALE" },
114
+ "race": { "code": "IN", "desc": "INDIAN" },
115
+ "nationality": { "code": "SG", "desc": "SINGAPORE CITIZEN" },
116
+ "residentialstatus": { "code": "C", "desc": "CITIZEN" },
117
+ "marital": { "code": "2", "desc": "MARRIED" },
118
+ "edulevel": { "code": "8", "desc": "MASTER'S OR EQUIVALENT" },
119
+ "email": { "value": "rajkumar@example.com" },
120
+ "mobileno": {
121
+ "prefix": { "value": "+" },
122
+ "areacode": { "value": "65" },
123
+ "nbr": { "value": "92223344" }
124
+ },
125
+ "regadd": {
126
+ "type": "SG",
127
+ "block": { "value": "789" },
128
+ "street": { "value": "SERANGOON GARDEN WAY" },
129
+ "floor": { "value": "03" },
130
+ "unit": { "value": "07" },
131
+ "postal": { "value": "555789" },
132
+ "country": { "code": "SG" }
133
+ },
134
+ "employment": { "value": "EMPLOYED" }
135
+ },
136
+
137
+ "permanent_resident": {
138
+ "name": { "value": "KIM SOO YOUNG" },
139
+ "uinfin": { "value": "G6543210D" },
140
+ "dob": { "value": "1990-09-03" },
141
+ "sex": { "code": "F", "desc": "FEMALE" },
142
+ "race": { "code": "OT", "desc": "OTHERS" },
143
+ "nationality": { "code": "KR", "desc": "KOREAN" },
144
+ "residentialstatus": { "code": "P", "desc": "PERMANENT RESIDENT" },
145
+ "marital": { "code": "1", "desc": "SINGLE" },
146
+ "edulevel": { "code": "8", "desc": "MASTER'S OR EQUIVALENT" },
147
+ "email": { "value": "sooyoung.kim@example.com" },
148
+ "mobileno": {
149
+ "prefix": { "value": "+" },
150
+ "areacode": { "value": "65" },
151
+ "nbr": { "value": "94445566" }
152
+ },
153
+ "regadd": {
154
+ "type": "SG",
155
+ "block": { "value": "21" },
156
+ "street": { "value": "SCOTTS ROAD" },
157
+ "floor": { "value": "18" },
158
+ "unit": { "value": "1801" },
159
+ "postal": { "value": "228221" },
160
+ "country": { "code": "SG" }
161
+ },
162
+ "employment": { "value": "EMPLOYED" }
163
+ },
164
+
165
+ "sparse_data": {
166
+ "name": { "value": "TAN BOON HUAT" },
167
+ "uinfin": { "value": "S5078901E" },
168
+ "dob": { "value": "1950-12-19" },
169
+ "sex": { "code": "M", "desc": "MALE" },
170
+ "race": { "code": "CN", "desc": "CHINESE" },
171
+ "nationality": { "code": "SG", "desc": "SINGAPORE CITIZEN" },
172
+ "residentialstatus": { "code": "C", "desc": "CITIZEN" },
173
+ "marital": { "code": "3", "desc": "WIDOWED" },
174
+ "edulevel": { "code": "1", "desc": "PRIMARY OR LOWER SECONDARY" },
175
+ "email": { "value": "tan.boon.huat@example.com" },
176
+ "mobileno": {
177
+ "prefix": { "value": "+" },
178
+ "areacode": { "value": "65" },
179
+ "nbr": { "value": "98889999" }
180
+ },
181
+ "regadd": {
182
+ "type": "SG",
183
+ "block": { "value": "20" },
184
+ "street": { "value": "TOA PAYOH LOR 4" },
185
+ "postal": { "value": "310020" },
186
+ "country": { "code": "SG" }
187
+ },
188
+ "employment": {}
189
+ },
190
+
191
+ "fin_holder": {
192
+ "name": { "value": "RAJESH KUMAR" },
193
+ "uinfin": { "value": "G7890123H" },
194
+ "dob": { "value": "1988-08-12" },
195
+ "sex": { "code": "M", "desc": "MALE" },
196
+ "race": { "code": "IN", "desc": "INDIAN" },
197
+ "nationality": { "code": "IN", "desc": "INDIAN" },
198
+ "residentialstatus": { "code": "F", "desc": "FOREIGNER" },
199
+ "passtype": { "code": "EP", "desc": "EMPLOYMENT PASS" },
200
+ "passstatus": { "code": "LIVE", "desc": "LIVE" },
201
+ "passexpirydate": { "value": "2027-08-31" },
202
+ "employmentsector": { "code": "FINANCIAL_SERVICES", "desc": "FINANCIAL SERVICES" },
203
+ "employment": { "value": "GLOBAL FINANCE PTE LTD" },
204
+ "occupation": { "code": "2411", "desc": "ACCOUNTANT" },
205
+ "marital": { "code": "1", "desc": "SINGLE" },
206
+ "email": { "value": "rajesh.kumar@example.com" },
207
+ "mobileno": {
208
+ "prefix": { "value": "+" },
209
+ "areacode": { "value": "65" },
210
+ "nbr": { "value": "96667788" }
211
+ },
212
+ "regadd": {
213
+ "type": "SG",
214
+ "block": { "value": "55" },
215
+ "street": { "value": "BENCOOLEN STREET" },
216
+ "floor": { "value": "08" },
217
+ "unit": { "value": "12" },
218
+ "postal": { "value": "189627" },
219
+ "country": { "code": "SG" }
220
+ }
221
+ },
222
+
223
+ "error_paths": {
224
+ "name": { "value": "ONG WEI MING" },
225
+ "uinfin": { "value": "S8345678F" },
226
+ "dob": { "value": "1983-04-27" },
227
+ "sex": { "code": "M", "desc": "MALE" },
228
+ "race": { "code": "CN", "desc": "CHINESE" },
229
+ "nationality": { "code": "SG", "desc": "SINGAPORE CITIZEN" },
230
+ "residentialstatus": { "code": "C", "desc": "CITIZEN" },
231
+ "marital": { "code": "1", "desc": "SINGLE" },
232
+ "edulevel": { "code": "7", "desc": "BACHELOR'S OR EQUIVALENT" },
233
+ "email": { "value": "weiming.ong@example.com" },
234
+ "mobileno": {
235
+ "prefix": { "value": "+" },
236
+ "areacode": { "value": "65" },
237
+ "nbr": { "value": "93334455" }
238
+ },
239
+ "regadd": {
240
+ "type": "SG",
241
+ "block": { "value": "88" },
242
+ "street": { "value": "QUEENSWAY" },
243
+ "floor": { "value": "10" },
244
+ "unit": { "value": "1010" },
245
+ "postal": { "value": "149053" },
246
+ "country": { "code": "SG" }
247
+ },
248
+ "employment": { "value": "EMPLOYED" }
249
+ }
250
+ }
@@ -0,0 +1,45 @@
1
+ # typed: ignore
2
+
3
+ # Sorbet skips this file — Rails::Generators::Base / Thor's dynamic API is
4
+ # not modelled in tapioca's generated RBIs and adding shims for it has
5
+ # diminishing returns for a one-class generator.
6
+
7
+ require "rails/generators"
8
+
9
+ module StandardSingpass
10
+ module Generators
11
+ # Installs StandardSingpass in a host Rails application.
12
+ #
13
+ # Writes the initializer at `config/initializers/standard_singpass.rb`
14
+ # with the full configuration surface commented out so consumers know
15
+ # what's available without being forced to wire everything up front.
16
+ #
17
+ # Idempotent: re-running the generator skips an existing initializer
18
+ # unless `--force` is passed.
19
+ class InstallGenerator < Rails::Generators::Base
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ desc <<~DESC
23
+ Installs StandardSingpass. By default this:
24
+ * writes config/initializers/standard_singpass.rb
25
+
26
+ The generator is idempotent — an existing initializer is left alone
27
+ unless --force is passed.
28
+ DESC
29
+
30
+ class_option :force, type: :boolean, default: false,
31
+ desc: "Overwrite config/initializers/standard_singpass.rb if it already exists"
32
+
33
+ def copy_initializer
34
+ initializer_path = "config/initializers/standard_singpass.rb"
35
+
36
+ if File.exist?(File.join(destination_root, initializer_path)) && !options[:force]
37
+ say_status("identical", "#{initializer_path} (already exists; pass --force to overwrite)", :blue)
38
+ return
39
+ end
40
+
41
+ template "initializer.rb.erb", initializer_path, force: options[:force]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ # StandardSingpass MyInfo configuration.
2
+ #
3
+ # All Singpass FAPI 2.0 flow logic lives in the gem; this file is the
4
+ # host-side adapter that maps environment variables onto the gem's
5
+ # configuration object.
6
+ #
7
+ # Required environment variables:
8
+ #
9
+ # MYINFO_CLIENT_ID App ID from the Singpass Developer Portal
10
+ # MYINFO_REDIRECT_URL OAuth callback URL (e.g. https://example.com/singpass/callback)
11
+ # MYINFO_PRIVATE_JWKS Full JWKS JSON with private signing + encryption keys
12
+ # (keys identified by "use": "sig" and "use": "enc")
13
+ #
14
+ # Optional environment variables:
15
+ #
16
+ # MYINFO_SCOPE Space-separated scopes (defaults to DEFAULT_SCOPE)
17
+ # MYINFO_USERINFO_URL Userinfo endpoint override (rarely needed)
18
+ # MYINFO_USERINFO_JWKS_URL Userinfo JWKS endpoint override (rarely needed)
19
+ # MYINFO_MIN_ACR Required Authentication Context Class Reference
20
+ # URN, e.g. urn:singpass:authentication:loa:3
21
+ # MYINFO_MOCK_MODE When set, suppresses missing-key warnings and
22
+ # unlocks any mock-callback routes the host registers
23
+
24
+ StandardSingpass::Myinfo.configure do |c|
25
+ # Endpoint set. :production → live Singpass FAPI; :staging → sandbox.
26
+ # If RAILS_ENV is "production" on multiple deployment environments
27
+ # (e.g. staging vs. production share the same RAILS_ENV), key this off
28
+ # your own environment discriminator rather than Rails.env.
29
+ c.environment = Rails.env.production? ? :production : :staging
30
+
31
+ c.client_id = ENV["MYINFO_CLIENT_ID"]
32
+ c.redirect_url = ENV["MYINFO_REDIRECT_URL"]
33
+
34
+ c.scope = ENV.fetch("MYINFO_SCOPE", StandardSingpass::Myinfo::Configuration::DEFAULT_SCOPE)
35
+
36
+ # Endpoint overrides — only set if you're running against a non-standard
37
+ # Singpass sandbox or mocked service.
38
+ c.userinfo_url = ENV["MYINFO_USERINFO_URL"] if ENV["MYINFO_USERINFO_URL"].present?
39
+ c.userinfo_jwks_url = ENV["MYINFO_USERINFO_JWKS_URL"] if ENV["MYINFO_USERINFO_JWKS_URL"].present?
40
+
41
+ c.minimum_acr = ENV["MYINFO_MIN_ACR"]
42
+ c.mock_mode = ENV["MYINFO_MOCK_MODE"].present?
43
+
44
+ # Path to a JSON file of test personas the host can drive mock callbacks
45
+ # against. Defaults to the gem's bundled fixture set. Override here if you
46
+ # maintain your own persona file.
47
+ # c.personas_path = Rails.root.join("e2e/fixtures/myinfo-personas.json")
48
+
49
+ # Wrap outbound Faraday calls in a resilience layer (circuit breaker, retry,
50
+ # tracing). Default is the identity wrapper.
51
+ # c.network_wrapper = ->(&block) { StandardCircuit.run(:myinfo, &block) }
52
+
53
+ # Required: full JWKS JSON containing both sig (ES256) and enc
54
+ # (ECDH-ES+A256KW) keys with the private scalar `d`. Set last so any
55
+ # missing-key warnings the gem logs include the rest of the context.
56
+ c.private_jwks_json = ENV["MYINFO_PRIVATE_JWKS"]
57
+ end
@@ -0,0 +1,17 @@
1
+ # Defines the Rails engine when Rails is loaded. In tooling contexts that
2
+ # load the gem without Rails (e.g. `tapioca gems`, which calls
3
+ # `Bundler.require` before any explicit `require "rails"`), this file is
4
+ # still required by `lib/standard_singpass.rb` but the engine class is
5
+ # simply not defined — which is fine, because rake-task autoloading is
6
+ # only meaningful inside a Rails host anyway.
7
+ if defined?(::Rails::Engine)
8
+ module StandardSingpass
9
+ class Engine < ::Rails::Engine
10
+ isolate_namespace StandardSingpass
11
+
12
+ rake_tasks do
13
+ load File.expand_path("../tasks/standard_singpass.rake", __dir__)
14
+ end
15
+ end
16
+ end
17
+ end