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:
parent
772df7785b
commit
4ae7a9d023
24
.env.example
Normal file
24
.env.example
Normal 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
169
docker-compose.yml
Normal 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:
|
||||
@ -21,6 +21,6 @@
|
||||
"bcryptjs": ">=2.4.0",
|
||||
"jose": ">=5.0.0",
|
||||
"react": ">=18.0.0",
|
||||
"zod": ">=3.20.0"
|
||||
"zod": "^3.24.0"
|
||||
}
|
||||
}
|
||||
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<string, unknown>,
|
||||
) {
|
||||
super(429, message);
|
||||
}
|
||||
}
|
||||
export {
|
||||
ServiceError,
|
||||
BadRequestError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
TooManyRequestsError,
|
||||
} from "@bytelyst/errors";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"),
|
||||
});
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -4,5 +4,6 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user