diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 00000000..aad8c599 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,21 @@ +{ + "name": "@bytelyst/config", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "peerDependencies": { + "zod": ">=3.20.0" + } +} diff --git a/packages/config/src/base-schema.ts b/packages/config/src/base-schema.ts new file mode 100644 index 00000000..81179a27 --- /dev/null +++ b/packages/config/src/base-schema.ts @@ -0,0 +1,21 @@ +/** + * Base environment schema shared by all Fastify microservices. + * Each service extends this with its own fields via loadConfig(). + */ + +import { z } from "zod"; + +export const baseEnvSchema = z.object({ + PORT: z.coerce.number().default(3000), + 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(), + 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"), +}); + +export type BaseEnv = z.infer; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 00000000..2519f6fe --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,8 @@ +export { baseEnvSchema, type BaseEnv } from "./base-schema.js"; +export { loadConfig } from "./loader.js"; +export { + loadProductIdentity, + getProductId, + _resetProductIdentity, + type ProductIdentity, +} from "./product-identity.js"; diff --git a/packages/config/src/loader.ts b/packages/config/src/loader.ts new file mode 100644 index 00000000..992d5544 --- /dev/null +++ b/packages/config/src/loader.ts @@ -0,0 +1,26 @@ +/** + * Config loader — parses process.env against the base schema + any extensions. + */ + +import { z, type ZodRawShape } from "zod"; +import { baseEnvSchema } from "./base-schema.js"; + +/** + * Load and validate environment configuration. + * + * @param extension - Additional Zod fields specific to this service + * @returns Parsed and validated config object + * + * @example + * ```ts + * const config = loadConfig({ + * STRIPE_SECRET_KEY: z.string().min(1), + * BILLING_INTERNAL_KEY: z.string().optional(), + * }); + * ``` + */ +export function loadConfig(extension?: T) { + const schema = extension ? baseEnvSchema.extend(extension) : baseEnvSchema; + return schema.parse(process.env) as z.infer & + (T extends ZodRawShape ? z.infer> : Record); +} diff --git a/packages/config/src/product-identity.ts b/packages/config/src/product-identity.ts new file mode 100644 index 00000000..a477f49b --- /dev/null +++ b/packages/config/src/product-identity.ts @@ -0,0 +1,75 @@ +/** + * Product identity — reads from a product.json file or falls back to env vars. + * Eliminates the need for hardcoded product-config.ts files in every service. + */ + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +export interface ProductIdentity { + productId: string; + displayName: string; + licensePrefix: string; + configDirName: string; + envVarPrefix: string; + bundleIdSuffix: string; + packageName: string; +} + +let _cached: ProductIdentity | null = null; + +/** + * Load product identity from a JSON file or environment variables. + * + * @param jsonPath - Path to product.json (optional, tries common locations) + * @returns Product identity object + */ +export function loadProductIdentity(jsonPath?: string): ProductIdentity { + if (_cached) return _cached; + + // Try loading from file + const paths = jsonPath + ? [jsonPath] + : [ + resolve("shared/product.json"), + resolve("../shared/product.json"), + resolve("../../shared/product.json"), + ]; + + for (const p of paths) { + try { + const raw = readFileSync(p, "utf-8"); + _cached = JSON.parse(raw) as ProductIdentity; + return _cached; + } catch { + // Try next path + } + } + + // Fallback to env vars / defaults + _cached = { + productId: process.env.PRODUCT_ID || "lysnrai", + displayName: process.env.DISPLAY_NAME || "LysnrAI", + licensePrefix: process.env.LICENSE_PREFIX || "LYSNR", + configDirName: process.env.CONFIG_DIR_NAME || ".LysnrAI", + envVarPrefix: process.env.ENV_VAR_PREFIX || "LYSNR", + bundleIdSuffix: process.env.BUNDLE_ID_SUFFIX || "LysnrAI", + packageName: process.env.PACKAGE_NAME || "lysnrai", + }; + return _cached; +} + +/** + * Convenience: get just the product ID string. + */ +export function getProductId(): string { + return loadProductIdentity().productId; +} + +/** + * Reset the cache (useful for testing). + * @internal + */ +export function _resetProductIdentity(): void { + _cached = null; +} diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json new file mode 100644 index 00000000..5edad813 --- /dev/null +++ b/packages/config/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +}