Files
registry/ts/registry.ts

374 lines
12 KiB
TypeScript
Raw Normal View History

/**
* 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<void> {
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<void> {
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<Response> => {
return await this.handleRequest(request);
}
);
console.log(`[StackGalleryRegistry] Server running on http://${host}:${port}`);
}
/**
* Handle incoming HTTP request
*/
private async handleRequest(request: Request): Promise<Response> {
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<Response> {
const uiDistPath = './ui/dist/registry-ui/browser';
// Map path to file
let filePath = path === '/' ? '/index.html' : path;
// Content type mapping
const contentTypes: Record<string, string> = {
'.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<Response> {
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<void> {
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<StackGalleryRegistry> {
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();
}
}