This commit is contained in:
2025-11-19 20:45:37 +00:00
parent 754ec7b7db
commit cf891cf275
7 changed files with 683 additions and 199 deletions

View File

@@ -47,11 +47,14 @@ export class NpmRegistry extends BaseRegistry {
public async handleRequest(context: IRequestContext): Promise<IResponse> {
const path = context.path.replace(this.basePath, '');
console.log(`[NPM handleRequest] method=${context.method}, path=${path}`);
// Extract token from Authorization header
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
const tokenString = authHeader?.replace(/^Bearer\s+/i, '');
console.log(`[NPM handleRequest] authHeader=${authHeader}, tokenString=${tokenString}`);
const token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
console.log(`[NPM handleRequest] token validated:`, token);
// Registry root
if (path === '/' || path === '') {
@@ -88,20 +91,38 @@ export class NpmRegistry extends BaseRegistry {
return this.handleTarballDownload(packageName, filename, token);
}
// Package operations: /{package}
const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
if (packageMatch) {
const packageName = packageMatch[1];
return this.handlePackage(context.method, packageName, context.body, context.query, token);
// Unpublish specific version: DELETE /{package}/-/{version}
const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
if (unpublishVersionMatch && context.method === 'DELETE') {
const [, packageName, version] = unpublishVersionMatch;
console.log(`[unpublishVersionMatch] packageName=${packageName}, version=${version}`);
return this.unpublishVersion(packageName, version, token);
}
// Unpublish entire package: DELETE /{package}/-rev/{rev}
const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
if (unpublishPackageMatch && context.method === 'DELETE') {
const [, packageName, rev] = unpublishPackageMatch;
console.log(`[unpublishPackageMatch] packageName=${packageName}, rev=${rev}`);
return this.unpublishPackage(packageName, token);
}
// Package version: /{package}/{version}
const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
if (versionMatch) {
const [, packageName, version] = versionMatch;
console.log(`[versionMatch] matched! packageName=${packageName}, version=${version}`);
return this.handlePackageVersion(packageName, version, token);
}
// Package operations: /{package}
const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
if (packageMatch) {
const packageName = packageMatch[1];
console.log(`[packageMatch] matched! packageName=${packageName}`);
return this.handlePackage(context.method, packageName, context.body, context.query, token);
}
return {
status: 404,
headers: { 'Content-Type': 'application/json' },
@@ -209,7 +230,12 @@ export class NpmRegistry extends BaseRegistry {
version: string,
token: IAuthToken | null
): Promise<IResponse> {
console.log(`[handlePackageVersion] packageName=${packageName}, version=${version}`);
const packument = await this.storage.getNpmPackument(packageName);
console.log(`[handlePackageVersion] packument found:`, !!packument);
if (packument) {
console.log(`[handlePackageVersion] versions:`, Object.keys(packument.versions || {}));
}
if (!packument) {
return {
status: 404,
@@ -252,7 +278,10 @@ export class NpmRegistry extends BaseRegistry {
body: IPublishRequest,
token: IAuthToken | null
): Promise<IResponse> {
if (!await this.checkPermission(token, packageName, 'write')) {
console.log(`[publishPackage] packageName=${packageName}, token=`, token);
const hasPermission = await this.checkPermission(token, packageName, 'write');
console.log(`[publishPackage] hasPermission=${hasPermission}`);
if (!hasPermission) {
return {
status: 401,
headers: {},
@@ -361,6 +390,67 @@ export class NpmRegistry extends BaseRegistry {
};
}
private async unpublishVersion(
packageName: string,
version: string,
token: IAuthToken | null
): Promise<IResponse> {
if (!await this.checkPermission(token, packageName, 'delete')) {
return {
status: 401,
headers: {},
body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
};
}
const packument = await this.storage.getNpmPackument(packageName);
if (!packument) {
return {
status: 404,
headers: {},
body: this.createError('E404', 'Package not found'),
};
}
// Check if version exists
if (!packument.versions[version]) {
return {
status: 404,
headers: {},
body: this.createError('E404', 'Version not found'),
};
}
// Delete tarball
await this.storage.deleteNpmTarball(packageName, version);
// Remove version from packument
delete packument.versions[version];
if (packument.time) {
delete packument.time[version];
packument.time.modified = new Date().toISOString();
}
// Update latest tag if this was the latest version
if (packument['dist-tags']?.latest === version) {
const remainingVersions = Object.keys(packument.versions);
if (remainingVersions.length > 0) {
packument['dist-tags'].latest = remainingVersions[remainingVersions.length - 1];
} else {
delete packument['dist-tags'].latest;
}
}
// Save updated packument
await this.storage.putNpmPackument(packageName, packument);
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: { ok: true },
};
}
private async unpublishPackage(
packageName: string,
token: IAuthToken | null
@@ -438,14 +528,64 @@ export class NpmRegistry extends BaseRegistry {
const size = parseInt(query.size || '20', 10);
const from = parseInt(query.from || '0', 10);
// Simple search implementation (in production, use proper search index)
// Simple search implementation
const results: ISearchResult[] = [];
// For now, return empty results
// In production, implement full-text search across packuments
try {
// List all package paths
const packagePaths = await this.storage.listObjects('npm/packages/');
// Extract unique package names from paths (format: npm/packages/{packageName}/...)
const packageNames = new Set<string>();
for (const path of packagePaths) {
const match = path.match(/^npm\/packages\/([^\/]+)\/index\.json$/);
if (match) {
packageNames.add(match[1]);
}
}
// Load packuments and filter by search text
for (const packageName of packageNames) {
if (!text || packageName.toLowerCase().includes(text.toLowerCase())) {
const packument = await this.storage.getNpmPackument(packageName);
if (packument) {
const latestVersion = packument['dist-tags']?.latest;
const versionData = latestVersion ? packument.versions[latestVersion] : null;
results.push({
package: {
name: packument.name,
version: latestVersion || '0.0.0',
description: packument.description || versionData?.description || '',
keywords: versionData?.keywords || [],
date: packument.time?.modified || new Date().toISOString(),
links: {},
author: versionData?.author || {},
publisher: versionData?._npmUser || {},
maintainers: packument.maintainers || [],
},
score: {
final: 1.0,
detail: {
quality: 1.0,
popularity: 1.0,
maintenance: 1.0,
},
},
searchScore: 1.0,
});
}
}
}
} catch (error) {
console.error('[handleSearch] Error:', error);
}
// Apply pagination
const paginatedResults = results.slice(from, from + size);
const response: ISearchResponse = {
objects: results,
objects: paginatedResults,
total: results.length,
time: new Date().toISOString(),
};
@@ -581,7 +721,7 @@ export class NpmRegistry extends BaseRegistry {
const newToken = await this.authManager.createNpmToken(token.userId, readonly);
return {
status: 201,
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
token: newToken,