From 30a9d65410928f538ec6b2c89e4d2b7d3f2e2e94 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 08:01:04 +0000 Subject: [PATCH] fix(billing-service): import @fuzefront/shared via its declared ./kafka 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 Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc --- .../tests/sharedSubpathExports.test.ts | 17 ++++++++--------- services/billing-service/Dockerfile | 10 ++++++++++ services/billing-service/jest.config.js | 4 +++- services/billing-service/src/index.ts | 2 +- services/billing-service/src/kafka/consumer.ts | 2 +- .../src/kafka/org-deleted.consumer.ts | 2 +- .../src/kafka/org-deleted.handler.ts | 2 +- services/billing-service/src/kafka/producer.ts | 2 +- .../src/kafka/ref-index.consumer.ts | 2 +- .../tests/kafka/org-deleted.handler.test.ts | 2 +- services/billing-service/tsconfig.json | 4 ++++ 11 files changed, 32 insertions(+), 17 deletions(-) diff --git a/backend/applications/tests/sharedSubpathExports.test.ts b/backend/applications/tests/sharedSubpathExports.test.ts index cecc6a7c3..bdff1c1a9 100644 --- a/backend/applications/tests/sharedSubpathExports.test.ts +++ b/backend/applications/tests/sharedSubpathExports.test.ts @@ -23,16 +23,15 @@ const REPO_ROOT = path.resolve(__dirname, '../../..') const SHARED_PKG = path.join(REPO_ROOT, 'shared', 'package.json') /** - * billing-service still imports `@fuzefront/shared/dist/kafka`. It does not - * crash only because its runtime image copies `shared/dist` WITHOUT - * `shared/package.json` (see services/billing-service/Dockerfile), so there is - * no `exports` map in the image to enforce — it works by accident, and adding - * that one COPY line would break it exactly as applications-service broke. - * Recorded here as a known exception rather than silently excluded; migrating - * it needs its tsconfig `paths` and jest moduleNameMapper updated too, which - * is deliberately not bundled into an incident fix for a different service. + * billing-service used to import `@fuzefront/shared/dist/kafka` and only + * avoided crashing because its runtime image copied `shared/dist` WITHOUT + * `shared/package.json`, so there was no `exports` map in the image to + * enforce — it worked by accident. Fixed in #753: the imports now name the + * supported `@fuzefront/shared/kafka` subpath, with tsconfig `paths`, jest + * `moduleNameMapper`, and the Dockerfile's `shared/package.json` COPY updated + * to match. No exception left to record here. */ -const KNOWN_UNMIGRATED = ['services/billing-service/'] +const KNOWN_UNMIGRATED: string[] = [] // Matches only real import syntax — `from '...'` / `require('...')` — not any // quoted occurrence. An earlier version matched backtick-delimited text too and diff --git a/services/billing-service/Dockerfile b/services/billing-service/Dockerfile index 6b57ffcf4..a641beb1c 100644 --- a/services/billing-service/Dockerfile +++ b/services/billing-service/Dockerfile @@ -61,6 +61,16 @@ RUN apk add --no-cache dumb-init && \ COPY --from=base /app/services/billing-service/node_modules ./services/billing-service/node_modules COPY --from=build /app/shared/dist ./shared/dist +# shared/package.json carries the `exports` map that makes `@fuzefront/shared/kafka` +# (the supported subpath — see src/index.ts et al.) resolvable at all: with no +# package.json here, Node has no `exports` to consult and a subpath import beyond +# the package root simply 404s (MODULE_NOT_FOUND), it does not "fall back" to +# anything. Omitting this file is NOT a safe shortcut — a prior version of this +# image omitted it, which happened to make an undeclared deep path +# (`@fuzefront/shared/dist/kafka`) resolve by accident (no exports map to enforce +# against). That was the bug fixed here (issue #753); this COPY is what makes the +# *correct* subpath import work in the image the same way it works locally. +COPY --from=build /app/shared/package.json ./shared/package.json # shared/dist/kafka/* does require('zod') + kafkajs at runtime, resolved by walking # UP from the file → it reaches /app/node_modules. The service deps (which include # zod + kafkajs) only sat in the sibling services/billing-service/node_modules, diff --git a/services/billing-service/jest.config.js b/services/billing-service/jest.config.js index e26cc80cc..00251e6c5 100644 --- a/services/billing-service/jest.config.js +++ b/services/billing-service/jest.config.js @@ -12,8 +12,10 @@ module.exports = { // Pointing to shared/src/index.ts would pull in AppContext.tsx (JSX), which ts-jest // cannot compile without --jsx. Since billing-service only ever imports from the kafka // sub-tree of shared, the narrower mapping is intentional — not a partial-import trap. + // The mapper key names the SUPPORTED subpath (@fuzefront/shared/kafka, matching the + // package's declared `exports`), not the dist/ internal it used to point at. moduleNameMapper: { - '^@fuzefront/shared/dist/kafka$': '/../../shared/src/kafka/index.ts', + '^@fuzefront/shared/kafka$': '/../../shared/src/kafka/index.ts', '^@izzywdev/fuzefront-identity$': '/../../packages/identity/src/index.ts', '^@fuzefront/shared$': '/../../shared/src/kafka/index.ts', }, diff --git a/services/billing-service/src/index.ts b/services/billing-service/src/index.ts index bb03b3c26..f649672c8 100644 --- a/services/billing-service/src/index.ts +++ b/services/billing-service/src/index.ts @@ -3,7 +3,7 @@ import { createKafkaClient, TypedProducer, TypedConsumer, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { loadConfig } from './config'; import { createApp, AppDeps } from './app'; import { createPool, runMigrations } from './db'; diff --git a/services/billing-service/src/kafka/consumer.ts b/services/billing-service/src/kafka/consumer.ts index 681745f42..b447f5cf3 100644 --- a/services/billing-service/src/kafka/consumer.ts +++ b/services/billing-service/src/kafka/consumer.ts @@ -5,7 +5,7 @@ import { billingUsageRecordedSchemaV1, BillingUsageRecordedPayloadV1, FuzeEvent, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { MeteringService } from '../services/metering.service'; /** diff --git a/services/billing-service/src/kafka/org-deleted.consumer.ts b/services/billing-service/src/kafka/org-deleted.consumer.ts index 3fb27c841..3cdb532e4 100644 --- a/services/billing-service/src/kafka/org-deleted.consumer.ts +++ b/services/billing-service/src/kafka/org-deleted.consumer.ts @@ -5,7 +5,7 @@ import { identityOrgDeletedSchemaV1, IdentityOrgDeletedPayloadV1, FuzeEvent, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { handleOrgDeleted, OrgDeletedHandlerDeps, diff --git a/services/billing-service/src/kafka/org-deleted.handler.ts b/services/billing-service/src/kafka/org-deleted.handler.ts index 1c49167a3..5d8c934cf 100644 --- a/services/billing-service/src/kafka/org-deleted.handler.ts +++ b/services/billing-service/src/kafka/org-deleted.handler.ts @@ -1,7 +1,7 @@ import { FuzeEvent, IdentityOrgDeletedPayloadV1, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { CustomerRepository } from '../repositories/customer.repository'; import { SubscriptionRepository } from '../repositories/subscription.repository'; import { SubscriptionService } from '../services/subscription.service'; diff --git a/services/billing-service/src/kafka/producer.ts b/services/billing-service/src/kafka/producer.ts index d1ee9a7a4..d2642a90d 100644 --- a/services/billing-service/src/kafka/producer.ts +++ b/services/billing-service/src/kafka/producer.ts @@ -9,7 +9,7 @@ import { BillingTenantRegisteredPayloadV1, billingPaymentMethodUpdatedSchemaV1, BillingPaymentMethodUpdatedPayloadV1, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { randomUUID } from 'crypto'; /** diff --git a/services/billing-service/src/kafka/ref-index.consumer.ts b/services/billing-service/src/kafka/ref-index.consumer.ts index 525d3e8ab..010b45ab2 100644 --- a/services/billing-service/src/kafka/ref-index.consumer.ts +++ b/services/billing-service/src/kafka/ref-index.consumer.ts @@ -3,7 +3,7 @@ import { TypedConsumer, TypedProducer, FuzeEvent, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { applyEventToRefIndex, REF_INDEX_TOPICS, diff --git a/services/billing-service/tests/kafka/org-deleted.handler.test.ts b/services/billing-service/tests/kafka/org-deleted.handler.test.ts index a82561f3a..7591131f4 100644 --- a/services/billing-service/tests/kafka/org-deleted.handler.test.ts +++ b/services/billing-service/tests/kafka/org-deleted.handler.test.ts @@ -3,7 +3,7 @@ import { FuzeEvent, TOPICS, IdentityOrgDeletedPayloadV1, -} from '@fuzefront/shared/dist/kafka'; +} from '@fuzefront/shared/kafka'; import { BillingCustomer, BillingSubscription } from '../../src/types'; const ORG_ID = '33333333-3333-3333-3333-333333333333'; diff --git a/services/billing-service/tsconfig.json b/services/billing-service/tsconfig.json index f20af9ad3..15486938f 100644 --- a/services/billing-service/tsconfig.json +++ b/services/billing-service/tsconfig.json @@ -4,6 +4,10 @@ "module": "commonjs", "outDir": "./dist", "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@fuzefront/shared/kafka": ["../../shared/dist/kafka/index.d.ts"] + }, "strict": false, "noImplicitAny": false, "declaration": true,