v0.22.0 — Production Security Suite + Test Infrastructure Overhaul
A month-long hardening sprint that turned the template into a security-conscious, enterprise-grade base: tokens moved off plaintext storage into the platform keystore, six opt-in production-security features added, the entire test infrastructure rebuilt around a deterministic global bootstrap with golden + e2e coverage, global mutable config replaced with immutable DI, and the toolchain upgraded to Flutter 3.44.2 / Dart 3.12.2.
By the numbers — 47 PRs merged · ~105 commits · 350 files changed (+17,816 / −2,397) · 1,626 tests passing (up from 1,417) · May 20 → Jun 19, 2026
Production Security Suite (#129–#143)
The headline of this release. A full pass to make the template safe to ship to production.
Secure token storage + JWT sanitization (#129)
The largest single effort — JWT access/refresh tokens no longer live in plaintext SharedPreferences.
- Tokens persisted via
flutter_secure_storage(iOS Keychain, Android AES-GCM ciphers); keychain accessibility pinned tofirst_unlock_this_device(no iCloud sync). Non-secret session fields (username,roles) stay inSharedPreferencesfor synchronous access. - A single
ISecureStorageinstance is threaded through DI (AppDependencies→AppScope→ interceptors) — no ad-hoc instantiation. - One-shot
SessionMigrationcopies legacy plaintext keys into secure storage on first launch after upgrade, then cleans up lingering plaintext. LogSanitizer.maskToken+ maskedtoString()onAuthSession/JWTToken; OTP debug logs sanitized — no secret ever reaches a log sink.- Single-flight refresh: concurrent 401s coalesce into one
/api/token/refreshcall; refresh-loop guard viaRequestOptions.extramarker; symmetric rollback on partial-write failure; all UI logout sites now wipe secure storage. - Hardened across ~40 commits and 21 rounds of independent/Copilot PR review.
Six opt-in production-security features
| Feature | Issue / PR | What it does |
|---|---|---|
| Screen-capture protection | #134 / #138 | Blocks screenshots/recording on sensitive screens (opt-in) |
| Idempotency-Key header | #136 / #139 | Opt-in Idempotency-Key on POST/PUT/PATCH to defeat duplicate writes |
| Inactivity auto-logout | #130 / #140 | Configurable idle timeout (15 min prod default, off in dev); background-aware via wall-clock check |
| Certificate pinning | #133 / #141 | Configurable SHA-256 pins for production HTTPS; default off; web is a documented no-op |
| Sentry crash reporting | #131 / #142 | Opt-in production backend with conservative PII/token scrubber; no DSN committed |
| Role-based route guards | #132 / #143 | Admin/User route protection + returnUrl propagation through the auth flow |
Test Infrastructure Overhaul (#135, #147–#155, #168)
The test suite was rebuilt from scattered per-file setup into a coherent, deterministic harness.
- Global bootstrap +
TestEnv(#150) —flutter_test_config.dartruns once per isolate (binding init, logger/Equatable config, mocktail fallbacks) and resets state per test. Tests need zero manual storage/router/secure-storage setup. - Deterministic time (#148) —
fakeAsynceverywhere; no real-timeawait Future.delayedwaits in tests. - Golden tests (#135, 4 phases) —
alchemistharness → component goldens → shared-widget goldens → screen goldens; relocated to a feature-first mirror. - E2E smoke test (#152) — full-app boot → login → dashboard flow.
dart_test.yaml+ test tags (#154), test-double convention docs (#155) (mocktail mock / hand-fake / Dio handler stub),pumpAndSettlehygiene (#151).- Screen-test quality epic (#168) —
whenListen-based bloc stubbing, verify-by-event-type, dead-mock and brittle-text-assert cleanup. - Coverage gate (#149) excludes generated/bootstrap code; new
docs/testing-architecture.md. - 1,417 → 1,626 tests.
Architecture & Configuration
- Immutable
AppConfig+ DI-injectedApiClient(#144 / #146) — replaced the global mutable config map (a ~2019 pattern) with a typed, immutableAppConfiginjected through DI. No global state. dynamic_formsrelocatedfeatures/→shared/(#121) — the forms engine has no standalone domain entity, so it belongs inshared/. AddedFormBundleEntity+ a{schema, values}bundle endpoint, nativepathParamssupport, and a User Extended-Info page that renders a dynamic schema with prefilled values (/user/:id/extended-info).
Tooling & Developer Experience
- Dev console — opt-in verbose request/response body logging with a Network-tab toggle; mock mode now feeds the DevConsole capture + verbose logging.
- Fix — Environment tab no longer crashes with a
GoRouterStateerror.
Toolchain & Dependencies (#173)
- Flutter 3.44.0 → 3.44.2, Dart 3.12.0 → 3.12.2.
- Bumps:
go_router17.2.3 → 17.3.0,flutter_secure_storage10.2.0 → 10.3.1,sentry_flutter9.20.0 → 9.22.0. - Documented pins (with inline rationale):
test1.31.0 (SDK pinstest_api 0.7.11),intl_utils2.8.15 (analyzer cap),screen_protector1.5.1 (1.5.2 dropped its KotlinjvmTargetpin → Android release-build JVM-target mismatch under JDK 21+). - Verified across
dart analyze, 1,626 tests, web + APK + appbundle release builds, and a Chrome mock-login smoke run.
Documentation
README.md+CLAUDE.mdsynced to current architecture; secure-storage design + wiring docs; dynamic-forms engine docs;web/llms*.txtmetadata reconciled withpubspec.yaml.
Full changelog: v0.21.0...v0.22.0