ephemeral-tokens.ts was free functions, and AuthService owned the cleanup
timer only because it happened to be the domain that mints tokens. Issuing
them and sweeping them are different jobs, and FilesService and TokenService
mint or consume them too.
EphemeralTokenService owns the lifecycle and hands the three operations out.
The store and the interval stay module state next door, and that is
load-bearing rather than a leftover: src/websocket.ts consumes ws tokens on
the upgrade handshake, outside the container, before any injector is in
reach. Per-instance state would hand it an empty Map and close every
connection with 4001, which no test would catch because the ws suites drive
the transport directly. WS-027 through WS-029 pin the session gate that runs
through exactly that path.
The MAX_STORE_SIZE back-pressure is the second reason: createEphemeralToken
returns null at 10,000 live tokens and TokenService turns that into a 503. A
second store would raise the real ceiling with nobody noticing.
Its own leaf module rather than a provider in AuthModule: TokensModule mints
download and ws tokens as well, AuthModule already imports TokensModule, and
TokensModule is documented as a deliberate leaf that three graphs reach for.
Same shape as MailerModule, which broke AuthModule <-> NotificationsModule.
Twelve hand-wired construction sites in tests needed the new collaborator.
The typecheck named all twelve; before this branch they would have been
undefined at runtime.
Sixteen more test files are cleared here too, each at its root cause. Four of
them were passing for the wrong reason:
files.controller.test.ts drove two handlers with shifted arguments, a string
tripId where the trip row belongs and undefined where tripId and id belong.
It stayed green because FilesService is fully stubbed and the stubs ignore
what they are given, so DELETE /:id/permanent and DELETE /trash/empty never
covered that the trip row reaches the permission check.
conflictUpdate.test.ts was missing real collaborators on PlacesService and
PackingService. None of the exercised paths reach them, so it passed by luck.
assignments.service.test.ts builds its service by hand, and the one consumer
of the collaborator it was missing swallows every throw in a bare catch. A
broken journey reconciliation would still have shown 28 green tests.
scheduler.test.ts mocks two modules with paths that resolve one directory
above the server root, so they mock nothing at all. Harmless only because
src/scheduler.ts imports neither.
conditions.test.ts lost noTrips when the condition kind landed; oauth.test.ts
lost the RFC 8707 resource indicator when it became required. Both are
fixtures that stopped matching a contract that grew, and both are filled in
with the value that keeps every existing case reading as it did.
budget.service.calc.test.ts typed its prepare mock from the default
implementation rather than from prepare's own signature, so all ten later
mockImplementation calls had to reproduce that exact three-member literal.
One stmt() helper names the widening instead.
Prevents users who upgrade across multiple versions from seeing all
interim notices at once. Version bounds are evaluated server-side using
semver.coerce so prerelease builds compare as their base release.
Range is lower-inclusive, upper-exclusive: maxVersion: '4.0.0' hides
the notice once 4.0.0 ships.
Server-side notice registry with per-user condition evaluation (firstLogin,
existingUserBeforeVersion, addonEnabled, dateWindow, role, custom).
Notices are sorted by priority then severity, filtered against dismissals
stored in a new user_notice_dismissals table, and served via
GET /api/system-notices/active + POST /api/system-notices/:id/dismiss.
Client renders notices through a host component that partitions by
display type (modal / banner / toast). The modal renderer supports
multi-page pagination with directional slide transitions, keyboard
navigation, and correct dismiss-all semantics on CTA / X / ESC.
Dismissals are optimistic with a single background retry.
Includes 3.0.0 upgrade notices (v3-photos, v3-journey, v3-features),
onboarding welcome modal, and full i18n coverage across 15 languages.
The /journey route is addon-gated on both client and server.
Also includes: unit + integration test suites, registry integrity test
that validates action CTA IDs against client source, and technical
documentation in docs/system-notices.md.