feat(config): implement P0-1 - Product manifest specification (product.json schema)

- Add ProductManifestSchema with Zod validation for product identity
- Support theme tokens, cosmos containers, feature flags, ports
- Add loadProductManifest(), validateProductManifest(), resolveTheme()
- Export all schemas and types from config package index
This commit is contained in:
saravanakumardb1 2026-03-03 10:01:40 -08:00
parent 89458d7f75
commit 7f5ff4c790
2 changed files with 273 additions and 0 deletions

View File

@ -13,3 +13,24 @@ export {
type SecretMapping,
type SecretsProviderType,
} from './keyvault.js';
export {
ProductManifestSchema,
PlatformSchema,
ThemeSchema,
ContainerDefSchema,
FeatureFlagSchema,
PortConfigSchema,
ExtendedProductManifestSchema,
DEFAULT_THEME,
loadProductManifest,
loadProductManifestSync,
resolveTheme,
validateProductManifest,
safeValidateProductManifest,
type ProductManifest,
type Platform,
type Theme,
type ContainerDef,
type FeatureFlag,
type PortConfig,
} from './product-manifest.js';

View File

@ -0,0 +1,252 @@
/**
* Product Manifest Specification
*
* Defines the JSON schema for product.json files that capture everything
* about a ByteLyst product identity, theme, features, containers, flags, ports.
*
* @module @bytelyst/config/product-manifest
*/
import { z } from 'zod';
/**
* Platform identifiers
*/
export const PlatformSchema = z.enum(['web', 'ios', 'android', 'desktop', 'watch']);
/**
* Theme color token
*/
export const ColorTokenSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/, {
message: 'Color must be a hex value like #5AE68C',
});
/**
* Theme specification
*/
export const ThemeSchema = z.object({
primary: ColorTokenSchema,
secondary: ColorTokenSchema,
accent: ColorTokenSchema,
background: ColorTokenSchema,
surface: ColorTokenSchema,
text: ColorTokenSchema,
error: ColorTokenSchema,
warning: ColorTokenSchema,
success: ColorTokenSchema,
});
/**
* Cosmos container definition
*/
export const ContainerDefSchema = z.object({
name: z.string(),
partitionKey: z.string(),
ttlSeconds: z.number().optional(),
uniqueKeys: z.array(z.string()).optional(),
});
/**
* Feature flag default
*/
export const FeatureFlagSchema = z.object({
key: z.string(),
defaultValue: z.union([z.boolean(), z.string(), z.number()]),
description: z.string().optional(),
});
/**
* Port configuration
*/
export const PortConfigSchema = z.object({
service: z.number().optional(),
dashboard: z.number().optional(),
web: z.number().optional(),
});
/**
* Product manifest schema (Zod)
*
* Example product.json:
* ```json
* {
* "id": "lysnrai",
* "name": "LysnrAI",
* "displayName": "LysnrAI",
* "bundleId": "com.saravana.lysnrai",
* "domain": "lysnrai.app",
* "description": "Voice-to-text dictation platform",
* "platforms": ["web", "ios", "android", "desktop"],
* "theme": {
* "primary": "#5AE68C",
* "secondary": "#5A8CFF",
* "accent": "#2EE6D6",
* "background": "#06070A",
* "surface": "#121725",
* "text": "#EFF4FF",
* "error": "#FF6E6E",
* "warning": "#F59E0B",
* "success": "#34D399"
* },
* "features": {
* "voiceDictation": true,
* "realtimeTranscription": true,
* "keyboardExtension": true,
* "cloudSync": true
* },
* "cosmos": {
* "containers": [
* { "name": "transcripts", "partitionKey": "/userId" },
* { "name": "sessions", "partitionKey": "/userId" }
* ]
* },
* "flags": [
* { "key": "new-ui-enabled", "defaultValue": false },
* { "key": "max-audio-duration", "defaultValue": 300 }
* ],
* "ports": {
* "service": 4015,
* "dashboard": 3002
* }
* }
* ```
*/
export const ProductManifestSchema = z.object({
// Identity (all required)
id: z.string().regex(/^[a-z][a-z0-9]*$/, {
message: 'Product ID must be lowercase alphanumeric, starting with letter',
}),
name: z.string().min(1).max(50),
displayName: z.string().min(1).max(50),
bundleId: z.string().regex(/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/, {
message: 'Bundle ID must be valid reverse-DNS format',
}),
domain: z.string().regex(/^[a-z0-9-]+\.[a-z]{2,}$/, {
message: 'Domain must be valid format like "product.app"',
}),
// Optional metadata
description: z.string().max(200).optional(),
platforms: z.array(PlatformSchema).default(['web']),
version: z.string().regex(/^\d+\.\d+\.\d+$/).optional(),
// Theming (optional, uses defaults if not specified)
theme: ThemeSchema.partial().optional(),
// Feature flags (optional)
features: z.record(
z.string(),
z.boolean().or(z.string()).or(z.number())
).optional(),
// Cosmos containers (optional)
cosmos: z.object({
containers: z.array(ContainerDefSchema).default([]),
}).optional(),
// Feature flags with defaults (optional)
flags: z.array(FeatureFlagSchema).default([]),
// Port configuration (optional)
ports: PortConfigSchema.optional(),
});
/**
* Extended manifest that allows additional unknown keys
* Use this when you need to access custom fields not in the schema
*/
export const ExtendedProductManifestSchema = ProductManifestSchema.passthrough();
/**
* Inferred TypeScript type for ProductManifest
*/
export type ProductManifest = z.infer<typeof ProductManifestSchema>;
export type Platform = z.infer<typeof PlatformSchema>;
export type Theme = z.infer<typeof ThemeSchema>;
export type ContainerDef = z.infer<typeof ContainerDefSchema>;
export type FeatureFlag = z.infer<typeof FeatureFlagSchema>;
export type PortConfig = z.infer<typeof PortConfigSchema>;
/**
* Default theme colors (ByteLyst brand palette)
*/
export const DEFAULT_THEME: Theme = {
primary: '#5AE68C',
secondary: '#5A8CFF',
accent: '#2EE6D6',
background: '#06070A',
surface: '#121725',
text: '#EFF4FF',
error: '#FF6E6E',
warning: '#F59E0B',
success: '#34D399',
};
/**
* Load and validate a product manifest from a file path
*
* @param path Path to product.json file
* @returns Validated ProductManifest
* @throws ZodError if validation fails
*
* @example
* ```ts
* const manifest = await loadProductManifest('./product.json');
* console.log(manifest.id); // 'lysnrai'
* ```
*/
export async function loadProductManifest(path: string): Promise<ProductManifest> {
const fs = await import('fs/promises');
const content = await fs.readFile(path, 'utf-8');
const json = JSON.parse(content);
return ProductManifestSchema.parse(json);
}
/**
* Synchronous version of loadProductManifest (for startup use)
*
* @param path Path to product.json file
* @returns Validated ProductManifest
* @throws ZodError if validation fails
*/
export function loadProductManifestSync(path: string): ProductManifest {
const fs = require('fs');
const content = fs.readFileSync(path, 'utf-8');
const json = JSON.parse(content);
return ProductManifestSchema.parse(json);
}
/**
* Merge manifest theme with defaults
*
* @param manifest Product manifest
* @returns Complete theme with defaults filled in
*/
export function resolveTheme(manifest: ProductManifest): Theme {
return {
...DEFAULT_THEME,
...manifest.theme,
};
}
/**
* Validate a product manifest object without loading from file
*
* @param json Parsed JSON object
* @returns Validated ProductManifest
* @throws ZodError if validation fails
*/
export function validateProductManifest(json: unknown): ProductManifest {
return ProductManifestSchema.parse(json);
}
/**
* Safe validation that returns null on failure
*
* @param json Parsed JSON object
* @returns Validated ProductManifest or null
*/
export function safeValidateProductManifest(json: unknown): ProductManifest | null {
const result = ProductManifestSchema.safeParse(json);
return result.success ? result.data : null;
}