2025-11-27 22:15:38 +00:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 23:47:33 +00:00
|
|
|
// Registry protocol endpoints (handled by smartregistry)
|
|
|
|
|
// NPM: /-/..., /@scope/package (but not /packages which is UI route)
|
2025-11-27 22:15:38 +00:00
|
|
|
// OCI: /v2/...
|
|
|
|
|
// Maven: /maven2/...
|
|
|
|
|
// PyPI: /simple/..., /pypi/...
|
|
|
|
|
// Cargo: /api/v1/crates/...
|
|
|
|
|
// Composer: /packages.json, /p/...
|
|
|
|
|
// RubyGems: /api/v1/gems/..., /gems/...
|
2025-11-27 23:47:33 +00:00
|
|
|
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'));
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2025-11-27 23:47:33 +00:00
|
|
|
if (this.smartRegistry && isRegistryPath) {
|
2025-11-27 22:15:38 +00:00
|
|
|
try {
|
2025-11-27 23:47:33 +00:00
|
|
|
const response = await this.smartRegistry.handleRequest(request);
|
|
|
|
|
if (response) return response;
|
2025-11-27 22:15:38 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[StackGalleryRegistry] Request error:', error);
|
|
|
|
|
return new Response(
|
|
|
|
|
JSON.stringify({ error: 'Internal server error' }),
|
|
|
|
|
{
|
|
|
|
|
status: 500,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 23:47:33 +00:00
|
|
|
// 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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-27 22:15:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2025-11-27 23:47:33 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
}
|