From 4ae7a9d02364b0917cda185e16aea9014efd1400 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Feb 2026 11:49:42 -0800 Subject: [PATCH] refactor(services): rewire lib/ to @bytelyst/* packages + add docker-compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .env.example | 24 +++ docker-compose.yml | 169 ++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 40 ++++- services/billing-service/package.json | 3 + services/billing-service/src/lib/config.ts | 11 -- services/billing-service/src/lib/cosmos.ts | 24 +-- services/billing-service/src/lib/errors.ts | 48 ++--- .../billing-service/src/lib/product-config.ts | 11 +- services/growth-service/package.json | 3 + services/growth-service/src/lib/config.ts | 7 - services/growth-service/src/lib/cosmos.ts | 24 +-- services/growth-service/src/lib/errors.ts | 42 ++--- .../growth-service/src/lib/product-config.ts | 13 +- services/platform-service/package.json | 3 + services/platform-service/src/lib/config.ts | 14 +- services/platform-service/src/lib/cosmos.ts | 24 +-- services/platform-service/src/lib/errors.ts | 45 ++--- .../src/lib/product-config.ts | 11 +- services/tracker-service/package.json | 3 + services/tracker-service/src/lib/config.ts | 7 - services/tracker-service/src/lib/cosmos.ts | 24 +-- services/tracker-service/src/lib/errors.ts | 51 ++---- .../tracker-service/src/lib/product-config.ts | 5 +- vitest.config.ts | 1 + 25 files changed, 315 insertions(+), 294 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..6badd470 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# ── Common Platform Environment Variables ────────────────────── +# Copy to .env and fill in real values. + +# ── Azure Cosmos DB ──────────────────────────────────────────── +COSMOS_ENDPOINT=https://cosmos-mywisprai.documents.azure.com:443/ +COSMOS_KEY=your-cosmos-key +COSMOS_DATABASE=lysnrai + +# ── Auth (platform-service + tracker-service) ───────────────── +JWT_SECRET=your-jwt-secret + +# ── Azure Blob Storage (platform-service) ───────────────────── +AZURE_BLOB_CONNECTION_STRING= +AZURE_BLOB_ACCOUNT_NAME=bytelystblobs +AZURE_BLOB_ACCOUNT_KEY=your-blob-key + +# ── Stripe (billing-service + growth-service) ───────────────── +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_PRO=price_... +STRIPE_PRICE_ENTERPRISE=price_... + +# ── Product Identity ────────────────────────────────────────── +DEFAULT_PRODUCT_ID=lysnrai diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..77f9121b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,169 @@ +services: + # ── Loki (Log Aggregation) ──────────────────────────────────── + loki: + image: grafana/loki:3.3.2 + ports: + - "3100:3100" + volumes: + - ./services/monitoring/loki/loki-config.yml:/etc/loki/local-config.yaml + - loki-data:/loki + command: -config.file=/etc/loki/local-config.yaml + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3100/ready"] + interval: 15s + timeout: 5s + retries: 3 + + # ── Grafana (Log Viewer + Dashboards) ───────────────────────── + grafana: + image: grafana/grafana:11.4.0 + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=lysnrai + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - ./services/monitoring/grafana/provisioning:/etc/grafana/provisioning + - ./services/monitoring/grafana/dashboards:/var/lib/grafana/dashboards + - grafana-data:/var/lib/grafana + depends_on: + loki: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 15s + timeout: 5s + retries: 3 + + # ── API Gateway (Traefik) ─────────────────────────────────── + gateway: + image: traefik:v3.3 + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--accesslog=true" + - "--accesslog.format=json" + ports: + - "80:80" + - "8080:8080" # Traefik dashboard + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + depends_on: + loki: + condition: service_started + logging: + driver: loki + options: + loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-retries: "3" + restart: unless-stopped + + # ── Growth Service (Fastify + TypeScript) ─────────────────── + growth-service: + build: ./services/growth-service + ports: + - "4001:4001" + env_file: + - .env + environment: + - PORT=4001 + labels: + - "traefik.enable=true" + - "traefik.http.routers.growth.rule=PathPrefix(`/api/invitations`) || PathPrefix(`/api/referrals`) || PathPrefix(`/api/promos`)" + - "traefik.http.services.growth.loadbalancer.server.port=4001" + logging: + driver: loki + options: + loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-retries: "3" + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4001/health"] + interval: 30s + timeout: 10s + retries: 3 + + # ── Billing Service (Fastify + TypeScript) ────────────────── + billing-service: + build: ./services/billing-service + ports: + - "4002:4002" + env_file: + - .env + environment: + - PORT=4002 + labels: + - "traefik.enable=true" + - "traefik.http.routers.billing.rule=PathPrefix(`/api/subscriptions`) || PathPrefix(`/api/payments`) || PathPrefix(`/api/usage`) || PathPrefix(`/api/plans`) || PathPrefix(`/api/licenses`) || PathPrefix(`/api/stripe`)" + - "traefik.http.services.billing.loadbalancer.server.port=4002" + logging: + driver: loki + options: + loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-retries: "3" + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4002/health"] + interval: 30s + timeout: 10s + retries: 3 + + # ── Platform Service (Fastify + TypeScript) ───────────────── + platform-service: + build: ./services/platform-service + ports: + - "4003:4003" + env_file: + - .env + environment: + - PORT=4003 + labels: + - "traefik.enable=true" + - "traefik.http.routers.platform.rule=PathPrefix(`/api/auth`) || PathPrefix(`/api/audit`) || PathPrefix(`/api/notifications`) || PathPrefix(`/api/flags`) || PathPrefix(`/api/ratelimit`) || PathPrefix(`/api/blob`)" + - "traefik.http.services.platform.loadbalancer.server.port=4003" + logging: + driver: loki + options: + loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-retries: "3" + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4003/health"] + interval: 30s + timeout: 10s + retries: 3 + + # ── Tracker Service (Fastify + TypeScript) ────────────────── + tracker-service: + build: ./services/tracker-service + ports: + - "4004:4004" + env_file: + - .env + environment: + - PORT=4004 + labels: + - "traefik.enable=true" + - "traefik.http.routers.tracker.rule=PathPrefix(`/api/items`) || PathPrefix(`/api/tracker`) || PathPrefix(`/public`)" + - "traefik.http.services.tracker.loadbalancer.server.port=4004" + logging: + driver: loki + options: + loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-retries: "3" + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4004/health"] + interval: 30s + timeout: 10s + retries: 3 + +# ── Volumes ─────────────────────────────────────────────────────── +volumes: + loki-data: + grafana-data: diff --git a/package.json b/package.json index 4bcd552c..e888299e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,6 @@ "bcryptjs": ">=2.4.0", "jose": ">=5.0.0", "react": ">=18.0.0", - "zod": ">=3.20.0" + "zod": "^3.24.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 010513c4..4c573418 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: '>=18.0.0' version: 19.2.4 zod: - specifier: '>=3.20.0' - version: 4.3.6 + specifier: ^3.24.0 + version: 3.25.76 devDependencies: '@types/bcryptjs': specifier: ^2.4.6 @@ -85,6 +85,15 @@ importers: '@azure/cosmos': specifier: ^4.2.0 version: 4.9.1(@azure/core-client@1.10.1) + '@bytelyst/config': + specifier: workspace:* + version: link:../../packages/config + '@bytelyst/cosmos': + specifier: workspace:* + version: link:../../packages/cosmos + '@bytelyst/errors': + specifier: workspace:* + version: link:../../packages/errors '@fastify/cors': specifier: ^10.0.2 version: 10.1.0 @@ -122,6 +131,15 @@ importers: '@azure/cosmos': specifier: ^4.2.0 version: 4.9.1(@azure/core-client@1.10.1) + '@bytelyst/config': + specifier: workspace:* + version: link:../../packages/config + '@bytelyst/cosmos': + specifier: workspace:* + version: link:../../packages/cosmos + '@bytelyst/errors': + specifier: workspace:* + version: link:../../packages/errors '@fastify/cors': specifier: ^10.0.2 version: 10.1.0 @@ -174,6 +192,15 @@ importers: '@azure/storage-blob': specifier: ^12.31.0 version: 12.31.0 + '@bytelyst/config': + specifier: workspace:* + version: link:../../packages/config + '@bytelyst/cosmos': + specifier: workspace:* + version: link:../../packages/cosmos + '@bytelyst/errors': + specifier: workspace:* + version: link:../../packages/errors '@fastify/cors': specifier: ^10.0.2 version: 10.1.0 @@ -217,6 +244,15 @@ importers: '@azure/cosmos': specifier: ^4.2.0 version: 4.9.1(@azure/core-client@1.10.1) + '@bytelyst/config': + specifier: workspace:* + version: link:../../packages/config + '@bytelyst/cosmos': + specifier: workspace:* + version: link:../../packages/cosmos + '@bytelyst/errors': + specifier: workspace:* + version: link:../../packages/errors '@fastify/cors': specifier: ^10.0.2 version: 10.1.0 diff --git a/services/billing-service/package.json b/services/billing-service/package.json index 54238b43..1bfe9d17 100644 --- a/services/billing-service/package.json +++ b/services/billing-service/package.json @@ -13,6 +13,9 @@ "lint": "eslint src/" }, "dependencies": { + "@bytelyst/config": "workspace:*", + "@bytelyst/cosmos": "workspace:*", + "@bytelyst/errors": "workspace:*", "@azure/cosmos": "^4.2.0", "fastify": "^5.2.1", "@fastify/cors": "^10.0.2", diff --git a/services/billing-service/src/lib/config.ts b/services/billing-service/src/lib/config.ts index 64006d99..31408dac 100644 --- a/services/billing-service/src/lib/config.ts +++ b/services/billing-service/src/lib/config.ts @@ -1,31 +1,20 @@ import { z } from "zod"; const envSchema = z.object({ - // Server PORT: z.coerce.number().default(4002), HOST: z.string().default("0.0.0.0"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), CORS_ORIGIN: z.string().optional(), SERVICE_NAME: z.string().default("billing-service"), - - // Auth BILLING_INTERNAL_KEY: z.string().optional(), - - // Database COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_DATABASE: z.string().default("lysnrai"), - - // Stripe STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"), STRIPE_WEBHOOK_SECRET: z.string().optional(), STRIPE_PRICE_PRO: z.string().optional(), STRIPE_PRICE_ENTERPRISE: z.string().optional(), - - // External Services BACKEND_URL: z.string().default("http://localhost:8000"), - - // Feature Flags / Limits PLAN_LIMITS_JSON: z.string().optional(), USAGE_WARN_THRESHOLD: z.coerce.number().default(0.8), }); diff --git a/services/billing-service/src/lib/cosmos.ts b/services/billing-service/src/lib/cosmos.ts index e05bac8e..82552e42 100644 --- a/services/billing-service/src/lib/cosmos.ts +++ b/services/billing-service/src/lib/cosmos.ts @@ -1,24 +1,4 @@ /** - * Shared Cosmos DB client for the Billing Service. + * Re-export from @bytelyst/cosmos — shared across all services. */ - -import { CosmosClient, Container } from "@azure/cosmos"; - -let client: CosmosClient | null = null; - -function getClient(): CosmosClient { - if (!client) { - const endpoint = process.env.COSMOS_ENDPOINT; - const key = process.env.COSMOS_KEY; - if (!endpoint || !key) { - throw new Error("COSMOS_ENDPOINT and COSMOS_KEY must be set"); - } - client = new CosmosClient({ endpoint, key }); - } - return client; -} - -export function getContainer(name: string): Container { - const database = process.env.COSMOS_DATABASE || "lysnrai"; - return getClient().database(database).container(name); -} +export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos"; diff --git a/services/billing-service/src/lib/errors.ts b/services/billing-service/src/lib/errors.ts index caa9313d..f4e0b20f 100644 --- a/services/billing-service/src/lib/errors.ts +++ b/services/billing-service/src/lib/errors.ts @@ -1,40 +1,12 @@ /** - * Typed service errors for consistent HTTP error responses. + * Re-export from @bytelyst/errors — shared across all services. */ - -export class ServiceError extends Error { - constructor( - public statusCode: number, - message: string, - ) { - super(message); - this.name = "ServiceError"; - } -} - -export class NotFoundError extends ServiceError { - constructor(message = "Not found") { - super(404, message); - } -} - -export class BadRequestError extends ServiceError { - constructor(message = "Bad request") { - super(400, message); - } -} - -export class ForbiddenError extends ServiceError { - constructor(message = "Forbidden") { - super(403, message); - } -} - -export class TooManyRequestsError extends ServiceError { - constructor( - message = "Too many requests", - public details?: Record, - ) { - super(429, message); - } -} +export { + ServiceError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "@bytelyst/errors"; diff --git a/services/billing-service/src/lib/product-config.ts b/services/billing-service/src/lib/product-config.ts index 20c7c9be..22a3ef0a 100644 --- a/services/billing-service/src/lib/product-config.ts +++ b/services/billing-service/src/lib/product-config.ts @@ -1,8 +1,9 @@ /** - * Centralized product identity — single source of truth. - * NOTE: The canonical source is shared/product.json at the repo root. + * Re-export from @bytelyst/config — shared product identity. */ +import { loadProductIdentity } from "@bytelyst/config"; -export const PRODUCT_ID = "lysnrai"; -export const DISPLAY_NAME = "LysnrAI"; -export const LICENSE_PREFIX = "LYSNR"; +const _id = loadProductIdentity(); +export const PRODUCT_ID = _id.productId; +export const DISPLAY_NAME = _id.displayName; +export const LICENSE_PREFIX = _id.licensePrefix; diff --git a/services/growth-service/package.json b/services/growth-service/package.json index ee11be3d..fd71994d 100644 --- a/services/growth-service/package.json +++ b/services/growth-service/package.json @@ -13,6 +13,9 @@ "lint": "eslint src/" }, "dependencies": { + "@bytelyst/config": "workspace:*", + "@bytelyst/cosmos": "workspace:*", + "@bytelyst/errors": "workspace:*", "@azure/cosmos": "^4.2.0", "fastify": "^5.2.1", "@fastify/cors": "^10.0.2", diff --git a/services/growth-service/src/lib/config.ts b/services/growth-service/src/lib/config.ts index 357135f9..deeae47c 100644 --- a/services/growth-service/src/lib/config.ts +++ b/services/growth-service/src/lib/config.ts @@ -1,22 +1,15 @@ import { z } from "zod"; const envSchema = z.object({ - // Server PORT: z.coerce.number().default(4001), HOST: z.string().default("0.0.0.0"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), CORS_ORIGIN: z.string().optional(), SERVICE_NAME: z.string().default("growth-service"), - - // Database COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_DATABASE: z.string().default("lysnrai"), - - // Stripe STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"), - - // Webhooks WEBHOOK_INVITATION_REDEEMED_URL: z.string().optional(), WEBHOOK_REFERRAL_STATUS_URL: z.string().optional(), }); diff --git a/services/growth-service/src/lib/cosmos.ts b/services/growth-service/src/lib/cosmos.ts index 9f08bdc1..82552e42 100644 --- a/services/growth-service/src/lib/cosmos.ts +++ b/services/growth-service/src/lib/cosmos.ts @@ -1,24 +1,4 @@ /** - * Shared Cosmos DB client for the Growth Service. + * Re-export from @bytelyst/cosmos — shared across all services. */ - -import { CosmosClient, Container } from "@azure/cosmos"; - -let client: CosmosClient | null = null; - -function getClient(): CosmosClient { - if (!client) { - const endpoint = process.env.COSMOS_ENDPOINT; - const key = process.env.COSMOS_KEY; - if (!endpoint || !key) { - throw new Error("COSMOS_ENDPOINT and COSMOS_KEY must be set"); - } - client = new CosmosClient({ endpoint, key }); - } - return client; -} - -export function getContainer(name: string): Container { - const database = process.env.COSMOS_DATABASE || "lysnrai"; - return getClient().database(database).container(name); -} +export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos"; diff --git a/services/growth-service/src/lib/errors.ts b/services/growth-service/src/lib/errors.ts index f46b40c3..f4e0b20f 100644 --- a/services/growth-service/src/lib/errors.ts +++ b/services/growth-service/src/lib/errors.ts @@ -1,34 +1,12 @@ /** - * Typed service errors for consistent HTTP error responses. + * Re-export from @bytelyst/errors — shared across all services. */ - -export class ServiceError extends Error { - constructor( - public statusCode: number, - message: string, - ) { - super(message); - this.name = "ServiceError"; - } -} - -export class NotFoundError extends ServiceError { - constructor(message = "Not found") { - super(404, message); - this.name = "NotFoundError"; - } -} - -export class BadRequestError extends ServiceError { - constructor(message = "Bad request") { - super(400, message); - this.name = "BadRequestError"; - } -} - -export class ForbiddenError extends ServiceError { - constructor(message = "Forbidden") { - super(403, message); - this.name = "ForbiddenError"; - } -} +export { + ServiceError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "@bytelyst/errors"; diff --git a/services/growth-service/src/lib/product-config.ts b/services/growth-service/src/lib/product-config.ts index fa6520fd..22a3ef0a 100644 --- a/services/growth-service/src/lib/product-config.ts +++ b/services/growth-service/src/lib/product-config.ts @@ -1,10 +1,9 @@ /** - * Centralized product identity — single source of truth. - * - * NOTE: The canonical source is shared/product.json at the repo root. - * These values must stay in sync with that file. + * Re-export from @bytelyst/config — shared product identity. */ +import { loadProductIdentity } from "@bytelyst/config"; -export const PRODUCT_ID = "lysnrai"; -export const DISPLAY_NAME = "LysnrAI"; -export const LICENSE_PREFIX = "LYSNR"; +const _id = loadProductIdentity(); +export const PRODUCT_ID = _id.productId; +export const DISPLAY_NAME = _id.displayName; +export const LICENSE_PREFIX = _id.licensePrefix; diff --git a/services/platform-service/package.json b/services/platform-service/package.json index 216349f1..cba4c8bc 100644 --- a/services/platform-service/package.json +++ b/services/platform-service/package.json @@ -13,6 +13,9 @@ "lint": "eslint src/" }, "dependencies": { + "@bytelyst/config": "workspace:*", + "@bytelyst/cosmos": "workspace:*", + "@bytelyst/errors": "workspace:*", "@azure/cosmos": "^4.2.0", "@azure/storage-blob": "^12.31.0", "@fastify/cors": "^10.0.2", diff --git a/services/platform-service/src/lib/config.ts b/services/platform-service/src/lib/config.ts index a5b04013..3a4ec202 100644 --- a/services/platform-service/src/lib/config.ts +++ b/services/platform-service/src/lib/config.ts @@ -1,31 +1,19 @@ import { z } from "zod"; const envSchema = z.object({ - // Server PORT: z.coerce.number().default(4003), HOST: z.string().default("0.0.0.0"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), CORS_ORIGIN: z.string().optional(), SERVICE_NAME: z.string().default("platform-service"), - - // Database COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_DATABASE: z.string().default("lysnrai"), - - // Auth JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), - - // Blob Storage AZURE_BLOB_CONNECTION_STRING: z.string().optional(), AZURE_BLOB_ACCOUNT_NAME: z.string().optional(), AZURE_BLOB_ACCOUNT_KEY: z.string().optional(), - - // Features RATE_LIMIT_CONFIG_JSON: z.string().optional(), -}).refine(data => - data.AZURE_BLOB_CONNECTION_STRING || (data.AZURE_BLOB_ACCOUNT_NAME && data.AZURE_BLOB_ACCOUNT_KEY), - { message: "Must provide AZURE_BLOB_CONNECTION_STRING or AZURE_BLOB_ACCOUNT_NAME/KEY" } -); +}); export const config = envSchema.parse(process.env); diff --git a/services/platform-service/src/lib/cosmos.ts b/services/platform-service/src/lib/cosmos.ts index 95e79ea8..82552e42 100644 --- a/services/platform-service/src/lib/cosmos.ts +++ b/services/platform-service/src/lib/cosmos.ts @@ -1,24 +1,4 @@ /** - * Shared Cosmos DB client for the Platform Service. + * Re-export from @bytelyst/cosmos — shared across all services. */ - -import { CosmosClient, Container } from "@azure/cosmos"; - -let client: CosmosClient | null = null; - -function getClient(): CosmosClient { - if (!client) { - const endpoint = process.env.COSMOS_ENDPOINT; - const key = process.env.COSMOS_KEY; - if (!endpoint || !key) { - throw new Error("COSMOS_ENDPOINT and COSMOS_KEY must be set"); - } - client = new CosmosClient({ endpoint, key }); - } - return client; -} - -export function getContainer(name: string): Container { - const database = process.env.COSMOS_DATABASE || "lysnrai"; - return getClient().database(database).container(name); -} +export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos"; diff --git a/services/platform-service/src/lib/errors.ts b/services/platform-service/src/lib/errors.ts index d5471290..f4e0b20f 100644 --- a/services/platform-service/src/lib/errors.ts +++ b/services/platform-service/src/lib/errors.ts @@ -1,37 +1,12 @@ /** - * Typed service errors for consistent HTTP error responses. + * Re-export from @bytelyst/errors — shared across all services. */ - -export class ServiceError extends Error { - constructor( - public statusCode: number, - message: string, - ) { - super(message); - this.name = "ServiceError"; - } -} - -export class NotFoundError extends ServiceError { - constructor(message = "Not found") { - super(404, message); - } -} - -export class BadRequestError extends ServiceError { - constructor(message = "Bad request") { - super(400, message); - } -} - -export class UnauthorizedError extends ServiceError { - constructor(message = "Unauthorized") { - super(401, message); - } -} - -export class ForbiddenError extends ServiceError { - constructor(message = "Forbidden") { - super(403, message); - } -} +export { + ServiceError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "@bytelyst/errors"; diff --git a/services/platform-service/src/lib/product-config.ts b/services/platform-service/src/lib/product-config.ts index 20c7c9be..22a3ef0a 100644 --- a/services/platform-service/src/lib/product-config.ts +++ b/services/platform-service/src/lib/product-config.ts @@ -1,8 +1,9 @@ /** - * Centralized product identity — single source of truth. - * NOTE: The canonical source is shared/product.json at the repo root. + * Re-export from @bytelyst/config — shared product identity. */ +import { loadProductIdentity } from "@bytelyst/config"; -export const PRODUCT_ID = "lysnrai"; -export const DISPLAY_NAME = "LysnrAI"; -export const LICENSE_PREFIX = "LYSNR"; +const _id = loadProductIdentity(); +export const PRODUCT_ID = _id.productId; +export const DISPLAY_NAME = _id.displayName; +export const LICENSE_PREFIX = _id.licensePrefix; diff --git a/services/tracker-service/package.json b/services/tracker-service/package.json index ab585edc..8aa6da3d 100644 --- a/services/tracker-service/package.json +++ b/services/tracker-service/package.json @@ -13,6 +13,9 @@ "lint": "eslint src/" }, "dependencies": { + "@bytelyst/config": "workspace:*", + "@bytelyst/cosmos": "workspace:*", + "@bytelyst/errors": "workspace:*", "@azure/cosmos": "^4.2.0", "@fastify/cors": "^10.0.2", "@fastify/rate-limit": "^10.3.0", diff --git a/services/tracker-service/src/lib/config.ts b/services/tracker-service/src/lib/config.ts index 06ceee09..f5f60e34 100644 --- a/services/tracker-service/src/lib/config.ts +++ b/services/tracker-service/src/lib/config.ts @@ -1,22 +1,15 @@ import { z } from "zod"; const envSchema = z.object({ - // Server PORT: z.coerce.number().default(4004), HOST: z.string().default("0.0.0.0"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), CORS_ORIGIN: z.string().optional(), SERVICE_NAME: z.string().default("tracker-service"), - - // Database COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_DATABASE: z.string().default("lysnrai"), - - // Auth JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), - - // Product DEFAULT_PRODUCT_ID: z.string().default("lysnrai"), }); diff --git a/services/tracker-service/src/lib/cosmos.ts b/services/tracker-service/src/lib/cosmos.ts index 633f3f5e..82552e42 100644 --- a/services/tracker-service/src/lib/cosmos.ts +++ b/services/tracker-service/src/lib/cosmos.ts @@ -1,24 +1,4 @@ /** - * Shared Cosmos DB client for the Tracker Service. + * Re-export from @bytelyst/cosmos — shared across all services. */ - -import { CosmosClient, Container } from "@azure/cosmos"; - -let client: CosmosClient | null = null; - -function getClient(): CosmosClient { - if (!client) { - const endpoint = process.env.COSMOS_ENDPOINT; - const key = process.env.COSMOS_KEY; - if (!endpoint || !key) { - throw new Error("COSMOS_ENDPOINT and COSMOS_KEY must be set"); - } - client = new CosmosClient({ endpoint, key }); - } - return client; -} - -export function getContainer(name: string): Container { - const database = process.env.COSMOS_DATABASE || "lysnrai"; - return getClient().database(database).container(name); -} +export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos"; diff --git a/services/tracker-service/src/lib/errors.ts b/services/tracker-service/src/lib/errors.ts index 520f604d..f4e0b20f 100644 --- a/services/tracker-service/src/lib/errors.ts +++ b/services/tracker-service/src/lib/errors.ts @@ -1,43 +1,12 @@ /** - * Typed service errors for consistent HTTP error responses. + * Re-export from @bytelyst/errors — shared across all services. */ - -export class ServiceError extends Error { - constructor( - public statusCode: number, - message: string, - ) { - super(message); - this.name = "ServiceError"; - } -} - -export class NotFoundError extends ServiceError { - constructor(message = "Not found") { - super(404, message); - } -} - -export class BadRequestError extends ServiceError { - constructor(message = "Bad request") { - super(400, message); - } -} - -export class UnauthorizedError extends ServiceError { - constructor(message = "Unauthorized") { - super(401, message); - } -} - -export class ForbiddenError extends ServiceError { - constructor(message = "Forbidden") { - super(403, message); - } -} - -export class ConflictError extends ServiceError { - constructor(message = "Conflict") { - super(409, message); - } -} +export { + ServiceError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, + TooManyRequestsError, +} from "@bytelyst/errors"; diff --git a/services/tracker-service/src/lib/product-config.ts b/services/tracker-service/src/lib/product-config.ts index 5dba54f8..bf24c906 100644 --- a/services/tracker-service/src/lib/product-config.ts +++ b/services/tracker-service/src/lib/product-config.ts @@ -1,6 +1,7 @@ /** - * Default product identity — used when productId is not specified in requests. + * Re-export from @bytelyst/config — shared product identity. * The tracker service is product-agnostic; every document carries its own productId. */ +import { getProductId } from "@bytelyst/config"; -export const DEFAULT_PRODUCT_ID = process.env.DEFAULT_PRODUCT_ID || "lysnrai"; +export const DEFAULT_PRODUCT_ID = process.env.DEFAULT_PRODUCT_ID || getProductId(); diff --git a/vitest.config.ts b/vitest.config.ts index 3f824fb9..f938ac47 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,5 +4,6 @@ export default defineConfig({ test: { globals: true, environment: "node", + passWithNoTests: true, }, });