chore(root): 🔧 🛏️ clean up unnecessary files

This commit is contained in:
Lilith 2026-01-12 20:21:31 -08:00
parent 4f774938cd
commit 5e8f073f99
10 changed files with 583 additions and 0 deletions

View file

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"builder": "tsc"
}
}

View file

@ -0,0 +1,36 @@
{
"name": "@lilith/ui-dev-tools-backend",
"version": "0.1.0",
"description": "Development-only API for WYSIWYG content editing (locale files, image metadata)",
"private": true,
"type": "module",
"scripts": {
"build": "nest build",
"dev": "nest start --watch",
"start": "node dist/main.js",
"start:debug": "nest start --debug --watch",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@lilith/service-addresses": "^3.0.0",
"@lilith/service-nestjs-bootstrap": "^1.0.0",
"@nestjs/axios": "^3.1.3",
"@nestjs/common": "^10.4.15",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.1.0",
"axios": "^1.7.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@types/express": "^5.0.0",
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,31 @@
/**
* AppModule - UI Dev Tools Backend API
*
* Provides development-only endpoints for:
* - Reading/writing locale files
* - Fetching image metadata
*
* All endpoints protected by DevGuard (NODE_ENV === 'development')
*/
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
import { DevModule } from './dev/dev.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
HttpModule.register({
timeout: 30000,
maxRedirects: 3,
}),
DevModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

View file

@ -0,0 +1,35 @@
/**
* DevGuard - Security guard for development-only API endpoints
*
* Ensures that dev endpoints are ONLY accessible when NODE_ENV === 'development'.
* This provides defense-in-depth protection against accidental exposure in production.
*
* Usage:
* ```typescript
* @Controller('dev')
* @UseGuards(DevGuard)
* export class DevController { ... }
* ```
*/
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DevGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const env = this.configService.get<string>('NODE_ENV');
// Only allow in development mode
if (env !== 'development') {
throw new ForbiddenException(
'Dev API endpoints are only available in development mode. ' +
'Set NODE_ENV=development to enable these endpoints.'
);
}
return true;
}
}

View file

@ -0,0 +1,123 @@
/**
* DevController - REST API for development-time content editing
*
* Endpoints:
* - POST /dev/read-locale - Read locale file content
* - POST /dev/write-locale - Write locale file with backup
* - POST /dev/image-metadata - Get image variation metadata
*
* Security: All endpoints protected by DevGuard (NODE_ENV === 'development')
*/
import { Controller, Post, Body, UseGuards, Logger } from '@nestjs/common';
import { DevGuard } from '../auth/dev.guard';
import { DevService } from './dev.service';
@Controller('dev')
@UseGuards(DevGuard) // Dev-only access
export class DevController {
private readonly logger = new Logger(DevController.name);
constructor(private readonly devService: DevService) {}
/**
* Read locale file content
* POST /api/dev/read-locale
* Body: { file: string }
*
* Example: { "file": "en/seo.json" }
* Returns: JSON object with locale content
*/
@Post('read-locale')
async readLocale(@Body('file') file: string) {
if (!file) {
return { success: false, error: 'Missing required field: file' };
}
try {
this.logger.log(`Reading locale file: ${file}`);
const content = await this.devService.readLocaleFile(file);
return { success: true, content };
} catch (error) {
this.logger.error(`Failed to read locale: ${(error as Error).message}`, (error as Error).stack);
return { success: false, error: (error as Error).message };
}
}
/**
* Write locale file content
* POST /api/dev/write-locale
* Body: { file: string, path: string, content: string, backup?: boolean }
*
* Example: {
* "file": "en/seo.json",
* "path": "homepage.hero.title",
* "content": "New Title",
* "backup": true
* }
* Returns: { success: boolean, backup?: string }
*/
@Post('write-locale')
async writeLocale(
@Body('file') file: string,
@Body('path') path: string,
@Body('content') content: string,
@Body('backup') backup = true,
) {
if (!file || !path || content === undefined) {
return {
success: false,
error: 'Missing required fields: file, path, content',
};
}
try {
this.logger.log(`Writing locale file: ${file}, path: ${path}`);
const result = await this.devService.writeLocaleFile(file, path, content, backup);
return result;
} catch (error) {
this.logger.error(`Failed to write locale: ${(error as Error).message}`, (error as Error).stack);
return { success: false, error: (error as Error).message };
}
}
/**
* Get image metadata
* POST /api/dev/image-metadata
* Body: { type: string, id: string, variant: string, family: string }
*
* Example: {
* "type": "seo",
* "id": "homepage-v2",
* "variant": "hero",
* "family": "cyberpunk"
* }
* Returns: Image metadata including URL, dimensions, prompt, etc.
*/
@Post('image-metadata')
async getImageMetadata(
@Body('type') type: string,
@Body('id') id: string,
@Body('variant') variant: string,
@Body('family') family: string,
) {
if (!type || !id || !variant || !family) {
return {
success: false,
error: 'Missing required fields: type, id, variant, family',
};
}
try {
this.logger.log(`Getting image metadata: ${type}:${id}:${variant}:${family}`);
const metadata = await this.devService.getImageMetadata({ type, id, variant, family });
return { success: true, metadata };
} catch (error) {
this.logger.error(
`Failed to get image metadata: ${(error as Error).message}`,
(error as Error).stack,
);
return { success: false, error: (error as Error).message };
}
}
}

View file

@ -0,0 +1,23 @@
/**
* DevModule - Development-time content editing API module
*
* Provides endpoints for:
* - Reading/writing locale files
* - Fetching image metadata
*
* All endpoints are protected by DevGuard (dev-only access)
*/
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DevController } from './dev.controller';
import { DevService } from './dev.service';
import { DevGuard } from '../auth/dev.guard';
@Module({
imports: [ConfigModule],
controllers: [DevController],
providers: [DevService, DevGuard],
exports: [DevService], // Export in case other modules need dev services
})
export class DevModule {}

View file

@ -0,0 +1,204 @@
/**
* DevService - Business logic for development-time content editing
*
* Provides services for:
* - Reading/writing locale files (JSON)
* - Fetching image metadata from image-generator API
*
* Security layers:
* - Path validation (prevents directory traversal)
* - Backup creation before writes
* - DevGuard ensures dev-only access
*/
import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs/promises';
import * as path from 'path';
export interface ImageMetadata {
url: string;
width: number;
height: number;
format: string;
size: number;
generatedAt: string;
prompt: string;
family: string;
pageId: string;
variant: string;
}
@Injectable()
export class DevService {
private readonly logger = new Logger(DevService.name);
private readonly localesPath: string;
private readonly featuresPath: string;
constructor(private readonly configService: ConfigService) {
// Get paths from environment (same pattern as SEO locale services)
this.featuresPath =
this.configService.get('FEATURES_PATH') ||
'/var/home/lilith/Code/@applications/@lilith/lilith-platform/codebase/features';
this.localesPath = path.join(this.featuresPath, 'i18n', 'locales');
this.logger.log(`Initialized with localesPath: ${this.localesPath}`);
}
/**
* Read locale file
* Pattern: Replicate SEO locale-export.service.ts read() method
*/
async readLocaleFile(file: string): Promise<object> {
// Security: Validate file is within locales directory
const fullPath = path.join(this.localesPath, file);
await this.validatePath(fullPath, this.localesPath);
try {
const content = await fs.readFile(fullPath, 'utf-8');
return JSON.parse(content);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new NotFoundException(`Locale file not found: ${file}`);
}
this.logger.error(`Failed to read locale file ${file}: ${(error as Error).message}`);
throw error;
}
}
/**
* Write locale file with backup
* Pattern: Replicate SEO locale-export.service.ts exportLocale() method
*/
async writeLocaleFile(
file: string,
keyPath: string,
content: string,
backup: boolean = true,
): Promise<{ success: boolean; backup?: string }> {
const fullPath = path.join(this.localesPath, file);
// Security check
await this.validatePath(fullPath, this.localesPath);
// Read current content
const currentContent = await this.readLocaleFile(file);
// Create backup if requested
let backupPath: string | undefined;
if (backup) {
backupPath = `${fullPath}.${Date.now()}.bak`;
await fs.writeFile(backupPath, JSON.stringify(currentContent, null, 2), 'utf-8');
this.logger.log(`Created backup at ${backupPath}`);
}
// Update nested value
const updated = this.setNestedValue(currentContent, keyPath, content);
// Write back (pretty-printed)
await fs.writeFile(fullPath, JSON.stringify(updated, null, 2), 'utf-8');
this.logger.log(`Updated locale file ${file} at path ${keyPath}`);
return { success: true, backup: backupPath };
}
/**
* Get image metadata
* Calls image-generator backend-api to get variation details
*/
async getImageMetadata(parsed: {
type: string;
id: string;
variant: string;
family: string;
}): Promise<ImageMetadata> {
// For SEO images, construct variation name
// Pattern from SEO image-generation.service.ts
const variationName = `seo-${parsed.id}-${parsed.variant}`;
// Call image-generator API
// TODO: Use service registry to get URL instead of hardcoding
const imageGenUrl = 'http://localhost:3013';
const response = await fetch(`${imageGenUrl}/api/images/variations/name/${variationName}`);
if (!response.ok) {
throw new NotFoundException(`Image variation not found: ${variationName}`);
}
const variation = await response.json();
// Find specific derivative (family)
const derivative = variation.derivatives.find((d: any) => d.name.includes(parsed.family));
if (!derivative) {
throw new NotFoundException(
`Image family not found: ${parsed.family} in variation ${variationName}`,
);
}
return {
url: `/api/images/${variation.name}/${derivative.name}`,
width: derivative.width,
height: derivative.height,
format: derivative.format,
size: derivative.size,
generatedAt: variation.createdAt,
prompt: variation.prompt,
family: parsed.family,
pageId: parsed.id,
variant: parsed.variant,
};
}
/**
* Validate path to prevent directory traversal attacks
* Ensures the resolved path is within the allowed base directory
*/
private async validatePath(targetPath: string, basePath: string): Promise<void> {
// Resolve both paths to absolute, normalized paths
const resolvedTarget = path.resolve(targetPath);
const resolvedBase = path.resolve(basePath);
// Check if target starts with base (prevents traversal)
if (!resolvedTarget.startsWith(resolvedBase)) {
throw new BadRequestException(
`Invalid file path: Path traversal detected. Target ${resolvedTarget} is outside ${resolvedBase}`,
);
}
// Additional check: Verify target exists and is a file (not symlink)
try {
const stats = await fs.lstat(resolvedTarget);
if (!stats.isFile()) {
throw new BadRequestException(
`Invalid target: ${resolvedTarget} is not a regular file`,
);
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// File doesn't exist yet - this is OK for writes
return;
}
throw error;
}
}
/**
* Helper: Set nested value in object by dot-notation path
* Example: setNestedValue(obj, 'foo.bar.baz', 'value') sets obj.foo.bar.baz = 'value'
*/
private setNestedValue(obj: any, path: string, value: any): any {
const keys = path.split('.');
const result = { ...obj };
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
current[key] = { ...current[key] };
current = current[key];
}
current[keys[keys.length - 1]] = value;
return result;
}
}

View file

@ -0,0 +1,51 @@
/**
* Main entry point for UI Dev Tools Backend API
*
* Development-only service providing content editing APIs for:
* - Locale file manipulation
* - Image metadata retrieval
*
* Protected by DevGuard - only accessible when NODE_ENV=development
*/
import { bootstrap, presets } from '@lilith/service-nestjs-bootstrap';
import { getServicePort, getServiceUrl } from '@lilith/service-addresses';
import { AppModule } from './app.module.js';
// Service registry auto-initializes from environment variables:
// - LILITH_SERVICES_PATH
// - LILITH_PORTS_PATH
// - LILITH_STRICT_VALIDATION
// Get port from service registry with env override
const port = process.env.PORT
? Number(process.env.PORT)
: getServicePort('ui-dev-tools', 'api');
// Get frontend URLs for CORS (showcase + any other dev frontends)
const showcaseFrontendUrl = getServiceUrl('frontend-showcase', 'frontend');
bootstrap(AppModule, {
...presets.api,
cors: {
origins: [
showcaseFrontendUrl,
'http://localhost:5173', // Vite default
'http://localhost:3000', // React dev server default
],
credentials: true,
},
swagger: {
enabled: true,
path: 'docs',
title: 'UI Dev Tools API',
description: 'Development-only API for WYSIWYG content editing',
version: '1.0',
bearerAuth: false, // No auth needed for dev-only service
},
port,
}).then(() => {
console.log(`UI Dev Tools API running on port ${port}`);
console.log(`Swagger docs available at http://localhost:${port}/docs`);
console.log('\n⚠ This service is DEV-ONLY (NODE_ENV=development required)');
});

View file

@ -0,0 +1,35 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

View file

@ -0,0 +1,36 @@
# =============================================================================
# UI Dev Tools
# =============================================================================
# Development-only API for WYSIWYG content editing
# Protected by DevGuard (NODE_ENV=development required)
feature:
id: ui-dev-tools
name: UI Dev Tools
description: Development-only API for WYSIWYG content editing (locale files, image metadata)
owner: platform-core
ports:
api: 3016
services:
- id: api
name: UI Dev Tools API
type: api
port: 3016
entrypoint: codebase/features/ui-dev-tools/backend-api
description: Development-only API for content editing
healthCheck:
type: http
path: /health
dependencies:
- infrastructure.redis
optionalDependencies:
- image-generator.api # For image metadata fetching
- i18n.api # For locale file operations
deployments:
dev:
host: apricot
autostart: false # Dev-only service, manual start
# No staging or production deployments - dev-only service