- Added cross-product fallback lookup by stripeCustomerId when metadata lacks productId
- Ensure invoice payments are stored under the resolved subscription productId
- Normalize checkout metadata plan value before persistence/sync
- Keep auth plan sync aligned with resolved product context
- Verified: tsc --noEmit clean, 20 test files / 183 tests passing
- Make auth register provisioning truly best-effort (warn on failure, do not fail signup)
- Process Stripe webhook events for all products (remove non-default skip)
- Derive updated subscription plan from Stripe price IDs on subscription.updated
- Sync derived plan to auth users and backend plan sync endpoint
- Verified: tsc --noEmit clean, 20 test files / 183 tests passing
- Added plan to auth UserDoc model and token payload typing
- Register flow initializes user.plan from product default plan
- Login/Register/Me responses now include user.plan
- Access tokens now include optional plan claim
- Verified: tsc --noEmit clean, 19 test files / 178 tests passing
- LoginSchema and RegisterSchema now require productId field
- Login/Register routes use productId from request body (not env var)
- PRODUCT_ID import removed from auth/routes.ts
- Test fixtures updated with productId: 'lysnrai'
- createAccessToken() and createRefreshToken() now require productId parameter
- Issuer changed from PRODUCT_ID env var to generic 'bytelyst-platform'
- verifyToken() validates against 'bytelyst-platform' issuer
- auth/routes.ts callers updated to pass productId (still from PRODUCT_ID env var for now)
- Refresh endpoint reads productId from user doc
- Best-effort JWT parsing on every request (non-blocking for unauthenticated routes)
- Attaches parsed payload to req.jwtPayload for downstream use by getRequestProductId()
- Invalid/expired tokens silently ignored — auth-required routes handle their own validation
- New lib/request-context.ts with product validation against cache
- Priority: JWT payload > X-Product-Id header > env var fallback
- Rejects unknown or disabled products with 400 Bad Request
- Augments FastifyRequest with jwtPayload type declaration
- getRequestProductConfig() for modules needing product-specific values
- New products container in Cosmos DB (partition key: /id)
- ProductDoc: displayName, licensePrefix, deviceLimits, trialDays, status
- In-memory cache loaded on startup via loadProductCache()
- CRUD routes: GET/POST /products, GET/PUT /products/:id
- Cache refreshed after admin writes (create/update)
- Registered before all other modules in server.ts
- All 4 service Dockerfiles now use repo root as build context
- Multi-stage: pnpm install → build packages+service → pnpm deploy for production
- docker-compose.yml updated with context: . and dockerfile paths
- Added scripts/docker-prep.sh convenience wrapper
- Docker build blocked by corporate proxy SSL (not a code issue)
- Add proper workspace dependency resolution
- Build packages before service
- Use pnpm deploy for production
- Add docker-prep.sh script for helper commands
Rewired all 4 services:
- lib/errors.ts → re-exports from @bytelyst/errors
- lib/cosmos.ts → re-exports from @bytelyst/cosmos
- lib/product-config.ts → uses loadProductIdentity()/getProductId() from @bytelyst/config
- lib/config.ts → kept self-contained (zod v3/v4 type mismatch with loadConfig)
Added workspace deps (@bytelyst/errors, @bytelyst/cosmos, @bytelyst/config) to all 4 services.
Added docker-compose.yml with Loki, Grafana, Traefik, and all 4 services.
Added .env.example with required env vars.
Added passWithNoTests to vitest.config.ts.
Pinned root zod to ^3.24.0 to match service zod versions.
All 12 projects build. 175 tests passing.