Skip to content

fix(config): guard against reading Config singleton before bootstrap#3835

Merged
TaprootFreak merged 1 commit into
developfrom
fix/guard-config-at-bootstrap
Jun 8, 2026
Merged

fix(config): guard against reading Config singleton before bootstrap#3835
TaprootFreak merged 1 commit into
developfrom
fix/guard-config-at-bootstrap

Conversation

@TaprootFreak

Copy link
Copy Markdown
Collaborator

Background

A recent incident took the whole API down on DEV: PricingRealUnitService read the Config singleton in a class field initializer. Config (export let Config in src/config/config.ts) is undefined until ConfigService is constructed, so when that provider was instantiated before ConfigService, Config.environment threw and NestJS bootstrap crashed (all health checks down). That service was fixed separately (#3832); this PR prevents the whole class of bug from recurring.

What this adds

A) Static guard + fix latent landmines

  • ESLint rule (no-restricted-syntax) forbidding reads of the Config singleton in field initializers of @Injectable/@Controller classes. Scoped to those decorators so request DTOs/entities (built at runtime, when Config is already set) are not affected.
  • Converted 4 pre-existing latent occurrences of the same pattern to getters — they currently survive only by instantiation-ordering luck:
    • faucet-request.service.ts (faucetBlockchain)
    • realunit.service.ts (tokenBlockchain)
    • lnurl-forward.service.ts (PAYMENT_LINK_PREFIX, PAYMENT_LINK_PAYMENT_PREFIX)

B) Regression test

src/config/__tests__/config-bootstrap-order.spec.ts compiles each affected provider in isolation, without ConfigService (so Config is undefined during construction) and asserts it does not throw. Verified to fail on the old field-initializer pattern with the exact production error and pass with getters.

Why not a full app-bootstrap test

A full AppModule.compile() smoke test is not viable pre-merge: many blockchain/exchange providers do real I/O in their constructors (Spark wallet init, Scrypt websocket, ethers clients) and need real secrets/URLs. The static rule + isolated regression test cover the failure class cleanly without that; the full boot is already exercised by the DEV deploy.

Local checks (npm ci, all green)

lint ✅ · format:check ✅ · build ✅ · test (76 suites / 1014 tests) ✅ · npm audit --audit-level=critical

Test plan

  • New regression spec passes (5/5) and fails on the reintroduced bug pattern
  • ESLint rule errors on @Injectable field-initializer Config reads, ignores DTOs
  • Full unit suite green; no existing specs broken

The exported Config singleton is undefined until ConfigService is constructed.
Reading it in a class field initializer crashes NestJS bootstrap when the
provider is instantiated before ConfigService (provider-ordering / circular
dependency) - this previously took the whole API down.

Add two guards:
- ESLint no-restricted-syntax rule forbidding Config reads in field
  initializers of @Injectable/@controller classes, and convert four
  pre-existing latent occurrences (faucet-request, realunit, lnurl x2) to
  getters so they survive any future instantiation-ordering change.
- Regression test that compiles each affected provider in isolation without
  ConfigService (Config undefined) and asserts construction does not throw.
@TaprootFreak TaprootFreak marked this pull request as ready for review June 8, 2026 10:10
@TaprootFreak TaprootFreak requested a review from davidleomay as a code owner June 8, 2026 10:10
@TaprootFreak TaprootFreak merged commit 2036d28 into develop Jun 8, 2026
7 checks passed
@TaprootFreak TaprootFreak deleted the fix/guard-config-at-bootstrap branch June 8, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants