feat(config): add @bytelyst/config package

- Zod-based baseEnvSchema (PORT, HOST, NODE_ENV, CORS_ORIGIN, SERVICE_NAME, COSMOS_*)
- loadConfig() with extension support for service-specific fields
- loadProductIdentity() reads product.json or falls back to env vars
- getProductId() convenience function
- Replaces 5 duplicated product-config.ts files across LysnrAI
- Peer dep: zod >=3.20.0
This commit is contained in:
saravanakumardb1 2026-02-12 11:19:49 -08:00
parent 2e9dcf49a8
commit 65bf79203b
6 changed files with 160 additions and 0 deletions

View File

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

View File

@ -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<typeof baseEnvSchema>;

View File

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

View File

@ -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<T extends ZodRawShape>(extension?: T) {
const schema = extension ? baseEnvSchema.extend(extension) : baseEnvSchema;
return schema.parse(process.env) as z.infer<typeof baseEnvSchema> &
(T extends ZodRawShape ? z.infer<z.ZodObject<T>> : Record<string, never>);
}

View File

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

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}