import * as plugins from '../../plugins.ts'; import * as interfaces from '../../../ts_interfaces/index.ts'; import type { OpsServer } from '../classes.opsserver.ts'; import { requireValidIdentity } from '../helpers/guards.ts'; import { Package, Repository } from '../../models/index.ts'; import { PermissionService } from '../../services/permission.service.ts'; export class PackageHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); private permissionService = new PermissionService(); constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } private registerHandlers(): void { // Search Packages this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'searchPackages', async (dataArg) => { try { const query = dataArg.query || ''; const protocol = dataArg.protocol; const organizationId = dataArg.organizationId; const limit = dataArg.limit || 50; const offset = dataArg.offset || 0; // Determine visibility: anonymous users see only public packages const hasIdentity = !!dataArg.identity?.jwt; const isPrivate = hasIdentity ? undefined : false; const packages = await Package.searchPackages(query, { protocol, organizationId, isPrivate, limit, offset, }); // Filter out packages user doesn't have access to const accessiblePackages: typeof packages = []; for (const pkg of packages) { if (!pkg.isPrivate) { accessiblePackages.push(pkg); continue; } if (hasIdentity && dataArg.identity) { const canAccess = await this.permissionService.canAccessPackage( dataArg.identity.userId, pkg.organizationId, pkg.repositoryId, 'read', ); if (canAccess) { accessiblePackages.push(pkg); } } } return { packages: accessiblePackages.map((pkg) => ({ id: pkg.id, name: pkg.name, description: pkg.description, protocol: pkg.protocol as interfaces.data.TRegistryProtocol, organizationId: pkg.organizationId, repositoryId: pkg.repositoryId, latestVersion: pkg.distTags?.['latest'], isPrivate: pkg.isPrivate, downloadCount: pkg.downloadCount || 0, starCount: pkg.starCount || 0, storageBytes: pkg.storageBytes || 0, updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt), createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt), })), total: accessiblePackages.length, limit, offset, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to search packages'); } }, ), ); // Get Package this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getPackage', async (dataArg) => { try { const pkg = await Package.findById(dataArg.packageId); if (!pkg) { throw new plugins.typedrequest.TypedResponseError('Package not found'); } // Check access for private packages if (pkg.isPrivate) { if (!dataArg.identity?.jwt) { throw new plugins.typedrequest.TypedResponseError('Authentication required'); } const canAccess = await this.permissionService.canAccessPackage( dataArg.identity.userId, pkg.organizationId, pkg.repositoryId, 'read', ); if (!canAccess) { throw new plugins.typedrequest.TypedResponseError('Access denied'); } } return { package: { id: pkg.id, name: pkg.name, description: pkg.description, protocol: pkg.protocol as interfaces.data.TRegistryProtocol, organizationId: pkg.organizationId, repositoryId: pkg.repositoryId, latestVersion: pkg.distTags?.['latest'], isPrivate: pkg.isPrivate, downloadCount: pkg.downloadCount || 0, starCount: pkg.starCount || 0, storageBytes: pkg.storageBytes || 0, distTags: pkg.distTags || {}, versions: Object.keys(pkg.versions || {}), updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt), createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt), }, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to get package'); } }, ), ); // Get Package Versions this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getPackageVersions', async (dataArg) => { try { const pkg = await Package.findById(dataArg.packageId); if (!pkg) { throw new plugins.typedrequest.TypedResponseError('Package not found'); } // Check access for private packages if (pkg.isPrivate) { if (!dataArg.identity?.jwt) { throw new plugins.typedrequest.TypedResponseError('Authentication required'); } const canAccess = await this.permissionService.canAccessPackage( dataArg.identity.userId, pkg.organizationId, pkg.repositoryId, 'read', ); if (!canAccess) { throw new plugins.typedrequest.TypedResponseError('Access denied'); } } const versions = Object.entries(pkg.versions || {}).map(([version, data]) => ({ version, publishedAt: data.publishedAt instanceof Date ? data.publishedAt.toISOString() : String(data.publishedAt || ''), size: data.size || 0, downloads: data.downloads || 0, checksum: data.metadata?.checksum as interfaces.data.IPackageVersion['checksum'], })); return { packageId: pkg.id, packageName: pkg.name, distTags: pkg.distTags || {}, versions, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to list versions'); } }, ), ); // Delete Package this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deletePackage', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const pkg = await Package.findById(dataArg.packageId); if (!pkg) { throw new plugins.typedrequest.TypedResponseError('Package not found'); } // Check delete permission const canDelete = await this.permissionService.canAccessPackage( dataArg.identity.userId, pkg.organizationId, pkg.repositoryId, 'delete', ); if (!canDelete) { throw new plugins.typedrequest.TypedResponseError('Delete permission required'); } // Update repository counts before deleting const repo = await Repository.findById(pkg.repositoryId); if (repo) { repo.packageCount = Math.max(0, repo.packageCount - 1); repo.storageBytes -= pkg.storageBytes; await repo.save(); } await pkg.delete(); return { message: 'Package deleted successfully' }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to delete package'); } }, ), ); // Delete Package Version this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deletePackageVersion', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const pkg = await Package.findById(dataArg.packageId); if (!pkg) { throw new plugins.typedrequest.TypedResponseError('Package not found'); } const versionData = pkg.versions?.[dataArg.version]; if (!versionData) { throw new plugins.typedrequest.TypedResponseError('Version not found'); } // Check delete permission const canDelete = await this.permissionService.canAccessPackage( dataArg.identity.userId, pkg.organizationId, pkg.repositoryId, 'delete', ); if (!canDelete) { throw new plugins.typedrequest.TypedResponseError('Delete permission required'); } // Check if this is the only version if (Object.keys(pkg.versions).length === 1) { throw new plugins.typedrequest.TypedResponseError( 'Cannot delete the only version. Delete the entire package instead.', ); } // Remove version const sizeReduction = versionData.size || 0; delete pkg.versions[dataArg.version]; pkg.storageBytes -= sizeReduction; // Update dist tags for (const [tag, tagVersion] of Object.entries(pkg.distTags || {})) { if (tagVersion === dataArg.version) { delete pkg.distTags[tag]; } } // Set new latest if needed if (!pkg.distTags['latest'] && Object.keys(pkg.versions).length > 0) { const versions = Object.keys(pkg.versions).sort(); pkg.distTags['latest'] = versions[versions.length - 1]; } await pkg.save(); // Update repository storage const repo = await Repository.findById(pkg.repositoryId); if (repo) { repo.storageBytes -= sizeReduction; await repo.save(); } return { message: 'Version deleted successfully' }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to delete version'); } }, ), ); } }