refactor(services): rewire lib/ to @bytelyst/* packages + add docker-compose

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.
This commit is contained in:
saravanakumardb1 2026-02-12 11:49:42 -08:00
parent 772df7785b
commit 4ae7a9d023
25 changed files with 315 additions and 294 deletions

24
.env.example Normal file
View File

@ -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

169
docker-compose.yml Normal file
View File

@ -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:

View File

@ -21,6 +21,6 @@
"bcryptjs": ">=2.4.0", "bcryptjs": ">=2.4.0",
"jose": ">=5.0.0", "jose": ">=5.0.0",
"react": ">=18.0.0", "react": ">=18.0.0",
"zod": ">=3.20.0" "zod": "^3.24.0"
} }
} }

40
pnpm-lock.yaml generated
View File

@ -21,8 +21,8 @@ importers:
specifier: '>=18.0.0' specifier: '>=18.0.0'
version: 19.2.4 version: 19.2.4
zod: zod:
specifier: '>=3.20.0' specifier: ^3.24.0
version: 4.3.6 version: 3.25.76
devDependencies: devDependencies:
'@types/bcryptjs': '@types/bcryptjs':
specifier: ^2.4.6 specifier: ^2.4.6
@ -85,6 +85,15 @@ importers:
'@azure/cosmos': '@azure/cosmos':
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.9.1(@azure/core-client@1.10.1) 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': '@fastify/cors':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.1.0 version: 10.1.0
@ -122,6 +131,15 @@ importers:
'@azure/cosmos': '@azure/cosmos':
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.9.1(@azure/core-client@1.10.1) 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': '@fastify/cors':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.1.0 version: 10.1.0
@ -174,6 +192,15 @@ importers:
'@azure/storage-blob': '@azure/storage-blob':
specifier: ^12.31.0 specifier: ^12.31.0
version: 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': '@fastify/cors':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.1.0 version: 10.1.0
@ -217,6 +244,15 @@ importers:
'@azure/cosmos': '@azure/cosmos':
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.9.1(@azure/core-client@1.10.1) 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': '@fastify/cors':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.1.0 version: 10.1.0

View File

@ -13,6 +13,9 @@
"lint": "eslint src/" "lint": "eslint src/"
}, },
"dependencies": { "dependencies": {
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@azure/cosmos": "^4.2.0", "@azure/cosmos": "^4.2.0",
"fastify": "^5.2.1", "fastify": "^5.2.1",
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",

View File

@ -1,31 +1,20 @@
import { z } from "zod"; import { z } from "zod";
const envSchema = z.object({ const envSchema = z.object({
// Server
PORT: z.coerce.number().default(4002), PORT: z.coerce.number().default(4002),
HOST: z.string().default("0.0.0.0"), HOST: z.string().default("0.0.0.0"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
CORS_ORIGIN: z.string().optional(), CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string().default("billing-service"), SERVICE_NAME: z.string().default("billing-service"),
// Auth
BILLING_INTERNAL_KEY: z.string().optional(), BILLING_INTERNAL_KEY: z.string().optional(),
// Database
COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"),
COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"),
COSMOS_DATABASE: z.string().default("lysnrai"), COSMOS_DATABASE: z.string().default("lysnrai"),
// Stripe
STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"), STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),
STRIPE_WEBHOOK_SECRET: z.string().optional(), STRIPE_WEBHOOK_SECRET: z.string().optional(),
STRIPE_PRICE_PRO: z.string().optional(), STRIPE_PRICE_PRO: z.string().optional(),
STRIPE_PRICE_ENTERPRISE: z.string().optional(), STRIPE_PRICE_ENTERPRISE: z.string().optional(),
// External Services
BACKEND_URL: z.string().default("http://localhost:8000"), BACKEND_URL: z.string().default("http://localhost:8000"),
// Feature Flags / Limits
PLAN_LIMITS_JSON: z.string().optional(), PLAN_LIMITS_JSON: z.string().optional(),
USAGE_WARN_THRESHOLD: z.coerce.number().default(0.8), USAGE_WARN_THRESHOLD: z.coerce.number().default(0.8),
}); });

View File

@ -1,24 +1,4 @@
/** /**
* Shared Cosmos DB client for the Billing Service. * Re-export from @bytelyst/cosmos shared across all services.
*/ */
export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos";
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);
}

View File

@ -1,40 +1,12 @@
/** /**
* Typed service errors for consistent HTTP error responses. * Re-export from @bytelyst/errors shared across all services.
*/ */
export {
export class ServiceError extends Error { ServiceError,
constructor( BadRequestError,
public statusCode: number, UnauthorizedError,
message: string, ForbiddenError,
) { NotFoundError,
super(message); ConflictError,
this.name = "ServiceError"; TooManyRequestsError,
} } from "@bytelyst/errors";
}
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<string, unknown>,
) {
super(429, message);
}
}

View File

@ -1,8 +1,9 @@
/** /**
* Centralized product identity single source of truth. * Re-export from @bytelyst/config shared product identity.
* NOTE: The canonical source is shared/product.json at the repo root.
*/ */
import { loadProductIdentity } from "@bytelyst/config";
export const PRODUCT_ID = "lysnrai"; const _id = loadProductIdentity();
export const DISPLAY_NAME = "LysnrAI"; export const PRODUCT_ID = _id.productId;
export const LICENSE_PREFIX = "LYSNR"; export const DISPLAY_NAME = _id.displayName;
export const LICENSE_PREFIX = _id.licensePrefix;

View File

@ -13,6 +13,9 @@
"lint": "eslint src/" "lint": "eslint src/"
}, },
"dependencies": { "dependencies": {
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@azure/cosmos": "^4.2.0", "@azure/cosmos": "^4.2.0",
"fastify": "^5.2.1", "fastify": "^5.2.1",
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",

View File

@ -1,22 +1,15 @@
import { z } from "zod"; import { z } from "zod";
const envSchema = z.object({ const envSchema = z.object({
// Server
PORT: z.coerce.number().default(4001), PORT: z.coerce.number().default(4001),
HOST: z.string().default("0.0.0.0"), HOST: z.string().default("0.0.0.0"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
CORS_ORIGIN: z.string().optional(), CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string().default("growth-service"), SERVICE_NAME: z.string().default("growth-service"),
// Database
COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"),
COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"),
COSMOS_DATABASE: z.string().default("lysnrai"), COSMOS_DATABASE: z.string().default("lysnrai"),
// Stripe
STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"), STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),
// Webhooks
WEBHOOK_INVITATION_REDEEMED_URL: z.string().optional(), WEBHOOK_INVITATION_REDEEMED_URL: z.string().optional(),
WEBHOOK_REFERRAL_STATUS_URL: z.string().optional(), WEBHOOK_REFERRAL_STATUS_URL: z.string().optional(),
}); });

View File

@ -1,24 +1,4 @@
/** /**
* Shared Cosmos DB client for the Growth Service. * Re-export from @bytelyst/cosmos shared across all services.
*/ */
export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos";
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);
}

View File

@ -1,34 +1,12 @@
/** /**
* Typed service errors for consistent HTTP error responses. * Re-export from @bytelyst/errors shared across all services.
*/ */
export {
export class ServiceError extends Error { ServiceError,
constructor( BadRequestError,
public statusCode: number, UnauthorizedError,
message: string, ForbiddenError,
) { NotFoundError,
super(message); ConflictError,
this.name = "ServiceError"; TooManyRequestsError,
} } from "@bytelyst/errors";
}
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";
}
}

View File

@ -1,10 +1,9 @@
/** /**
* Centralized product identity single source of truth. * Re-export from @bytelyst/config shared product identity.
*
* NOTE: The canonical source is shared/product.json at the repo root.
* These values must stay in sync with that file.
*/ */
import { loadProductIdentity } from "@bytelyst/config";
export const PRODUCT_ID = "lysnrai"; const _id = loadProductIdentity();
export const DISPLAY_NAME = "LysnrAI"; export const PRODUCT_ID = _id.productId;
export const LICENSE_PREFIX = "LYSNR"; export const DISPLAY_NAME = _id.displayName;
export const LICENSE_PREFIX = _id.licensePrefix;

View File

@ -13,6 +13,9 @@
"lint": "eslint src/" "lint": "eslint src/"
}, },
"dependencies": { "dependencies": {
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@azure/cosmos": "^4.2.0", "@azure/cosmos": "^4.2.0",
"@azure/storage-blob": "^12.31.0", "@azure/storage-blob": "^12.31.0",
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",

View File

@ -1,31 +1,19 @@
import { z } from "zod"; import { z } from "zod";
const envSchema = z.object({ const envSchema = z.object({
// Server
PORT: z.coerce.number().default(4003), PORT: z.coerce.number().default(4003),
HOST: z.string().default("0.0.0.0"), HOST: z.string().default("0.0.0.0"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
CORS_ORIGIN: z.string().optional(), CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string().default("platform-service"), SERVICE_NAME: z.string().default("platform-service"),
// Database
COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"),
COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"),
COSMOS_DATABASE: z.string().default("lysnrai"), COSMOS_DATABASE: z.string().default("lysnrai"),
// Auth
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
// Blob Storage
AZURE_BLOB_CONNECTION_STRING: z.string().optional(), AZURE_BLOB_CONNECTION_STRING: z.string().optional(),
AZURE_BLOB_ACCOUNT_NAME: z.string().optional(), AZURE_BLOB_ACCOUNT_NAME: z.string().optional(),
AZURE_BLOB_ACCOUNT_KEY: z.string().optional(), AZURE_BLOB_ACCOUNT_KEY: z.string().optional(),
// Features
RATE_LIMIT_CONFIG_JSON: z.string().optional(), 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); export const config = envSchema.parse(process.env);

View File

@ -1,24 +1,4 @@
/** /**
* Shared Cosmos DB client for the Platform Service. * Re-export from @bytelyst/cosmos shared across all services.
*/ */
export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos";
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);
}

View File

@ -1,37 +1,12 @@
/** /**
* Typed service errors for consistent HTTP error responses. * Re-export from @bytelyst/errors shared across all services.
*/ */
export {
export class ServiceError extends Error { ServiceError,
constructor( BadRequestError,
public statusCode: number, UnauthorizedError,
message: string, ForbiddenError,
) { NotFoundError,
super(message); ConflictError,
this.name = "ServiceError"; TooManyRequestsError,
} } from "@bytelyst/errors";
}
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);
}
}

View File

@ -1,8 +1,9 @@
/** /**
* Centralized product identity single source of truth. * Re-export from @bytelyst/config shared product identity.
* NOTE: The canonical source is shared/product.json at the repo root.
*/ */
import { loadProductIdentity } from "@bytelyst/config";
export const PRODUCT_ID = "lysnrai"; const _id = loadProductIdentity();
export const DISPLAY_NAME = "LysnrAI"; export const PRODUCT_ID = _id.productId;
export const LICENSE_PREFIX = "LYSNR"; export const DISPLAY_NAME = _id.displayName;
export const LICENSE_PREFIX = _id.licensePrefix;

View File

@ -13,6 +13,9 @@
"lint": "eslint src/" "lint": "eslint src/"
}, },
"dependencies": { "dependencies": {
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@azure/cosmos": "^4.2.0", "@azure/cosmos": "^4.2.0",
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",
"@fastify/rate-limit": "^10.3.0", "@fastify/rate-limit": "^10.3.0",

View File

@ -1,22 +1,15 @@
import { z } from "zod"; import { z } from "zod";
const envSchema = z.object({ const envSchema = z.object({
// Server
PORT: z.coerce.number().default(4004), PORT: z.coerce.number().default(4004),
HOST: z.string().default("0.0.0.0"), HOST: z.string().default("0.0.0.0"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
CORS_ORIGIN: z.string().optional(), CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string().default("tracker-service"), SERVICE_NAME: z.string().default("tracker-service"),
// Database
COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"), COSMOS_ENDPOINT: z.string().min(1, "COSMOS_ENDPOINT is required"),
COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"), COSMOS_KEY: z.string().min(1, "COSMOS_KEY is required"),
COSMOS_DATABASE: z.string().default("lysnrai"), COSMOS_DATABASE: z.string().default("lysnrai"),
// Auth
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
// Product
DEFAULT_PRODUCT_ID: z.string().default("lysnrai"), DEFAULT_PRODUCT_ID: z.string().default("lysnrai"),
}); });

View File

@ -1,24 +1,4 @@
/** /**
* Shared Cosmos DB client for the Tracker Service. * Re-export from @bytelyst/cosmos shared across all services.
*/ */
export { getContainer, getCosmosClient, getDatabase } from "@bytelyst/cosmos";
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);
}

View File

@ -1,43 +1,12 @@
/** /**
* Typed service errors for consistent HTTP error responses. * Re-export from @bytelyst/errors shared across all services.
*/ */
export {
export class ServiceError extends Error { ServiceError,
constructor( BadRequestError,
public statusCode: number, UnauthorizedError,
message: string, ForbiddenError,
) { NotFoundError,
super(message); ConflictError,
this.name = "ServiceError"; TooManyRequestsError,
} } from "@bytelyst/errors";
}
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);
}
}

View File

@ -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. * 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();

View File

@ -4,5 +4,6 @@ export default defineConfig({
test: { test: {
globals: true, globals: true,
environment: "node", environment: "node",
passWithNoTests: true,
}, },
}); });