/** * StackGalleryRegistry - Main registry class * Integrates smartregistry with Stack.Gallery's auth, storage, and database */ import * as plugins from './plugins.ts'; import { initDb, closeDb, isDbConnected } from './models/db.ts'; import { StackGalleryAuthProvider } from './providers/auth.provider.ts'; import { StackGalleryStorageHooks } from './providers/storage.provider.ts'; import { ApiRouter } from './api/router.ts'; export interface IRegistryConfig { // MongoDB configuration mongoUrl: string; mongoDb: string; // S3 configuration s3Endpoint: string; s3AccessKey: string; s3SecretKey: string; s3Bucket: string; s3Region?: string; // Server configuration host?: string; port?: number; // Registry settings storagePath?: string; enableUpstreamCache?: boolean; upstreamCacheExpiry?: number; // hours // JWT configuration jwtSecret?: string; } export class StackGalleryRegistry { private config: IRegistryConfig; private smartBucket: plugins.smartbucket.SmartBucket | null = null; private smartRegistry: plugins.smartregistry.SmartRegistry | null = null; private authProvider: StackGalleryAuthProvider | null = null; private storageHooks: StackGalleryStorageHooks | null = null; private apiRouter: ApiRouter | null = null; private isInitialized = false; constructor(config: IRegistryConfig) { this.config = { host: '0.0.0.0', port: 3000, storagePath: 'packages', enableUpstreamCache: true, upstreamCacheExpiry: 24, ...config, }; } /** * Initialize the registry */ public async init(): Promise { if (this.isInitialized) return; console.log('[StackGalleryRegistry] Initializing...'); // Initialize MongoDB console.log('[StackGalleryRegistry] Connecting to MongoDB...'); await initDb(this.config.mongoUrl, this.config.mongoDb); console.log('[StackGalleryRegistry] MongoDB connected'); // Initialize S3/SmartBucket console.log('[StackGalleryRegistry] Initializing S3 storage...'); this.smartBucket = new plugins.smartbucket.SmartBucket({ accessKey: this.config.s3AccessKey, accessSecret: this.config.s3SecretKey, endpoint: this.config.s3Endpoint, bucketName: this.config.s3Bucket, }); console.log('[StackGalleryRegistry] S3 storage initialized'); // Initialize auth provider this.authProvider = new StackGalleryAuthProvider(); // Initialize storage hooks this.storageHooks = new StackGalleryStorageHooks({ bucket: this.smartBucket, basePath: this.config.storagePath!, }); // Initialize smartregistry console.log('[StackGalleryRegistry] Initializing smartregistry...'); this.smartRegistry = new plugins.smartregistry.SmartRegistry({ authProvider: this.authProvider, storageHooks: this.storageHooks, storage: { type: 's3', bucket: this.smartBucket, basePath: this.config.storagePath, }, upstreamCache: this.config.enableUpstreamCache ? { enabled: true, expiryHours: this.config.upstreamCacheExpiry, } : undefined, }); console.log('[StackGalleryRegistry] smartregistry initialized'); // Initialize API router console.log('[StackGalleryRegistry] Initializing API router...'); this.apiRouter = new ApiRouter(); console.log('[StackGalleryRegistry] API router initialized'); this.isInitialized = true; console.log('[StackGalleryRegistry] Initialization complete'); } /** * Start the HTTP server */ public async start(): Promise { if (!this.isInitialized) { await this.init(); } const port = this.config.port!; const host = this.config.host!; console.log(`[StackGalleryRegistry] Starting server on ${host}:${port}...`); Deno.serve( { port, hostname: host }, async (request: Request): Promise => { return await this.handleRequest(request); } ); console.log(`[StackGalleryRegistry] Server running on http://${host}:${port}`); } /** * Handle incoming HTTP request */ private async handleRequest(request: Request): Promise { const url = new URL(request.url); const path = url.pathname; // Health check if (path === '/health' || path === '/healthz') { return this.healthCheck(); } // API endpoints (handled by REST API layer) if (path.startsWith('/api/')) { return await this.handleApiRequest(request); } // Registry protocol endpoints (handled by smartregistry) // NPM: /-/..., /@scope/package (but not /packages which is UI route) // OCI: /v2/... // Maven: /maven2/... // PyPI: /simple/..., /pypi/... // Cargo: /api/v1/crates/... // Composer: /packages.json, /p/... // RubyGems: /api/v1/gems/..., /gems/... const registryPaths = ['/-/', '/v2/', '/maven2/', '/simple/', '/pypi/', '/api/v1/crates/', '/packages.json', '/p/', '/api/v1/gems/', '/gems/']; const isRegistryPath = registryPaths.some(p => path.startsWith(p)) || (path.startsWith('/@') && !path.startsWith('/@stack')); if (this.smartRegistry && isRegistryPath) { try { const response = await this.smartRegistry.handleRequest(request); if (response) return response; } catch (error) { console.error('[StackGalleryRegistry] Request error:', error); return new Response( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ); } } // Serve static UI files return await this.serveStaticFile(path); } /** * Serve static files from UI dist */ private async serveStaticFile(path: string): Promise { const uiDistPath = './ui/dist/registry-ui/browser'; // Map path to file let filePath = path === '/' ? '/index.html' : path; // Content type mapping const contentTypes: Record = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', }; try { const fullPath = `${uiDistPath}${filePath}`; const file = await Deno.readFile(fullPath); const ext = filePath.substring(filePath.lastIndexOf('.')); const contentType = contentTypes[ext] || 'application/octet-stream'; return new Response(file, { status: 200, headers: { 'Content-Type': contentType }, }); } catch { // For SPA routing, serve index.html for unknown paths try { const indexFile = await Deno.readFile(`${uiDistPath}/index.html`); return new Response(indexFile, { status: 200, headers: { 'Content-Type': 'text/html' }, }); } catch { return new Response('Not Found', { status: 404 }); } } } /** * Handle API requests */ private async handleApiRequest(request: Request): Promise { if (!this.apiRouter) { return new Response( JSON.stringify({ error: 'API router not initialized' }), { status: 503, headers: { 'Content-Type': 'application/json' }, } ); } return await this.apiRouter.handle(request); } /** * Health check endpoint */ private healthCheck(): Response { const healthy = this.isInitialized && isDbConnected(); const status = { status: healthy ? 'healthy' : 'unhealthy', timestamp: new Date().toISOString(), services: { mongodb: isDbConnected() ? 'connected' : 'disconnected', s3: this.smartBucket ? 'initialized' : 'not initialized', registry: this.smartRegistry ? 'initialized' : 'not initialized', }, }; return new Response(JSON.stringify(status), { status: healthy ? 200 : 503, headers: { 'Content-Type': 'application/json' }, }); } /** * Stop the registry */ public async stop(): Promise { console.log('[StackGalleryRegistry] Shutting down...'); await closeDb(); this.isInitialized = false; console.log('[StackGalleryRegistry] Shutdown complete'); } /** * Get the smartregistry instance */ public getSmartRegistry(): plugins.smartregistry.SmartRegistry | null { return this.smartRegistry; } /** * Get the smartbucket instance */ public getSmartBucket(): plugins.smartbucket.SmartBucket | null { return this.smartBucket; } /** * Check if registry is initialized */ public getIsInitialized(): boolean { return this.isInitialized; } } /** * Create registry from environment variables */ export function createRegistryFromEnv(): StackGalleryRegistry { const config: IRegistryConfig = { mongoUrl: Deno.env.get('MONGODB_URL') || 'mongodb://localhost:27017', mongoDb: Deno.env.get('MONGODB_DB') || 'stackgallery', s3Endpoint: Deno.env.get('S3_ENDPOINT') || 'http://localhost:9000', s3AccessKey: Deno.env.get('S3_ACCESS_KEY') || 'minioadmin', s3SecretKey: Deno.env.get('S3_SECRET_KEY') || 'minioadmin', s3Bucket: Deno.env.get('S3_BUCKET') || 'registry', s3Region: Deno.env.get('S3_REGION'), host: Deno.env.get('HOST') || '0.0.0.0', port: parseInt(Deno.env.get('PORT') || '3000', 10), storagePath: Deno.env.get('STORAGE_PATH') || 'packages', enableUpstreamCache: Deno.env.get('ENABLE_UPSTREAM_CACHE') !== 'false', upstreamCacheExpiry: parseInt(Deno.env.get('UPSTREAM_CACHE_EXPIRY') || '24', 10), jwtSecret: Deno.env.get('JWT_SECRET'), }; return new StackGalleryRegistry(config); } /** * Create registry from .nogit/env.json file (for local development) * Falls back to environment variables if file doesn't exist */ export async function createRegistryFromEnvFile(): Promise { const envPath = '.nogit/env.json'; try { const envText = await Deno.readTextFile(envPath); const env = JSON.parse(envText); console.log('[StackGalleryRegistry] Loading config from .nogit/env.json'); // Build S3 endpoint from host/port/ssl settings const s3Protocol = env.S3_USESSL ? 'https' : 'http'; const s3Endpoint = `${s3Protocol}://${env.S3_HOST || 'localhost'}:${env.S3_PORT || '9000'}`; const config: IRegistryConfig = { mongoUrl: env.MONGODB_URL || `mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${env.MONGODB_PORT || '27017'}/${env.MONGODB_NAME}?authSource=admin`, mongoDb: env.MONGODB_NAME || 'stackgallery', s3Endpoint: s3Endpoint, s3AccessKey: env.S3_ACCESSKEY || env.S3_ACCESS_KEY || 'minioadmin', s3SecretKey: env.S3_SECRETKEY || env.S3_SECRET_KEY || 'minioadmin', s3Bucket: env.S3_BUCKET || 'registry', s3Region: env.S3_REGION, host: env.HOST || '0.0.0.0', port: parseInt(env.PORT || '3000', 10), storagePath: env.STORAGE_PATH || 'packages', enableUpstreamCache: env.ENABLE_UPSTREAM_CACHE !== false, upstreamCacheExpiry: parseInt(env.UPSTREAM_CACHE_EXPIRY || '24', 10), jwtSecret: env.JWT_SECRET, }; return new StackGalleryRegistry(config); } catch (error) { if (error instanceof Deno.errors.NotFound) { console.log('[StackGalleryRegistry] No .nogit/env.json found, using environment variables'); } else { console.warn('[StackGalleryRegistry] Error reading .nogit/env.json, falling back to env vars:', error); } return createRegistryFromEnv(); } }