Added
StandardSingpass::Myinfo::FailureClassifier.upstream_unavailable?(error)— one answer to the question every host asks in its rescue: is Singpass unavailable, or is our request wrong? It decides what the user is told, and the two answers are not interchangeable — an unavailable upstream clears itself within minutes ("try again shortly"), while "contact support and quote this reference" is a dead end for a transient blip. True for 502/503/504, HTTP 429, and transport failures on any leg; false for authentication, decryption, signature, and configuration errors.FailureClassifier::UPSTREAM_UNAVAILABLE_STATUSESis the documented status list, andClient::RETRYABLE_USERINFO_STATUSESis now an alias of it rather than a second copy — the statuses worth one more automatic attempt are exactly the statuses that mean "their side, transient".StandardSingpass::Myinfo::Error#transport?— true when the request never reached Singpass at all (DNS, connection refused, TLS, read timeout). Set on the PAR, token, and userinfo transport-failure paths. Previously the only way to recover this was to match"unreachable"in the message, which is the same trapApiError#statuswas added in 0.2.0 to close: the message is not a stable interface.StandardSingpass::Myinfo::Noa.history(parsed)/.basic_history(parsed)— normalise Notice-of-Assessment data into a single list regardless of which NOA scope the Singpass app was approved for. The width is a property of the app registration, not the request:noayields the latest year only,noahistorythe last two, andPersonDataParsermirrors that as:noa(one record) or:noa_history(an array). The records share a shape, so every consumer ended up writing the same "array, or wrap the single record" branch. Prefers the 2-year history when both are present (it is a superset), returnsnilwhen neither scope yielded data, and reads both Symbol and String keys so a hash round-tripped through JSON or a database column works unchanged.config.production_env_detector— an optional callable deciding whether the current deploy is real production, for the mock-mode guard below. Takes no arguments, returns a boolean;nil(the default) falls back toRails.env.production?. MirrorsStandardId's option of the same name. Staging and preview deploys routinely runRAILS_ENV=production, so hosts that distinguish a physical deploy environment fromRAILS_ENVcan supply-> { AppEnv.production? }.
Changed
-
Security::ValidationErrorandSecurity::DecryptionErrornow descend fromStandardSingpass::Myinfo::Error(previously bareStandardError), and JWKS-fetch failures carry the JWKS response's status — ortransport?when the endpoint was unreachable. Verifying a signature requires fetching Singpass's JWKS, so a JWKS host that is down previously surfaced as a statuslessSignatureError(orAuthenticationErroron the ID-token leg), indistinguishable from a genuinely bad signature and classified as our own bug. The client now propagates both attributes across those re-raises. -
statusmoved fromApiErrorup to the baseErrorclass, and is now set on every leg of the flow. 0.2.0 put it onApiError, which covers the token and userinfo legs — but PAR raisesPARError, so a 502 from the PAR endpoint carried no status and any classification keyed onApiErrorreported a Singpass-side outage as our own bug.statusandtransport?now both live onError, are populated on the PAR and authentication paths as well, andFailureClassifierkeys on the base class.ApiError#statusis unchanged for existing callers;Error#initializetakes both keywords optionally, soraise SomeError, "msg"still works. -
config.mock_modeis now guarded at boot (StandardSingpass::Myinfo::MockModeGuard). Mock mode serves fixture persona data instead of running the real FAPI 2.0 flow — right for development, CI, and staging rehearsals, catastrophic in production, where real users would submit fabricated identity data that is indistinguishable from verified MyInfo data downstream. A mis-set environment variable is all it takes, and nothing about the running app looks wrong afterwards, so it is checked at boot rather than per request. Three outcomes: a production deploy raisesConfigurationErrorand refuses to boot; a production-like deploy (RAILS_ENV=productionbut not production perproduction_env_detector) logs and reports viaRails.error, then boots; anything else is silent. Wired from the engine'safter_initializewith no opt-out — a guard you can forget to invoke is a guard that isn't there.Upgrade note: a host that sets
mock_modeon a deploy runningRAILS_ENV=productionwill now either fail to boot or emit a reported error. If that deploy is a staging or preview box, setconfig.production_env_detectorso the gem can tell the difference. Hosts that leavemock_modeat its default (false) are unaffected.Reporting goes through
Rails.errorrather than any specific error tracker, so whatever the host has subscribed (Sentry included) picks it up.
Notes
- All three additions are promotions of knowledge that had accumulated in a consumer app. Each is MyInfo protocol or scope-grant knowledge that any Singpass client needs — which statuses Singpass uses for upstream-agency outages, how the two NOA scope widths differ in shape, and that the gem's own mock mode must never reach real users — rather than host policy. Host policy (what copy to show a user, where to read an environment variable from) stays in the host: the gem still never reads
ENVitself, andproduction_env_detectoris the seam for the one environment opinion it does hold.