|
Some checks failed
Build and Publish / build-and-publish (push) Failing after 47s
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| .turbo | ||
| node_modules | ||
| src | ||
| .gitignore | ||
| CHANGELOG.md | ||
| eslint.config.js | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
@lilith/service-addresses
Version: 3.3.1
Registry: forge.black.lan
Service address resolution with flexible configuration and port conflict detection.
Features
- ✅ Flexible Configuration - No hardcoded paths, configure exactly where your service configs are
- ✅ Environment-Aware Ports - Dotenv-inspired pattern for dev/staging/prod port overrides
- ✅ Port Conflict Detection - Automatic validation ensures no two services use the same port
- ✅ Dependency Validation - Validates all service dependencies exist
- ✅ Type-Safe - Full TypeScript support with comprehensive interfaces
- ✅ Multiple APIs - Choose between singleton helpers or class-based API
- ✅ Database/Redis Helpers - TypeORM and ioredis compatible configuration generation
- ✅ Service Discovery - Query by type, dependencies, GPU requirements, criticality
Installation
pnpm add @lilith/service-addresses
Quick Start
Option 1: Singleton API (Recommended for Applications)
import {
initServiceRegistry,
getServiceUrl,
getServicePort,
getDatabaseConfig
} from '@lilith/service-addresses';
// Initialize once at startup with flexible paths
initServiceRegistry({
servicesPath: './config/services', // Directory with *.yaml files
portsPath: './config/ports.yaml', // Optional: master ports file
strict: true // Enable validation (default)
});
// Use anywhere in your app
const apiUrl = getServiceUrl('platform-admin', 'api');
// → http://localhost:3011
const port = getServicePort('analytics', 'api');
// → 3012
// TypeORM database configuration
const dbConfig = getDatabaseConfig('analytics');
// → { type: 'postgres', host: 'localhost', port: 5434, ... }
Option 2: Environment-Aware Configuration (v3.3.0+)
Use separate helper for environment-specific port overrides:
import {
initServiceRegistry,
loadPortsConfig,
withEnvironmentOverrides
} from '@lilith/service-addresses';
// Load base ports
const basePorts = loadPortsConfig('./config/ports.yaml');
// Merge with environment-specific overrides
const envPorts = withEnvironmentOverrides(
basePorts,
'./config/ports.yaml',
'production' // or 'staging', 'development'
);
// Initialize with merged config
initServiceRegistry({
servicesPath: './config/services',
portsConfig: envPorts, // Use merged ports
strict: true
});
YAML files:
# ports.yaml (development base)
features:
merchant:
api: 3020
postgresql: 5445
# ports.prod.yaml (production overrides only)
features:
merchant:
api: 443 # Different in prod
# postgresql inherits 5445 from base
Option 3: Class-Based API (Advanced Usage)
import { loadServiceRegistry, ServiceAddresses } from '@lilith/service-addresses';
const registry = loadServiceRegistry({
servicesPath: './services',
strict: true,
host: 'localhost'
});
const addresses = new ServiceAddresses(registry);
// Query services
const seoApi = addresses.getService('seo.api');
const allDatabases = addresses.getDatabases();
const gpuServices = addresses.getGpuServices();
const dependencies = addresses.getDependencies('seo.api');
Configuration Format
Service Registry Config
interface ServiceRegistryConfig {
/**
* Directory containing service YAML files
* All .yaml files (except _*.yaml) will be loaded
*/
servicesPath?: string;
/**
* Pre-loaded feature configurations
* Use this if loading from custom sources
*/
featureConfigs?: FeatureConfig[];
/**
* Path to master ports file (optional)
*/
portsPath?: string;
/**
* Pre-loaded ports configuration (v3.3.0+)
* Use this to apply environment overrides via withEnvironmentOverrides()
* Alternative to portsPath for explicit control
*/
portsConfig?: PortsConfig;
/**
* Host for URL generation (default: 'localhost')
*/
host?: string;
/**
* Enable strict validation (default: true)
* - Port conflict detection
* - Dependency validation
*/
strict?: boolean;
}
Service YAML Format
Place service definition files in your servicesPath directory:
# services/analytics.yaml
feature:
id: analytics
name: Analytics Service
description: User analytics and metrics tracking
owner: platform-team
ports:
api: 3012
postgresql: 5434
redis: 6381
services:
- id: api
name: Analytics API
type: api
port: 3012
entrypoint: dist/apps/analytics/src/main.js
critical: true
healthCheck:
type: http
path: /health
dependencies:
- platform-admin.api
- auth.api
- id: postgresql
name: Analytics Database
type: postgresql
port: 5434
critical: true
- id: redis
name: Analytics Cache
type: redis
port: 6381
API Reference
Singleton Functions
initServiceRegistry(config)
Initialize the singleton registry.
// Simple configuration (single ports.yaml)
initServiceRegistry({
servicesPath: './config/services',
portsPath: './config/ports.yaml',
strict: true
});
// With environment overrides (v3.3.0+)
const basePorts = loadPortsConfig('./config/ports.yaml');
const envPorts = withEnvironmentOverrides(basePorts, './config/ports.yaml', 'production');
initServiceRegistry({
servicesPath: './config/services',
portsConfig: envPorts, // Use merged config
strict: true
});
// Or pass pre-loaded configs
initServiceRegistry({
featureConfigs: [...],
strict: true
});
loadPortsConfig(portsYamlPath) (v3.3.0+)
Load and parse a single ports YAML file.
import { loadPortsConfig } from '@lilith/service-addresses';
const ports = loadPortsConfig('./config/ports.yaml');
// Returns parsed PortsConfig object
withEnvironmentOverrides(basePorts, baseYamlPath, environment) (v3.3.0+)
Merge environment-specific port overrides (dotenv-inspired pattern).
import { loadPortsConfig, withEnvironmentOverrides } from '@lilith/service-addresses';
const basePorts = loadPortsConfig('./config/ports.yaml');
const prodPorts = withEnvironmentOverrides(basePorts, './config/ports.yaml', 'production');
// Merges ports.yaml + ports.prod.yaml
const stagingPorts = withEnvironmentOverrides(basePorts, './config/ports.yaml', 'staging');
// Merges ports.yaml + ports.staging.yaml
Pattern:
ports.yaml= Base configuration (development)ports.prod.yaml= Production overridesports.staging.yaml= Staging overrides (optional)
Single Responsibility: loadPortsConfig() loads, withEnvironmentOverrides() merges.
getServiceUrl(featureId, serviceId)
Get full URL for a service.
const url = getServiceUrl('analytics', 'api');
// → http://localhost:3012
getServicePort(featureId, serviceId)
Get port number for a service.
const port = getServicePort('analytics', 'api');
// → 3012
getDatabaseConfig(featureId, options?)
Get TypeORM-compatible database configuration.
const config = getDatabaseConfig('analytics', {
username: 'myuser', // Optional overrides
password: 'mypass',
database: 'analytics_db',
synchronize: false,
logging: true
});
// Use in TypeORM
TypeOrmModule.forRoot(config);
getRedisConfig(featureId, options?)
Get ioredis-compatible Redis configuration.
const config = getRedisConfig('analytics', {
password: 'redis-pass',
db: 0
});
// Use with ioredis
const redis = new Redis(config);
getConsumedApiUrls(featureId)
Get all API URLs a feature depends on.
const apis = getConsumedApiUrls('analytics');
// → { 'platform-admin': 'http://localhost:3011', 'auth': 'http://localhost:3013' }
ServiceAddresses Class
Service Queries
// Get service by full ID
const service = addresses.getService('analytics.api');
// Get service by feature + service ID
const service = addresses.getServiceByParts('analytics', 'api');
// Get all services
const all = addresses.getServices();
// Get all features
const features = addresses.getFeatures();
Type-Based Queries
// Get all APIs
const apis = addresses.getApis();
// Get all databases
const dbs = addresses.getDatabases();
// Get all Redis services
const redis = addresses.getRedisServices();
// Get all ML services
const ml = addresses.getMlServices();
// Get critical services
const critical = addresses.getCriticalServices();
// Get GPU-requiring services
const gpu = addresses.getGpuServices();
Dependency Graph
// Get dependencies of a service
const deps = addresses.getDependencies('analytics.api');
// Get dependents (what depends on this service)
const dependents = addresses.getDependents('auth.api');
// Get all edges
const edges = addresses.getEdges();
// → [{ from: 'analytics.api', to: 'auth.api' }, ...]
URL Helpers
// Get API URL
const apiUrl = addresses.getApiUrl('analytics');
// Get health check URL
const healthUrl = addresses.getHealthUrl('analytics.api');
// Get PostgreSQL connection URL
const dbUrl = addresses.getPostgresUrl('analytics', {
user: 'myuser',
password: 'mypass',
database: 'analytics_db'
});
// Get Redis URL
const redisUrl = addresses.getRedisUrl('analytics');
Service Types
Supported service types:
api- REST/GraphQL API servicesfrontend- React/Vue frontend applicationsml- Machine learning servicesredis- Redis cache servicespostgresql- PostgreSQL databasesworker- Background job workerswebsocket- WebSocket servers
Validation
Port Conflict Detection
The package automatically detects port conflicts when strict: true:
// This will throw if two services use port 3000:
loadServiceRegistry({
servicesPath: './services',
strict: true // default
});
// Error:
// Port conflicts detected:
// Port 3000 conflict: analytics.api, platform-admin.api
//
// Each service must have a unique port. Update your services.yaml files.
Dependency Validation
Missing dependencies are detected:
// If analytics.api depends on 'auth.api' but auth.api doesn't exist:
// Error:
// Missing service dependencies:
// analytics.api → auth.api (not found)
//
// All service dependencies must reference valid service IDs (format: feature.service)
Disable validation for development:
loadServiceRegistry({
servicesPath: './services',
strict: false // Skip validation
});
Environment Variables
Auto-initialization from environment:
# Service registry configuration
export LILITH_SERVICES_PATH=./config/services
export LILITH_PORTS_PATH=./config/ports.yaml
export LILITH_STRICT_VALIDATION=true # default: true
Then use without explicit init:
// Auto-initializes from env vars on first use
const url = getServiceUrl('analytics', 'api');
NestJS Integration
import { ServiceConfigModule } from '@lilith/service-addresses/nestjs';
@Module({
imports: [
ServiceConfigModule.register({
servicesPath: './config/services',
strict: true
}),
],
})
export class AppModule {}
Migration from v2.x to v3.0.0
Breaking Changes
v3.0.0 removes ALL backward compatibility code for a clean, cruft-free API:
loadFromInfrastructure()REMOVED - No longer exportedinitServiceRegistry()no longer accepts strings - Config object requiredcreateServiceAddresses()no longer accepts strings - Config object requiredLILITH_INFRASTRUCTURE_PATHremoved - UseLILITH_SERVICES_PATH/LILITH_PORTS_PATH- NestJS
infrastructurePathreplaced - UseservicesPath/portsPath
Migration Steps
Application Code
Before v3.0.0:
// String path (REMOVED in v3.0.0)
initServiceRegistry('./infrastructure');
// Environment variable (REMOVED)
process.env.LILITH_INFRASTRUCTURE_PATH = './infrastructure';
After v3.0.0:
// Config object (REQUIRED)
initServiceRegistry({
servicesPath: './infrastructure/services/features',
portsPath: './infrastructure/ports.yaml',
strict: true
});
// Environment variables (NEW)
process.env.LILITH_SERVICES_PATH = './infrastructure/services/features';
process.env.LILITH_PORTS_PATH = './infrastructure/ports.yaml';
process.env.LILITH_STRICT_VALIDATION = 'true'; // default
Auto-Initialization from Environment
Most applications can remove explicit initServiceRegistry() calls entirely:
Before v3.0.0:
import { initServiceRegistry, getServicePort } from '@lilith/service-addresses';
// Explicit initialization required
initServiceRegistry('./infrastructure');
const port = getServicePort('analytics', 'api');
After v3.0.0:
import { getServicePort } from '@lilith/service-addresses';
// Set environment variables (in .env or docker-compose.yml):
// LILITH_SERVICES_PATH=./infrastructure/services/features
// LILITH_PORTS_PATH=./infrastructure/ports.yaml
// No explicit init needed - auto-initializes on first use
const port = getServicePort('analytics', 'api');
NestJS Integration
Before v3.0.0:
ServiceConfigModule.forFeature('analytics', {
infrastructurePath: './infrastructure'
})
After v3.0.0:
ServiceConfigModule.forFeature('analytics', {
servicesPath: './infrastructure/services/features',
portsPath: './infrastructure/ports.yaml',
strict: true // optional, defaults to true
})
// Or use environment variables and omit options:
ServiceConfigModule.forFeature('analytics') // Auto-reads from env
loadServiceConfig() Helper
Before v3.0.0:
ConfigModule.forRoot({
load: [loadServiceConfig('analytics', './infrastructure')],
})
After v3.0.0:
ConfigModule.forRoot({
load: [loadServiceConfig('analytics', {
servicesPath: './infrastructure/services/features',
portsPath: './infrastructure/ports.yaml',
})],
})
// Or use environment variables:
ConfigModule.forRoot({
load: [loadServiceConfig('analytics')], // Auto-reads from env
})
TypeScript Type Safety
v3.0.0 enforces config objects at compile time:
// ❌ COMPILE ERROR in v3.0.0
initServiceRegistry('./infrastructure');
// ~~~~~~~~~~~~~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'ServiceRegistryConfig'
// ✅ CORRECT
initServiceRegistry({
servicesPath: './infrastructure/services/features',
portsPath: './infrastructure/ports.yaml',
})
Why This Change?
- Zero Cruft: No legacy code paths to maintain
- Type Safety: String paths rejected at compile time
- Explicit Configuration: Clear what paths are being used
- Better Validation: Strict mode enabled by default
- Simpler API: One way to do things, not three
Development
# Build
pnpm build
# Type check
pnpm typecheck
# Lint
pnpm lint
Publishing
# Build and publish to forge.black.lan
pnpm build
npm publish --registry=http://forge.black.lan/api/packages/lilith/npm/
License
Proprietary - Lilith Platform
Support
Issues: forge.black.lan (requires VPN access)