/** * IStorageHooks implementation for smartregistry * Integrates Stack.Gallery's storage with smartregistry */ import * as plugins from '../plugins.ts'; import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { Package } from '../models/package.ts'; import { Repository } from '../models/repository.ts'; import { Organization } from '../models/organization.ts'; import { AuditService } from '../services/audit.service.ts'; export interface IStorageConfig { bucket: plugins.smartbucket.SmartBucket; basePath: string; } /** * Storage hooks implementation that tracks packages in MongoDB * and stores artifacts in S3 via smartbucket */ export class StackGalleryStorageHooks implements plugins.smartregistry.IStorageHooks { private config: IStorageConfig; constructor(config: IStorageConfig) { this.config = config; } /** * Called before a package is stored * Use this to validate, transform, or prepare for storage */ public async beforeStore(context: plugins.smartregistry.IStorageContext): Promise { // Validate organization exists and has quota const org = await Organization.findById(context.organizationId); if (!org) { throw new Error(`Organization not found: ${context.organizationId}`); } // Check storage quota const newSize = context.size || 0; if (org.settings.quotas.maxStorageBytes > 0) { if (org.usedStorageBytes + newSize > org.settings.quotas.maxStorageBytes) { throw new Error('Organization storage quota exceeded'); } } // Validate repository exists const repo = await Repository.findById(context.repositoryId); if (!repo) { throw new Error(`Repository not found: ${context.repositoryId}`); } // Check repository protocol if (!repo.protocols.includes(context.protocol as TRegistryProtocol)) { throw new Error(`Repository does not support ${context.protocol} protocol`); } return context; } /** * Called after a package is successfully stored * Update database records and metrics */ public async afterStore(context: plugins.smartregistry.IStorageContext): Promise { const protocol = context.protocol as TRegistryProtocol; const packageId = Package.generateId(protocol, context.organizationName, context.packageName); // Get or create package record let pkg = await Package.findById(packageId); if (!pkg) { pkg = new Package(); pkg.id = packageId; pkg.organizationId = context.organizationId; pkg.repositoryId = context.repositoryId; pkg.protocol = protocol; pkg.name = context.packageName; pkg.createdById = context.actorId || ''; pkg.createdAt = new Date(); } // Add version pkg.addVersion({ version: context.version, publishedAt: new Date(), publishedBy: context.actorId || '', size: context.size || 0, checksum: context.checksum || '', checksumAlgorithm: context.checksumAlgorithm || 'sha256', downloads: 0, metadata: context.metadata || {}, }); // Update dist tags if provided if (context.tags) { for (const [tag, version] of Object.entries(context.tags)) { pkg.distTags[tag] = version; } } // Set latest tag if not set if (!pkg.distTags['latest']) { pkg.distTags['latest'] = context.version; } await pkg.save(); // Update organization storage usage const org = await Organization.findById(context.organizationId); if (org) { org.usedStorageBytes += context.size || 0; await org.save(); } // Audit log await AuditService.withContext({ actorId: context.actorId, actorType: context.actorId ? 'user' : 'anonymous', organizationId: context.organizationId, repositoryId: context.repositoryId, }).logPackagePublished( packageId, context.packageName, context.version, context.organizationId, context.repositoryId ); } /** * Called before a package is fetched */ public async beforeFetch(context: plugins.smartregistry.IFetchContext): Promise { return context; } /** * Called after a package is fetched * Update download metrics */ public async afterFetch(context: plugins.smartregistry.IFetchContext): Promise { const protocol = context.protocol as TRegistryProtocol; const packageId = Package.generateId(protocol, context.organizationName, context.packageName); const pkg = await Package.findById(packageId); if (pkg) { await pkg.incrementDownloads(context.version); } // Audit log for authenticated users if (context.actorId) { await AuditService.withContext({ actorId: context.actorId, actorType: 'user', organizationId: context.organizationId, repositoryId: context.repositoryId, }).logPackageDownloaded( packageId, context.packageName, context.version || 'latest', context.organizationId, context.repositoryId ); } } /** * Called before a package is deleted */ public async beforeDelete(context: plugins.smartregistry.IDeleteContext): Promise { return context; } /** * Called after a package is deleted */ public async afterDelete(context: plugins.smartregistry.IDeleteContext): Promise { const protocol = context.protocol as TRegistryProtocol; const packageId = Package.generateId(protocol, context.organizationName, context.packageName); const pkg = await Package.findById(packageId); if (!pkg) return; if (context.version) { // Delete specific version const version = pkg.versions[context.version]; if (version) { const sizeReduction = version.size; delete pkg.versions[context.version]; pkg.storageBytes -= sizeReduction; // Update dist tags for (const [tag, ver] of Object.entries(pkg.distTags)) { if (ver === context.version) { delete pkg.distTags[tag]; } } // If no versions left, delete the package if (Object.keys(pkg.versions).length === 0) { await pkg.delete(); } else { await pkg.save(); } // Update org storage const org = await Organization.findById(context.organizationId); if (org) { org.usedStorageBytes -= sizeReduction; await org.save(); } } } else { // Delete entire package const sizeReduction = pkg.storageBytes; await pkg.delete(); // Update org storage const org = await Organization.findById(context.organizationId); if (org) { org.usedStorageBytes -= sizeReduction; await org.save(); } } // Audit log await AuditService.withContext({ actorId: context.actorId, actorType: context.actorId ? 'user' : 'system', organizationId: context.organizationId, repositoryId: context.repositoryId, }).log('PACKAGE_DELETED', 'package', { resourceId: packageId, resourceName: context.packageName, metadata: { version: context.version }, success: true, }); } /** * Get the S3 path for a package artifact */ public getArtifactPath( protocol: string, organizationName: string, packageName: string, version: string, filename: string ): string { return `${this.config.basePath}/${protocol}/${organizationName}/${packageName}/${version}/${filename}`; } /** * Store artifact in S3 */ public async storeArtifact( path: string, data: Uint8Array, contentType?: string ): Promise { const bucket = await this.config.bucket.getBucket(); await bucket.fastPut({ path, contents: Buffer.from(data), contentType: contentType || 'application/octet-stream', }); return path; } /** * Fetch artifact from S3 */ public async fetchArtifact(path: string): Promise { try { const bucket = await this.config.bucket.getBucket(); const file = await bucket.fastGet({ path }); if (!file) return null; return new Uint8Array(file.contents); } catch { return null; } } /** * Delete artifact from S3 */ public async deleteArtifact(path: string): Promise { try { const bucket = await this.config.bucket.getBucket(); await bucket.fastDelete({ path }); return true; } catch { return false; } } }