standard_singpass 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +17 -18
- data/lib/standard_singpass/myinfo/ecdh_jwe.rb +0 -25
- data/lib/standard_singpass/myinfo/mock_mode_guard.rb +17 -8
- data/lib/standard_singpass/myinfo/security.rb +0 -15
- data/lib/standard_singpass/myinfo/test_personas.rb +0 -12
- data/lib/standard_singpass/version.rb +1 -1
- data/lib/standard_singpass.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ea89fcd5571a503ae9bec3dcc4c69f038e8109e68c3dfba9a119eb08efdf392
|
|
4
|
+
data.tar.gz: 43c8d9ee1f0344de72f0b6bffa181d0df12475c9f10c5ee6d80f92e9bd192a05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8a3c50ad2b135eef0d4ecea3d979b21f1b223a54966114544f08aa1ce78fad0d25779e28f365c8ff57c87ae1ec03836ab1290624e11d0de3c0dbc862324d884
|
|
7
|
+
data.tar.gz: c335087baa92503937ed273d6a5528bac1310ac2636a7e529e7f9619e4414b6ce64245142760899544327b4e8138fdbaebde31205e8f7cbd44ed91aa68bfa250
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The mock-mode guard follows `APP_ENVIRONMENT`, failing closed.** Without a
|
|
15
|
+
`production_env_detector`, "production deploy" is now `APP_ENVIRONMENT` when it
|
|
16
|
+
is set (only `staging`, `preview`, `development` and `test` are non-production;
|
|
17
|
+
any other value is production), else `Rails.env.production?` as before. A
|
|
18
|
+
staging box with `RAILS_ENV=production` and `APP_ENVIRONMENT=staging` boots
|
|
19
|
+
with mock mode on (reported as production-like) without detector wiring. The
|
|
20
|
+
same convention as standard_id, standard_audit and standard_health. A
|
|
21
|
+
configured detector still wins, so existing hosts are unaffected.
|
|
22
|
+
- **Requires Rails 8.1** (`rails >= 8.1, < 9`, was `>= 8.0`). Every consumer
|
|
23
|
+
app runs 8.1; 8.0 was never exercised in CI.
|
|
24
|
+
|
|
25
|
+
## [0.5.0] - 2026-09-24
|
|
26
|
+
|
|
27
|
+
**Breaking.** Removes everything 0.4.0 deprecated. No other behaviour changes.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- **`StandardSingpass::Myinfo::Security::DecryptionError` / `Security::ValidationError`.** These were aliases of `StandardSingpass::Myinfo::DecryptionError` / `SignatureError`, which `Security` has raised directly since 0.4.0. Referencing the old names now raises `NameError`. Rescue `DecryptionError` / `SignatureError`, or the `StandardSingpass::Myinfo::Error` base.
|
|
32
|
+
- **`StandardSingpass::Myinfo::EcdhJwe.encrypt`**, the forwarder. Use `require "standard_singpass/testing"` and `StandardSingpass::Testing::EcdhJwe.encrypt` (same signature). `EcdhJwe.decrypt` is unchanged.
|
|
33
|
+
- **`StandardSingpass::Myinfo::TestPersonas.reload!`**. Personas are read once per process. Restart, or point `personas_path` at a different file.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `StandardSingpass.deprecator`'s horizon is now `0.6`.
|
|
38
|
+
|
|
39
|
+
### Upgrade notes
|
|
40
|
+
|
|
41
|
+
- **No code change required in the one consumer, fundbright-web.** Grepped `origin/main` on 2026-09-24: no reference to `Security::DecryptionError`, `Security::ValidationError`, `EcdhJwe.encrypt` or `TestPersonas.reload!`.
|
|
42
|
+
- Regenerate the gem RBI when bumping (`bin/tapioca gem standard_singpass`). `sorbet/rbi/gems/standard_singpass@0.4.0.rbi` still declares the removed constants and methods.
|
|
43
|
+
|
|
10
44
|
## [0.4.0] - 2026-09-24
|
|
11
45
|
|
|
12
46
|
### Upgrading from 0.3.x
|
data/README.md
CHANGED
|
@@ -60,8 +60,9 @@ StandardSingpass::Myinfo.configure do |c|
|
|
|
60
60
|
c.mock_mode = ENV["MYINFO_MOCK_MODE"].present?
|
|
61
61
|
|
|
62
62
|
# Optional: how the gem decides whether this deploy is real production, for
|
|
63
|
-
# the mock-mode guard. Defaults to
|
|
64
|
-
#
|
|
63
|
+
# the mock-mode guard. Defaults to APP_ENVIRONMENT (failing closed), then
|
|
64
|
+
# Rails.env.production? — see "Mock mode" below.
|
|
65
|
+
# c.production_env_detector = -> { ... }
|
|
65
66
|
end
|
|
66
67
|
```
|
|
67
68
|
|
|
@@ -115,21 +116,19 @@ opt-out — a guard you can forget to wire is a guard that isn't there.
|
|
|
115
116
|
| Anything else | yes | Silent. |
|
|
116
117
|
| Any | no | Silent. |
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
"Production" follows the estate's `APP_ENVIRONMENT` convention (the same rule
|
|
120
|
+
standard_id, standard_audit and standard_health use): when `APP_ENVIRONMENT` is
|
|
121
|
+
set, only `staging`, `preview`, `development` and `test` are non-production and
|
|
122
|
+
any other value (a typo included) is production, so a mis-set variable fails
|
|
123
|
+
closed; when it is unset, `Rails.env.production?` decides. Staging and preview
|
|
124
|
+
deploys routinely run `RAILS_ENV=production`, so with `APP_ENVIRONMENT=staging`
|
|
125
|
+
they land in the production-like tier, where mock mode boots and is reported,
|
|
126
|
+
with no extra wiring.
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
`production_env_detector` takes no arguments and returns a boolean; when `nil`
|
|
129
|
-
(the default) the gem falls back to `Rails.env.production?`, so existing
|
|
130
|
-
consumers are unaffected. The production-like tier reports through
|
|
131
|
-
`Rails.error` rather than a specific error tracker, so whatever the host has
|
|
132
|
-
subscribed (Sentry included) picks it up.
|
|
128
|
+
`config.production_env_detector` (a callable taking no arguments, returning a
|
|
129
|
+
boolean) overrides that, for a host with a different discriminator. The
|
|
130
|
+
production-like tier reports through `Rails.error` rather than a specific error
|
|
131
|
+
tracker, so whatever the host has subscribed (Sentry included) picks it up.
|
|
133
132
|
|
|
134
133
|
## Initiating the flow
|
|
135
134
|
|
|
@@ -215,7 +214,7 @@ All errors descend from `StandardSingpass::Myinfo::Error`, which in turn descend
|
|
|
215
214
|
- `RateLimitError` — Singpass returned HTTP 429
|
|
216
215
|
- `ConfigurationError` — gem is misconfigured (e.g. invalid ACR URN)
|
|
217
216
|
|
|
218
|
-
`Security` raises `DecryptionError` / `SignatureError` directly. The pre-0.4.0 `Security::DecryptionError` / `Security::ValidationError`
|
|
217
|
+
`Security` raises `DecryptionError` / `SignatureError` directly. The pre-0.4.0 `Security::DecryptionError` / `Security::ValidationError` aliases were removed in 0.5.0; rescue `DecryptionError` / `SignatureError` (or `StandardSingpass::Myinfo::Error`).
|
|
219
218
|
|
|
220
219
|
`DecryptionError` and `SignatureError` usually indicate a key/cert misconfiguration rather than an upstream outage. **Usually, not always:** verifying a signature requires fetching Singpass's JWKS, so a JWKS host that is down surfaces as a `SignatureError` (or an `AuthenticationError` on the ID-token leg) too. Those carry the JWKS response's `status` / `transport?`, so ask `FailureClassifier` rather than deciding by class — including when choosing what to feed a circuit breaker.
|
|
221
220
|
|
|
@@ -301,7 +300,7 @@ bundle exec srb tc # Sorbet typecheck — keep it green
|
|
|
301
300
|
)
|
|
302
301
|
```
|
|
303
302
|
|
|
304
|
-
`standard_singpass/testing` is not loaded by `require "standard_singpass"`. The old `StandardSingpass::Myinfo::EcdhJwe.encrypt`
|
|
303
|
+
`standard_singpass/testing` is not loaded by `require "standard_singpass"`. The old `StandardSingpass::Myinfo::EcdhJwe.encrypt` forwarder was removed in 0.5.0.
|
|
305
304
|
- **Specs that mutate configuration** should call `StandardSingpass::Myinfo.reset_configuration!` afterwards.
|
|
306
305
|
|
|
307
306
|
## License
|
|
@@ -41,31 +41,6 @@ module StandardSingpass
|
|
|
41
41
|
"A256GCM" => 32
|
|
42
42
|
}.freeze, T::Hash[String, Integer])
|
|
43
43
|
|
|
44
|
-
# Deprecated (0.4.0): encryption is only ever needed to build fixtures
|
|
45
|
-
# in tests — Singpass encrypts, we decrypt. It lives in
|
|
46
|
-
# `StandardSingpass::Testing::EcdhJwe` now (`require
|
|
47
|
-
# "standard_singpass/testing"`); this forwarder will be removed in a
|
|
48
|
-
# future minor release.
|
|
49
|
-
sig do
|
|
50
|
-
params(
|
|
51
|
-
payload: String,
|
|
52
|
-
public_key: OpenSSL::PKey::EC,
|
|
53
|
-
alg: String,
|
|
54
|
-
enc: String,
|
|
55
|
-
kid: T.nilable(String),
|
|
56
|
-
apu: T.nilable(String),
|
|
57
|
-
apv: T.nilable(String)
|
|
58
|
-
).returns(String)
|
|
59
|
-
end
|
|
60
|
-
def self.encrypt(payload, public_key:, alg:, enc:, kid: nil, apu: nil, apv: nil)
|
|
61
|
-
StandardSingpass.deprecator.warn(
|
|
62
|
-
"StandardSingpass::Myinfo::EcdhJwe.encrypt is deprecated and will be removed; " \
|
|
63
|
-
"require \"standard_singpass/testing\" and use StandardSingpass::Testing::EcdhJwe.encrypt instead."
|
|
64
|
-
)
|
|
65
|
-
require "standard_singpass/testing"
|
|
66
|
-
StandardSingpass::Testing::EcdhJwe.encrypt(payload, public_key:, alg:, enc:, kid:, apu:, apv:)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
44
|
# Decrypts a compact-serialized JWE string.
|
|
70
45
|
sig { params(jwe_string: String, private_key: OpenSSL::PKey::EC).returns(String) }
|
|
71
46
|
def self.decrypt(jwe_string, private_key:)
|
|
@@ -27,10 +27,13 @@ module StandardSingpass
|
|
|
27
27
|
# loud-but-non-fatal signal instead.
|
|
28
28
|
#
|
|
29
29
|
# "Production deploy" is decided by `config.production_env_detector` when
|
|
30
|
-
# set,
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# set, else by the estate's APP_ENVIRONMENT convention (shared with
|
|
31
|
+
# standard_id, standard_audit and standard_health): APP_ENVIRONMENT when it
|
|
32
|
+
# is set, where only staging, preview, development and test are
|
|
33
|
+
# non-production and any other value (a typo included) is production, so a
|
|
34
|
+
# mis-set variable fails closed; else `Rails.env.production?`. A staging box
|
|
35
|
+
# with RAILS_ENV=production and APP_ENVIRONMENT=staging is therefore
|
|
36
|
+
# production-*like*, not production, with no detector wiring.
|
|
34
37
|
#
|
|
35
38
|
# Wired automatically from the engine's `after_initialize`, so a host gets
|
|
36
39
|
# the guard by configuring `mock_mode` at all. There is no opt-out: a
|
|
@@ -38,6 +41,9 @@ module StandardSingpass
|
|
|
38
41
|
module MockModeGuard
|
|
39
42
|
extend T::Sig
|
|
40
43
|
|
|
44
|
+
# APP_ENVIRONMENT values that are not production; anything else is.
|
|
45
|
+
NON_PRODUCTION_APP_ENVIRONMENTS = T.let(%w[staging preview development test].freeze, T::Array[String])
|
|
46
|
+
|
|
41
47
|
sig { void }
|
|
42
48
|
def self.check!
|
|
43
49
|
return unless StandardSingpass::Myinfo.configuration.mock_mode
|
|
@@ -50,14 +56,16 @@ module StandardSingpass
|
|
|
50
56
|
end
|
|
51
57
|
|
|
52
58
|
# Whether this deploy is real production for the purpose of the guard.
|
|
53
|
-
# Defers to config.production_env_detector when set
|
|
54
|
-
#
|
|
55
|
-
# back to Rails.env.production?.
|
|
59
|
+
# Defers to config.production_env_detector when set, else to
|
|
60
|
+
# APP_ENVIRONMENT (failing closed), else Rails.env.production?.
|
|
56
61
|
sig { returns(T::Boolean) }
|
|
57
62
|
def self.production_deploy?
|
|
58
63
|
detector = StandardSingpass::Myinfo.configuration.production_env_detector
|
|
59
64
|
return !!detector.call if detector
|
|
60
65
|
|
|
66
|
+
app_env = ENV["APP_ENVIRONMENT"].to_s.strip
|
|
67
|
+
return !NON_PRODUCTION_APP_ENVIRONMENTS.include?(app_env) unless app_env.empty?
|
|
68
|
+
|
|
61
69
|
rails_production?
|
|
62
70
|
end
|
|
63
71
|
|
|
@@ -75,7 +83,8 @@ module StandardSingpass
|
|
|
75
83
|
"identity data that is indistinguishable from verified Myinfo " \
|
|
76
84
|
"data downstream. The application refuses to boot. Unset the " \
|
|
77
85
|
"mock-mode environment variable on this deploy and redeploy. If " \
|
|
78
|
-
"this deploy is not actually production, set " \
|
|
86
|
+
"this deploy is not actually production, set APP_ENVIRONMENT " \
|
|
87
|
+
"(staging, preview, development or test) or " \
|
|
79
88
|
"config.production_env_detector so the gem can tell."
|
|
80
89
|
end
|
|
81
90
|
private_class_method :production_message
|
|
@@ -5,21 +5,6 @@ module StandardSingpass
|
|
|
5
5
|
class Security
|
|
6
6
|
extend T::Sig
|
|
7
7
|
|
|
8
|
-
# Deprecated aliases (0.4.0). Security used to raise its own
|
|
9
|
-
# DecryptionError / ValidationError, which the client then translated
|
|
10
|
-
# into the public Myinfo errors. It now raises the public errors
|
|
11
|
-
# directly: JWE failures raise `Myinfo::DecryptionError`, JWS / JWKS
|
|
12
|
-
# failures raise `Myinfo::SignatureError` (carrying the JWKS response's
|
|
13
|
-
# `status`, or `transport?` when the JWKS host was unreachable, so an
|
|
14
|
-
# outage stays distinguishable from a genuinely bad signature).
|
|
15
|
-
#
|
|
16
|
-
# The old names still resolve — to the same classes, so an existing
|
|
17
|
-
# `rescue Security::ValidationError` keeps catching — and will be
|
|
18
|
-
# removed in a future minor release.
|
|
19
|
-
DecryptionError = Myinfo::DecryptionError
|
|
20
|
-
ValidationError = Myinfo::SignatureError
|
|
21
|
-
deprecate_constant :DecryptionError, :ValidationError
|
|
22
|
-
|
|
23
8
|
JWKS_CACHE_TTL = T.let(1.hour, ActiveSupport::Duration)
|
|
24
9
|
|
|
25
10
|
# Generates a PKCE code verifier and code challenge pair (S256).
|
|
@@ -31,18 +31,6 @@ module StandardSingpass
|
|
|
31
31
|
@data ||= JSON.parse(fixture_path.read).freeze
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Deprecated (0.4.0) — unused by any consumer, and will be removed in a
|
|
35
|
-
# future minor release. Personas are read once per process; restart the
|
|
36
|
-
# process (or point `personas_path` at a different file) to pick up
|
|
37
|
-
# edits.
|
|
38
|
-
def self.reload!
|
|
39
|
-
StandardSingpass.deprecator.warn(
|
|
40
|
-
"StandardSingpass::Myinfo::TestPersonas.reload! is deprecated and will be removed."
|
|
41
|
-
)
|
|
42
|
-
@data = nil
|
|
43
|
-
data
|
|
44
|
-
end
|
|
45
|
-
|
|
46
34
|
def self.fixture_path
|
|
47
35
|
configured = StandardSingpass::Myinfo.configuration.personas_path
|
|
48
36
|
configured ? Pathname.new(configured) : GEM_FIXTURE_PATH
|
data/lib/standard_singpass.rb
CHANGED
|
@@ -25,7 +25,7 @@ module StandardSingpass
|
|
|
25
25
|
# with the host's `Rails.application.deprecators` by the engine, so the
|
|
26
26
|
# host's deprecation behaviour (log / raise / silence) applies.
|
|
27
27
|
def deprecator
|
|
28
|
-
@deprecator ||= ActiveSupport::Deprecation.new("0.
|
|
28
|
+
@deprecator ||= ActiveSupport::Deprecation.new("0.6", "StandardSingpass")
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_singpass
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '8.
|
|
18
|
+
version: '8.1'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '9'
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '8.
|
|
28
|
+
version: '8.1'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9'
|