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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b337f846e33125912e93e3819ad6ab9858dea86f6f2976f821650cd41eda5f2d
4
- data.tar.gz: d655d89457c6a8120223dec9c266945ef03601dc79d82268e9ea5149fe7de75b
3
+ metadata.gz: 2ea89fcd5571a503ae9bec3dcc4c69f038e8109e68c3dfba9a119eb08efdf392
4
+ data.tar.gz: 43c8d9ee1f0344de72f0b6bffa181d0df12475c9f10c5ee6d80f92e9bd192a05
5
5
  SHA512:
6
- metadata.gz: 0ccefce8b78a9ad7ef3213738a6266b544241c18e99ce645aa0de0bb1f6067f5bda79f793ee048757c4763cd1f990779649bc86c3d506849c5e3d7cfc19a0290
7
- data.tar.gz: 6fb74b23e6fdf75e2191d7eecc76a45969aac95f6d5dd8383572b563299c15b1013dd2199c2816189248713452f0043c861c74cf9e14d33504357226a3b1f1e7
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 Rails.env.production?.
64
- # c.production_env_detector = -> { AppEnv.production? }
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
- By default "production" means `Rails.env.production?`. Staging and preview
119
- deploys routinely run `RAILS_ENV=production` while being entirely legitimate
120
- places to enable mock mode, so hosts that distinguish a physical deploy
121
- environment from `RAILS_ENV` should say so:
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
- ```ruby
124
- # Mock mode stays legal on a physically-staging box, still refused on real prod.
125
- c.production_env_detector = -> { AppEnv.production? }
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` constants are deprecated aliases of those two classes.
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` still works but is deprecated.
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, falling back to `Rails.env.production?`. The gem cannot know about
31
- # a host's own environment discriminator (`APP_ENVIRONMENT` and friends),
32
- # and deliberately does not read ENV itself — the callable is the seam.
33
- # This mirrors `StandardId`'s `production_env_detector`.
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 (hosts that
54
- # distinguish a physical deploy environment from RAILS_ENV), else falls
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
@@ -1,3 +1,3 @@
1
1
  module StandardSingpass
2
- VERSION = "0.4.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -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.5", "StandardSingpass")
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.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.0'
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.0'
28
+ version: '8.1'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9'