fix(billing-service): import @fuzefront/shared via its declared ./kafka export - #824
Conversation
…ka export Root cause: @fuzefront/shared's package.json declares an `exports` map with exactly two entries, `.` and `./kafka`. That map makes every undeclared subpath unreachable under Node's package-exports resolution - `@fuzefront/shared/dist/kafka`, which billing-service imported in six source files (plus one test), is not a valid specifier for this package and should raise ERR_PACKAGE_PATH_NOT_EXPORTED. It only appeared to work because billing-service's production Dockerfile copied `shared/dist` into the image without `shared/package.json`. With no package.json present at that path, Node has no `exports` field to enforce, so the deep path resolved by plain filesystem lookup instead - an accident of packaging, not a valid resolution. applications-service hit the real failure mode in #751: it copies both `shared/dist` and `shared/package.json`, so the same deep import crash-looped it in prod (70 restarts) until fixed. Adding the missing COPY line to billing-service, on its own, would have reproduced that crash rather than fixed anything - the imports had to change first. Fix: - All @fuzefront/shared/dist/kafka imports in services/billing-service now use the declared ./kafka subpath (src/index.ts, kafka/producer.ts, kafka/consumer.ts, kafka/ref-index.consumer.ts, kafka/org-deleted.consumer.ts, kafka/org-deleted.handler.ts, tests/kafka/org-deleted.handler.test.ts - two more source files and the one test than the issue's four call sites, found by grepping the whole service rather than trusting the listed set). - jest.config.js moduleNameMapper key renamed to match the new specifier. - tsconfig.json gains an explicit `paths` entry for @fuzefront/shared/kafka (baseUrl "." + relative path to shared/dist/kafka/index.d.ts), matching the existing convention in backend/tsconfig.json and backend/security/tsconfig.json for the same typed-import case. - Dockerfile production stage now also COPYs shared/package.json alongside shared/dist, the same pairing applications-service's Dockerfile already uses (backend/applications/Dockerfile:86-87). Without the manifest, the *correct* subpath import would 404 in the image even though it type-checks and resolves locally - the exports map has to travel with the dist output for the supported entry point to actually be reachable at runtime. - Removed billing-service from KNOWN_UNMIGRATED in backend/applications/tests/sharedSubpathExports.test.ts, so that guard now covers it as the issue asked. Verified statically (no npm install / no image build - disk-constrained worktree): re-implemented the guard test's file-scan logic against the working tree and confirmed it finds 42 @fuzefront/shared import sites repo-wide with zero offenders now that billing-service is migrated; confirmed no `shared/dist/kafka` import specifier remains anywhere in the repo (only paths/mapper targets, which correctly point at the physical dist file); confirmed the edited tsconfig.json and jest.config.js both parse; confirmed billing-service names no bare `@fuzefront/shared` import that would additionally depend on the package's "main"/root export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
|
This PR touches only Why it surfaced here and not on the sibling PRs: the gate is path-filtered to It is a real defect, not a gate artifact. Verified directly: both paths are declared in Fix is in flight separately — a dedicated PR is being prepared for config-service, per the gate's own instruction: "fix the CODE to match it, or change the contract deliberately — never regenerate the spec from the code." It is deliberately not folded in here, because implementing a reveal-once secrets endpoint is security-sensitive work that deserves its own review, and widening a dependency-import fix to carry it would bury both. No re-run spent: a re-run cannot clear a missing route. Generated by Claude Code |
|
Verified rather than assumed: The fix exists: #840 implements both endpoints ( Not ported into this PR, deliberately. The protocol says to port an existing fix rather No re-run spent — a re-run cannot fix a deterministic contract gap. Generated by Claude Code |
…nfig/secrets/reveal (#840) Fixes the contract-conformance defect surfaced on #824: both endpoints were declared in services/config-service/openapi.yaml (the frozen contract) but had no implementing route, so a client generated from the contract got a 404. DECISION: option (a), implement — the endpoints are the load-bearing half of the 1.1.0 changelog (secret-audit flow, FFRNT-280), the spec's own prose already ties PUT /v1/config's `revertOf` to "an earlier value from GET /v1/config/history", and nothing in the repo suggested they were abandoned. Removing them would have meant editing the changelog, the Revert prose, RevealSecretRequest/Result, ConfigHistoryEntry, and the Identifiers section's `cvh_` reservation — a much higher bar than building routes the contract already fully specifies. What shipped: - migrations/004_config_history.sql — the append-only config.config_history table `listConfigHistory` reads and every set/unset/lock/unlock (PUT /v1/config) + reveal (POST /v1/config/secrets/reveal) writes. - repositories/history.repository.ts — PgHistoryRepository (append + keyset-paginated listPage), mirroring the existing repository conventions. - routes/config-read.routes.ts — GET /v1/config/history, gated on a NEW 'audit' action (distinct from 'read'), same authz-before-existence-check discipline as GET /v1/config. - routes/secrets.write.ts — POST /v1/config/secrets/reveal. Security weight, per CLAUDE.md "an id is never a capability": authorization is checked via checkAuthorization() against a NEW 'reveal' action, decided independently of read/write by the Security API — never derived from the caller having merely supplied a valid namespace/scope/key. Fail-closed throughout (403 on deny/undecidable, matching the existing checkAuthorization() discipline), with a real in-process sliding-window rate limiter (429 RATE_LIMITED, documented per-pod limitation) and a `reveal` history entry written for every attempt against a resolved secret, success or not. Encryption-at-rest is explicitly OUT OF SCOPE and named as such in the module doc: isSecret values were already stored as plaintext JSONB before this PR (S6/FFRNT-158's PUT /v1/config), and adding real encryption needs a key-management decision this change does not make. `decryptSecretValue()` is the documented seam a future change hangs off; the 409 SECRET_UNAVAILABLE path is wired but not yet reachable. - config.write.ts — PUT /v1/config now writes a history entry per applied op, in the SAME transaction as the value change (a rollback drops the history entry too). isSecret keys are redacted (oldValue/newValue always null) regardless of what was passed in. - registry.ts / registry.py / gate_identifier.py — registered the `cvh_` TypeID prefix (configHistory) the contract's Identifiers section already reserved, kept in parity across the TS/Python identity packages. Incidental fixes needed to get a working, verifiable baseline (found while building this, unrelated to the two missing routes, scoped narrowly): - packages/identity/dist/{registry.js,registry.d.ts} were stale relative to their own src (missing `namespace`/`keyDefinition`, added in #634 but never rebuilt) — rebuilt via `tsc`, which also picks up `configHistory`. - src/middleware/authz.ts's makeNoOpProxy() (+ its test-file mocks) didn't implement AuthzClient's grant/revoke/listGrants, added since @fuzefront/auth grew them — stubbed as throwing no-ops (config-service never calls them). - tests/helpers/fakeDb.ts matched unqualified table names (`config_namespaces`) against the real, schema-qualified SQL (`config.config_namespaces`), so every INSERT/SELECT it should have handled instead threw inside an un-awaited-for-errors async route handler — an unhandled rejection that hung the request (and the whole test run) until timeout rather than failing fast. Fixed the substring matchers and extended the fake to also handle config_history. VERIFIED: python scripts/gate_openapi_conformance.py --repo . now reports config-service OK (spec=9 code=9). Full config-service jest suite (21 suites / 297 tests, including new ones for the two endpoints) green; tsc --noEmit clean; gate_identifier.py --all OK. Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc Co-authored-by: Claude <noreply@anthropic.com>
Root cause: @fuzefront/shared's package.json declares an
exportsmap withexactly two entries,
.and./kafka. That map makes every undeclaredsubpath unreachable under Node's package-exports resolution -
@fuzefront/shared/dist/kafka, which billing-service imported in six sourcefiles (plus one test), is not a valid specifier for this package and should
raise ERR_PACKAGE_PATH_NOT_EXPORTED.
It only appeared to work because billing-service's production Dockerfile
copied
shared/distinto the image withoutshared/package.json. With nopackage.json present at that path, Node has no
exportsfield to enforce,so the deep path resolved by plain filesystem lookup instead - an accident
of packaging, not a valid resolution. applications-service hit the real
failure mode in #751: it copies both
shared/distandshared/package.json,so the same deep import crash-looped it in prod (70 restarts) until fixed.
Adding the missing COPY line to billing-service, on its own, would have
reproduced that crash rather than fixed anything - the imports had to change
first.
Fix:
use the declared ./kafka subpath (src/index.ts, kafka/producer.ts,
kafka/consumer.ts, kafka/ref-index.consumer.ts, kafka/org-deleted.consumer.ts,
kafka/org-deleted.handler.ts, tests/kafka/org-deleted.handler.test.ts - two
more source files and the one test than the issue's four call sites, found
by grepping the whole service rather than trusting the listed set).
pathsentry for @fuzefront/shared/kafka(baseUrl "." + relative path to shared/dist/kafka/index.d.ts), matching the
existing convention in backend/tsconfig.json and backend/security/tsconfig.json
for the same typed-import case.
shared/dist, the same pairing applications-service's Dockerfile already
uses (backend/applications/Dockerfile:86-87). Without the manifest, the
correct subpath import would 404 in the image even though it type-checks
and resolves locally - the exports map has to travel with the dist output
for the supported entry point to actually be reachable at runtime.
backend/applications/tests/sharedSubpathExports.test.ts, so that guard now
covers it as the issue asked.
Verified statically (no npm install / no image build - disk-constrained
worktree): re-implemented the guard test's file-scan logic against the
working tree and confirmed it finds 42 @fuzefront/shared import sites
repo-wide with zero offenders now that billing-service is migrated;
confirmed no
shared/dist/kafkaimport specifier remains anywhere in therepo (only paths/mapper targets, which correctly point at the physical
dist file); confirmed the edited tsconfig.json and jest.config.js both
parse; confirmed billing-service names no bare
@fuzefront/sharedimportthat would additionally depend on the package's "main"/root export.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc