Skip to content

v0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 30 Jul 14:53
365f2ba

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_STATUSES is the documented status list, and Client::RETRYABLE_USERINFO_STATUSES is 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 trap ApiError#status was 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: noa yields the latest year only, noahistory the last two, and PersonDataParser mirrors 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), returns nil when 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 to Rails.env.production?. Mirrors StandardId's option of the same name. Staging and preview deploys routinely run RAILS_ENV=production, so hosts that distinguish a physical deploy environment from RAILS_ENV can supply -> { AppEnv.production? }.

Changed

  • Security::ValidationError and Security::DecryptionError now descend from StandardSingpass::Myinfo::Error (previously bare StandardError), and JWKS-fetch failures carry the JWKS response's status — or transport? when the endpoint was unreachable. Verifying a signature requires fetching Singpass's JWKS, so a JWKS host that is down previously surfaced as a statusless SignatureError (or AuthenticationError on 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.

  • status moved from ApiError up to the base Error class, and is now set on every leg of the flow. 0.2.0 put it on ApiError, which covers the token and userinfo legs — but PAR raises PARError, so a 502 from the PAR endpoint carried no status and any classification keyed on ApiError reported a Singpass-side outage as our own bug. status and transport? now both live on Error, are populated on the PAR and authentication paths as well, and FailureClassifier keys on the base class. ApiError#status is unchanged for existing callers; Error#initialize takes both keywords optionally, so raise SomeError, "msg" still works.

  • config.mock_mode is 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 raises ConfigurationError and refuses to boot; a production-like deploy (RAILS_ENV=production but not production per production_env_detector) logs and reports via Rails.error, then boots; anything else is silent. Wired from the engine's after_initialize with no opt-out — a guard you can forget to invoke is a guard that isn't there.

    Upgrade note: a host that sets mock_mode on a deploy running RAILS_ENV=production will now either fail to boot or emit a reported error. If that deploy is a staging or preview box, set config.production_env_detector so the gem can tell the difference. Hosts that leave mock_mode at its default (false) are unaffected.

    Reporting goes through Rails.error rather 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 ENV itself, and production_env_detector is the seam for the one environment opinion it does hold.