/** * Package API handlers */ import type { IApiContext, IApiResponse } from '../router.ts'; import { PermissionService } from '../../services/permission.service.ts'; import { Package, Repository } from '../../models/index.ts'; import type { TRegistryProtocol } from '../../interfaces/auth.interfaces.ts'; export class PackageApi { private permissionService: PermissionService; constructor(permissionService: PermissionService) { this.permissionService = permissionService; } /** * GET /api/v1/packages (search) */ public async search(ctx: IApiContext): Promise { try { const query = ctx.url.searchParams.get('q') || ''; const protocol = ctx.url.searchParams.get('protocol') as TRegistryProtocol | undefined; const organizationId = ctx.url.searchParams.get('organizationId') || undefined; const limit = parseInt(ctx.url.searchParams.get('limit') || '50', 10); const offset = parseInt(ctx.url.searchParams.get('offset') || '0', 10); // For authenticated users, search includes private packages they have access to // For anonymous users, only search public packages const isPrivate = ctx.actor?.userId ? undefined : false; const packages = await Package.search(query, { protocol, organizationId, isPrivate, limit, offset, }); // Filter out packages user doesn't have access to const accessiblePackages = []; for (const pkg of packages) { if (!pkg.isPrivate) { accessiblePackages.push(pkg); continue; } if (ctx.actor?.userId) { const canAccess = await this.permissionService.canAccessPackage( ctx.actor.userId, pkg.organizationId, pkg.repositoryId, 'read' ); if (canAccess) { accessiblePackages.push(pkg); } } } return { status: 200, body: { packages: accessiblePackages.map((pkg) => ({ id: pkg.id, name: pkg.name, description: pkg.description, protocol: pkg.protocol, organizationId: pkg.organizationId, repositoryId: pkg.repositoryId, latestVersion: pkg.distTags['latest'], isPrivate: pkg.isPrivate, downloadCount: pkg.downloadCount, updatedAt: pkg.updatedAt, })), total: accessiblePackages.length, limit, offset, }, }; } catch (error) { console.error('[PackageApi] Search error:', error); return { status: 500, body: { error: 'Failed to search packages' } }; } } /** * GET /api/v1/packages/:id */ public async get(ctx: IApiContext): Promise { const { id } = ctx.params; try { const pkg = await Package.findById(decodeURIComponent(id)); if (!pkg) { return { status: 404, body: { error: 'Package not found' } }; } // Check access if (pkg.isPrivate) { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const canAccess = await this.permissionService.canAccessPackage( ctx.actor.userId, pkg.organizationId, pkg.repositoryId, 'read' ); if (!canAccess) { return { status: 403, body: { error: 'Access denied' } }; } } return { status: 200, body: { id: pkg.id, name: pkg.name, description: pkg.description, protocol: pkg.protocol, organizationId: pkg.organizationId, repositoryId: pkg.repositoryId, distTags: pkg.distTags, versions: Object.keys(pkg.versions), isPrivate: pkg.isPrivate, downloadCount: pkg.downloadCount, starCount: pkg.starCount, storageBytes: pkg.storageBytes, createdAt: pkg.createdAt, updatedAt: pkg.updatedAt, }, }; } catch (error) { console.error('[PackageApi] Get error:', error); return { status: 500, body: { error: 'Failed to get package' } }; } } /** * GET /api/v1/packages/:id/versions */ public async listVersions(ctx: IApiContext): Promise { const { id } = ctx.params; try { const pkg = await Package.findById(decodeURIComponent(id)); if (!pkg) { return { status: 404, body: { error: 'Package not found' } }; } // Check access if (pkg.isPrivate) { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const canAccess = await this.permissionService.canAccessPackage( ctx.actor.userId, pkg.organizationId, pkg.repositoryId, 'read' ); if (!canAccess) { return { status: 403, body: { error: 'Access denied' } }; } } const versions = Object.entries(pkg.versions).map(([version, data]) => ({ version, publishedAt: data.publishedAt, size: data.size, downloads: data.downloads, checksum: data.checksum, })); return { status: 200, body: { packageId: pkg.id, packageName: pkg.name, distTags: pkg.distTags, versions, }, }; } catch (error) { console.error('[PackageApi] List versions error:', error); return { status: 500, body: { error: 'Failed to list versions' } }; } } /** * DELETE /api/v1/packages/:id */ public async delete(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; try { const pkg = await Package.findById(decodeURIComponent(id)); if (!pkg) { return { status: 404, body: { error: 'Package not found' } }; } // Check delete permission const canDelete = await this.permissionService.canAccessPackage( ctx.actor.userId, pkg.organizationId, pkg.repositoryId, 'delete' ); if (!canDelete) { return { status: 403, body: { error: 'Delete permission required' } }; } // Delete the package await pkg.delete(); // Update repository package count const repo = await Repository.findById(pkg.repositoryId); if (repo) { repo.packageCount = Math.max(0, repo.packageCount - 1); repo.storageBytes -= pkg.storageBytes; await repo.save(); } return { status: 200, body: { message: 'Package deleted successfully' }, }; } catch (error) { console.error('[PackageApi] Delete error:', error); return { status: 500, body: { error: 'Failed to delete package' } }; } } /** * DELETE /api/v1/packages/:id/versions/:version */ public async deleteVersion(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id, version } = ctx.params; try { const pkg = await Package.findById(decodeURIComponent(id)); if (!pkg) { return { status: 404, body: { error: 'Package not found' } }; } const versionData = pkg.versions[version]; if (!versionData) { return { status: 404, body: { error: 'Version not found' } }; } // Check delete permission const canDelete = await this.permissionService.canAccessPackage( ctx.actor.userId, pkg.organizationId, pkg.repositoryId, 'delete' ); if (!canDelete) { return { status: 403, body: { error: 'Delete permission required' } }; } // Check if this is the only version if (Object.keys(pkg.versions).length === 1) { return { status: 400, body: { error: 'Cannot delete the only version. Delete the entire package instead.' }, }; } // Remove version const sizeReduction = versionData.size; delete pkg.versions[version]; pkg.storageBytes -= sizeReduction; // Update dist tags for (const [tag, tagVersion] of Object.entries(pkg.distTags)) { if (tagVersion === 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 { status: 200, body: { message: 'Version deleted successfully' }, }; } catch (error) { console.error('[PackageApi] Delete version error:', error); return { status: 500, body: { error: 'Failed to delete version' } }; } } }