feat(registry): add declarative protocol routing and request-scoped storage hook context across registries
This commit is contained in:
+204
-222
@@ -7,7 +7,6 @@ import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
|
||||
import { NpmUpstream } from './classes.npmupstream.js';
|
||||
import type {
|
||||
IPackument,
|
||||
INpmVersion,
|
||||
IPublishRequest,
|
||||
ISearchResponse,
|
||||
ISearchResult,
|
||||
@@ -16,6 +15,13 @@ import type {
|
||||
IUserAuthRequest,
|
||||
INpmError,
|
||||
} from './interfaces.npm.js';
|
||||
import {
|
||||
createNewPackument,
|
||||
getAttachmentForVersion,
|
||||
preparePublishedVersion,
|
||||
recordPublishedVersion,
|
||||
} from './helpers.npmpublish.js';
|
||||
import { parseNpmRequestRoute } from './helpers.npmroutes.js';
|
||||
|
||||
/**
|
||||
* NPM Registry implementation
|
||||
@@ -43,18 +49,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
this.registryUrl = registryUrl;
|
||||
this.upstreamProvider = upstreamProvider || null;
|
||||
|
||||
// Initialize logger
|
||||
this.logger = new Smartlog({
|
||||
logContext: {
|
||||
company: 'push.rocks',
|
||||
companyunit: 'smartregistry',
|
||||
containerName: 'npm-registry',
|
||||
environment: (process.env.NODE_ENV as any) || 'development',
|
||||
runtime: 'node',
|
||||
zone: 'npm'
|
||||
}
|
||||
});
|
||||
this.logger.enableConsole();
|
||||
this.logger = this.createProtocolLogger('npm-registry', 'npm');
|
||||
|
||||
if (upstreamProvider) {
|
||||
this.logger.log('info', 'NPM upstream provider configured');
|
||||
@@ -112,18 +107,10 @@ export class NpmRegistry extends BaseRegistry {
|
||||
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
||||
const path = context.path.replace(this.basePath, '');
|
||||
|
||||
// Extract token from Authorization header
|
||||
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
|
||||
const tokenString = authHeader?.replace(/^Bearer\s+/i, '');
|
||||
const tokenString = this.extractBearerToken(context);
|
||||
const token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
|
||||
|
||||
// Build actor context for upstream resolution
|
||||
const actor: IRequestActor = {
|
||||
userId: token?.userId,
|
||||
ip: context.headers['x-forwarded-for'] || context.headers['x-real-ip'],
|
||||
userAgent: context.headers['user-agent'],
|
||||
...context.actor, // Include any pre-populated actor info
|
||||
};
|
||||
const actor: IRequestActor = this.buildRequestActor(context, token);
|
||||
|
||||
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
||||
method: context.method,
|
||||
@@ -131,78 +118,75 @@ export class NpmRegistry extends BaseRegistry {
|
||||
hasAuth: !!token
|
||||
});
|
||||
|
||||
// Registry root
|
||||
if (path === '/' || path === '') {
|
||||
return this.handleRegistryInfo();
|
||||
}
|
||||
return this.storage.withContext({ protocol: 'npm', actor }, async () => {
|
||||
const route = parseNpmRequestRoute(path, context.method);
|
||||
if (!route) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: this.createError('E404', 'Not found'),
|
||||
};
|
||||
}
|
||||
|
||||
// Search: /-/v1/search
|
||||
if (path.startsWith('/-/v1/search')) {
|
||||
return this.handleSearch(context.query);
|
||||
}
|
||||
switch (route.type) {
|
||||
case 'root':
|
||||
return this.handleRegistryInfo();
|
||||
case 'search':
|
||||
return this.handleSearch(context.query);
|
||||
case 'userAuth':
|
||||
return this.handleUserAuth(context.method, route.username, context.body, token);
|
||||
case 'tokens':
|
||||
return this.handleTokens(context.method, route.path, context.body, token);
|
||||
case 'distTags':
|
||||
return this.withPackageContext(
|
||||
route.packageName,
|
||||
actor,
|
||||
async () => this.handleDistTags(context.method, route.packageName, route.tag, context.body, token)
|
||||
);
|
||||
case 'tarball':
|
||||
return this.handleTarballDownload(route.packageName, route.filename, token, actor);
|
||||
case 'unpublishVersion':
|
||||
this.logger.log('debug', 'unpublishVersionMatch', {
|
||||
packageName: route.packageName,
|
||||
version: route.version,
|
||||
});
|
||||
return this.withPackageVersionContext(
|
||||
route.packageName,
|
||||
route.version,
|
||||
actor,
|
||||
async () => this.unpublishVersion(route.packageName, route.version, token)
|
||||
);
|
||||
case 'unpublishPackage':
|
||||
this.logger.log('debug', 'unpublishPackageMatch', {
|
||||
packageName: route.packageName,
|
||||
rev: route.rev,
|
||||
});
|
||||
return this.withPackageContext(
|
||||
route.packageName,
|
||||
actor,
|
||||
async () => this.unpublishPackage(route.packageName, token)
|
||||
);
|
||||
case 'packageVersion':
|
||||
this.logger.log('debug', 'versionMatch', {
|
||||
packageName: route.packageName,
|
||||
version: route.version,
|
||||
});
|
||||
return this.withPackageVersionContext(
|
||||
route.packageName,
|
||||
route.version,
|
||||
actor,
|
||||
async () => this.handlePackageVersion(route.packageName, route.version, token, actor)
|
||||
);
|
||||
case 'package':
|
||||
this.logger.log('debug', 'packageMatch', { packageName: route.packageName });
|
||||
return this.withPackageContext(
|
||||
route.packageName,
|
||||
actor,
|
||||
async () => this.handlePackage(context.method, route.packageName, context.body, context.query, token, actor)
|
||||
);
|
||||
}
|
||||
|
||||
// User authentication: /-/user/org.couchdb.user:{username}
|
||||
const userMatch = path.match(/^\/-\/user\/org\.couchdb\.user:(.+)$/);
|
||||
if (userMatch) {
|
||||
return this.handleUserAuth(context.method, userMatch[1], context.body, token);
|
||||
}
|
||||
|
||||
// Token operations: /-/npm/v1/tokens
|
||||
if (path.startsWith('/-/npm/v1/tokens')) {
|
||||
return this.handleTokens(context.method, path, context.body, token);
|
||||
}
|
||||
|
||||
// Dist-tags: /-/package/{package}/dist-tags
|
||||
const distTagsMatch = path.match(/^\/-\/package\/(@?[^\/]+(?:\/[^\/]+)?)\/dist-tags(?:\/(.+))?$/);
|
||||
if (distTagsMatch) {
|
||||
const [, rawPkgName, tag] = distTagsMatch;
|
||||
return this.handleDistTags(context.method, decodeURIComponent(rawPkgName), tag, context.body, token);
|
||||
}
|
||||
|
||||
// Tarball download: /{package}/-/{filename}.tgz
|
||||
const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
|
||||
if (tarballMatch) {
|
||||
const [, rawPkgName, filename] = tarballMatch;
|
||||
return this.handleTarballDownload(decodeURIComponent(rawPkgName), filename, token, actor);
|
||||
}
|
||||
|
||||
// Unpublish specific version: DELETE /{package}/-/{version}
|
||||
const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
|
||||
if (unpublishVersionMatch && context.method === 'DELETE') {
|
||||
const [, rawPkgName, version] = unpublishVersionMatch;
|
||||
this.logger.log('debug', 'unpublishVersionMatch', { packageName: decodeURIComponent(rawPkgName), version });
|
||||
return this.unpublishVersion(decodeURIComponent(rawPkgName), version, token);
|
||||
}
|
||||
|
||||
// Unpublish entire package: DELETE /{package}/-rev/{rev}
|
||||
const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
|
||||
if (unpublishPackageMatch && context.method === 'DELETE') {
|
||||
const [, rawPkgName, rev] = unpublishPackageMatch;
|
||||
this.logger.log('debug', 'unpublishPackageMatch', { packageName: decodeURIComponent(rawPkgName), rev });
|
||||
return this.unpublishPackage(decodeURIComponent(rawPkgName), token);
|
||||
}
|
||||
|
||||
// Package version: /{package}/{version}
|
||||
const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
|
||||
if (versionMatch) {
|
||||
const [, rawPkgName, version] = versionMatch;
|
||||
this.logger.log('debug', 'versionMatch', { packageName: decodeURIComponent(rawPkgName), version });
|
||||
return this.handlePackageVersion(decodeURIComponent(rawPkgName), version, token, actor);
|
||||
}
|
||||
|
||||
// Package operations: /{package}
|
||||
const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
|
||||
if (packageMatch) {
|
||||
const packageName = decodeURIComponent(packageMatch[1]);
|
||||
this.logger.log('debug', 'packageMatch', { packageName });
|
||||
return this.handlePackage(context.method, packageName, context.body, context.query, token, actor);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: this.createError('E404', 'Not found'),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected async checkPermission(
|
||||
@@ -268,30 +252,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
query: Record<string, string>,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
let packument = await this.storage.getNpmPackument(packageName);
|
||||
this.logger.log('debug', `getPackument: ${packageName}`, {
|
||||
packageName,
|
||||
found: !!packument,
|
||||
versions: packument ? Object.keys(packument.versions).length : 0
|
||||
});
|
||||
|
||||
// If not found locally, try upstream
|
||||
if (!packument) {
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
|
||||
const upstreamPackument = await upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
this.logger.log('debug', `getPackument: found in upstream`, {
|
||||
packageName,
|
||||
versions: Object.keys(upstreamPackument.versions || {}).length
|
||||
});
|
||||
packument = upstreamPackument;
|
||||
// Optionally cache the packument locally (without tarballs)
|
||||
// We don't store tarballs here - they'll be fetched on demand
|
||||
}
|
||||
}
|
||||
}
|
||||
const packument = await this.getLocalOrUpstreamPackument(packageName, actor, 'getPackument');
|
||||
|
||||
if (!packument) {
|
||||
return {
|
||||
@@ -333,24 +294,12 @@ export class NpmRegistry extends BaseRegistry {
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
this.logger.log('debug', 'handlePackageVersion', { packageName, version });
|
||||
let packument = await this.storage.getNpmPackument(packageName);
|
||||
const packument = await this.getLocalOrUpstreamPackument(packageName, actor, 'handlePackageVersion');
|
||||
this.logger.log('debug', 'handlePackageVersion packument', { found: !!packument });
|
||||
if (packument) {
|
||||
this.logger.log('debug', 'handlePackageVersion versions', { versions: Object.keys(packument.versions || {}) });
|
||||
}
|
||||
|
||||
// If not found locally, try upstream
|
||||
if (!packument) {
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
|
||||
const upstreamPackument = await upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
packument = upstreamPackument;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!packument) {
|
||||
return {
|
||||
status: 404,
|
||||
@@ -424,19 +373,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
const isNew = !packument;
|
||||
|
||||
if (isNew) {
|
||||
packument = {
|
||||
_id: packageName,
|
||||
name: packageName,
|
||||
description: body.description,
|
||||
'dist-tags': body['dist-tags'] || { latest: Object.keys(body.versions)[0] },
|
||||
versions: {},
|
||||
time: {
|
||||
created: new Date().toISOString(),
|
||||
modified: new Date().toISOString(),
|
||||
},
|
||||
maintainers: body.maintainers || [],
|
||||
readme: body.readme,
|
||||
};
|
||||
packument = createNewPackument(packageName, body, new Date().toISOString());
|
||||
}
|
||||
|
||||
// Process each new version
|
||||
@@ -450,12 +387,8 @@ export class NpmRegistry extends BaseRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
// Find attachment for this version
|
||||
const attachmentKey = Object.keys(body._attachments).find(key =>
|
||||
key.includes(version)
|
||||
);
|
||||
|
||||
if (!attachmentKey) {
|
||||
const attachment = getAttachmentForVersion(body, version);
|
||||
if (!attachment) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
@@ -463,38 +396,24 @@ export class NpmRegistry extends BaseRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
const attachment = body._attachments[attachmentKey];
|
||||
|
||||
// Decode base64 tarball
|
||||
const tarballBuffer = Buffer.from(attachment.data, 'base64');
|
||||
|
||||
// Calculate shasum
|
||||
const crypto = await import('crypto');
|
||||
const shasum = crypto.createHash('sha1').update(tarballBuffer).digest('hex');
|
||||
const integrity = `sha512-${crypto.createHash('sha512').update(tarballBuffer).digest('base64')}`;
|
||||
const preparedVersion = preparePublishedVersion({
|
||||
packageName,
|
||||
version,
|
||||
versionData,
|
||||
attachment,
|
||||
registryUrl: this.registryUrl,
|
||||
userId: token?.userId,
|
||||
});
|
||||
|
||||
// Store tarball
|
||||
await this.storage.putNpmTarball(packageName, version, tarballBuffer);
|
||||
await this.withPackageVersionContext(
|
||||
packageName,
|
||||
version,
|
||||
undefined,
|
||||
async () => this.storage.putNpmTarball(packageName, version, preparedVersion.tarballBuffer)
|
||||
);
|
||||
|
||||
// Update version data with dist info
|
||||
const safeName = packageName.replace('@', '').replace('/', '-');
|
||||
versionData.dist = {
|
||||
tarball: `${this.registryUrl}/${packageName}/-/${safeName}-${version}.tgz`,
|
||||
shasum,
|
||||
integrity,
|
||||
fileCount: 0,
|
||||
unpackedSize: tarballBuffer.length,
|
||||
};
|
||||
|
||||
versionData._id = `${packageName}@${version}`;
|
||||
versionData._npmUser = token ? { name: token.userId, email: '' } : undefined;
|
||||
|
||||
// Add version to packument
|
||||
packument.versions[version] = versionData;
|
||||
if (packument.time) {
|
||||
packument.time[version] = new Date().toISOString();
|
||||
packument.time.modified = new Date().toISOString();
|
||||
}
|
||||
recordPublishedVersion(packument, version, preparedVersion.versionData, new Date().toISOString());
|
||||
}
|
||||
|
||||
// Update dist-tags
|
||||
@@ -632,56 +551,119 @@ export class NpmRegistry extends BaseRegistry {
|
||||
|
||||
const version = versionMatch[1];
|
||||
|
||||
// Try local storage first (streaming)
|
||||
const streamResult = await this.storage.getNpmTarballStream(packageName, version);
|
||||
if (streamResult) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': streamResult.size.toString(),
|
||||
},
|
||||
body: streamResult.stream,
|
||||
};
|
||||
}
|
||||
return this.withPackageVersionContext(
|
||||
packageName,
|
||||
version,
|
||||
actor,
|
||||
async (): Promise<IResponse> => {
|
||||
// Try local storage first (streaming)
|
||||
const streamResult = await this.storage.getNpmTarballStream(packageName, version);
|
||||
if (streamResult) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': streamResult.size.toString(),
|
||||
},
|
||||
body: streamResult.stream,
|
||||
};
|
||||
}
|
||||
|
||||
// If not found locally, try upstream
|
||||
let tarball: Buffer | null = null;
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'tarball', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
|
||||
packageName,
|
||||
version,
|
||||
});
|
||||
const upstreamTarball = await upstream.fetchTarball(packageName, version);
|
||||
if (upstreamTarball) {
|
||||
tarball = upstreamTarball;
|
||||
// Cache the tarball locally for future requests
|
||||
await this.storage.putNpmTarball(packageName, version, tarball);
|
||||
this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
|
||||
packageName,
|
||||
version,
|
||||
size: tarball.length,
|
||||
});
|
||||
// If not found locally, try upstream
|
||||
let tarball: Buffer | null = null;
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'tarball', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
|
||||
packageName,
|
||||
version,
|
||||
});
|
||||
const upstreamTarball = await upstream.fetchTarball(packageName, version);
|
||||
if (upstreamTarball) {
|
||||
tarball = upstreamTarball;
|
||||
// Cache the tarball locally for future requests
|
||||
await this.storage.putNpmTarball(packageName, version, tarball);
|
||||
this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
|
||||
packageName,
|
||||
version,
|
||||
size: tarball.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tarball) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'Tarball not found'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': tarball.length.toString(),
|
||||
},
|
||||
body: tarball,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async withPackageContext<T>(
|
||||
packageName: string,
|
||||
actor: IRequestActor | undefined,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
return this.storage.withContext(
|
||||
{ protocol: 'npm', actor, metadata: { packageName } },
|
||||
fn
|
||||
);
|
||||
}
|
||||
|
||||
private async getLocalOrUpstreamPackument(
|
||||
packageName: string,
|
||||
actor: IRequestActor | undefined,
|
||||
logPrefix: string
|
||||
): Promise<IPackument | null> {
|
||||
const localPackument = await this.storage.getNpmPackument(packageName);
|
||||
this.logger.log('debug', `${logPrefix}: ${packageName}`, {
|
||||
packageName,
|
||||
found: !!localPackument,
|
||||
versions: localPackument ? Object.keys(localPackument.versions).length : 0,
|
||||
});
|
||||
|
||||
if (localPackument) {
|
||||
return localPackument;
|
||||
}
|
||||
|
||||
if (!tarball) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'Tarball not found'),
|
||||
};
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
|
||||
if (!upstream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': tarball.length.toString(),
|
||||
},
|
||||
body: tarball,
|
||||
};
|
||||
this.logger.log('debug', `${logPrefix}: fetching from upstream`, { packageName });
|
||||
const upstreamPackument = await upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
this.logger.log('debug', `${logPrefix}: found in upstream`, {
|
||||
packageName,
|
||||
versions: Object.keys(upstreamPackument.versions || {}).length,
|
||||
});
|
||||
}
|
||||
|
||||
return upstreamPackument;
|
||||
}
|
||||
|
||||
private async withPackageVersionContext<T>(
|
||||
packageName: string,
|
||||
version: string,
|
||||
actor: IRequestActor | undefined,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
return this.storage.withContext(
|
||||
{ protocol: 'npm', actor, metadata: { packageName, version } },
|
||||
fn
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSearch(query: Record<string, string>): Promise<IResponse> {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import type { IPackument, IPublishRequest, INpmVersion } from './interfaces.npm.js';
|
||||
|
||||
function getTarballFileName(packageName: string, version: string): string {
|
||||
const safeName = packageName.replace('@', '').replace('/', '-');
|
||||
return `${safeName}-${version}.tgz`;
|
||||
}
|
||||
|
||||
export function createNewPackument(
|
||||
packageName: string,
|
||||
body: IPublishRequest,
|
||||
timestamp: string
|
||||
): IPackument {
|
||||
return {
|
||||
_id: packageName,
|
||||
name: packageName,
|
||||
description: body.description,
|
||||
'dist-tags': body['dist-tags'] || { latest: Object.keys(body.versions)[0] },
|
||||
versions: {},
|
||||
time: {
|
||||
created: timestamp,
|
||||
modified: timestamp,
|
||||
},
|
||||
maintainers: body.maintainers || [],
|
||||
readme: body.readme,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAttachmentForVersion(
|
||||
body: IPublishRequest,
|
||||
version: string
|
||||
): IPublishRequest['_attachments'][string] | null {
|
||||
const attachmentKey = Object.keys(body._attachments).find((key) => key.includes(version));
|
||||
return attachmentKey ? body._attachments[attachmentKey] : null;
|
||||
}
|
||||
|
||||
export function preparePublishedVersion(options: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
versionData: INpmVersion;
|
||||
attachment: IPublishRequest['_attachments'][string];
|
||||
registryUrl: string;
|
||||
userId?: string;
|
||||
}): { tarballBuffer: Buffer; versionData: INpmVersion } {
|
||||
const tarballBuffer = Buffer.from(options.attachment.data, 'base64');
|
||||
const shasum = crypto.createHash('sha1').update(tarballBuffer).digest('hex');
|
||||
const integrity = `sha512-${crypto.createHash('sha512').update(tarballBuffer).digest('base64')}`;
|
||||
const tarballFileName = getTarballFileName(options.packageName, options.version);
|
||||
|
||||
return {
|
||||
tarballBuffer,
|
||||
versionData: {
|
||||
...options.versionData,
|
||||
dist: {
|
||||
...options.versionData.dist,
|
||||
tarball: `${options.registryUrl}/${options.packageName}/-/${tarballFileName}`,
|
||||
shasum,
|
||||
integrity,
|
||||
fileCount: 0,
|
||||
unpackedSize: tarballBuffer.length,
|
||||
},
|
||||
_id: `${options.packageName}@${options.version}`,
|
||||
...(options.userId ? { _npmUser: { name: options.userId, email: '' } } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function recordPublishedVersion(
|
||||
packument: IPackument,
|
||||
version: string,
|
||||
versionData: INpmVersion,
|
||||
timestamp: string
|
||||
): void {
|
||||
packument.versions[version] = versionData;
|
||||
if (packument.time) {
|
||||
packument.time[version] = timestamp;
|
||||
packument.time.modified = timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
export type TNpmRequestRoute =
|
||||
| { type: 'root' }
|
||||
| { type: 'search' }
|
||||
| { type: 'userAuth'; username: string }
|
||||
| { type: 'tokens'; path: string }
|
||||
| { type: 'distTags'; packageName: string; tag?: string }
|
||||
| { type: 'tarball'; packageName: string; filename: string }
|
||||
| { type: 'unpublishVersion'; packageName: string; version: string }
|
||||
| { type: 'unpublishPackage'; packageName: string; rev: string }
|
||||
| { type: 'packageVersion'; packageName: string; version: string }
|
||||
| { type: 'package'; packageName: string };
|
||||
|
||||
function decodePackageName(rawPackageName: string): string {
|
||||
return decodeURIComponent(rawPackageName);
|
||||
}
|
||||
|
||||
export function parseNpmRequestRoute(path: string, method: string): TNpmRequestRoute | null {
|
||||
if (path === '/' || path === '') {
|
||||
return { type: 'root' };
|
||||
}
|
||||
|
||||
if (path.startsWith('/-/v1/search')) {
|
||||
return { type: 'search' };
|
||||
}
|
||||
|
||||
const userMatch = path.match(/^\/-\/user\/org\.couchdb\.user:(.+)$/);
|
||||
if (userMatch) {
|
||||
return {
|
||||
type: 'userAuth',
|
||||
username: userMatch[1],
|
||||
};
|
||||
}
|
||||
|
||||
if (path.startsWith('/-/npm/v1/tokens')) {
|
||||
return {
|
||||
type: 'tokens',
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
const distTagsMatch = path.match(/^\/-\/package\/(@?[^\/]+(?:\/[^\/]+)?)\/dist-tags(?:\/(.+))?$/);
|
||||
if (distTagsMatch) {
|
||||
const [, rawPackageName, tag] = distTagsMatch;
|
||||
return {
|
||||
type: 'distTags',
|
||||
packageName: decodePackageName(rawPackageName),
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
||||
const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
|
||||
if (tarballMatch) {
|
||||
const [, rawPackageName, filename] = tarballMatch;
|
||||
return {
|
||||
type: 'tarball',
|
||||
packageName: decodePackageName(rawPackageName),
|
||||
filename,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
|
||||
if (unpublishVersionMatch) {
|
||||
const [, rawPackageName, version] = unpublishVersionMatch;
|
||||
return {
|
||||
type: 'unpublishVersion',
|
||||
packageName: decodePackageName(rawPackageName),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
|
||||
if (unpublishPackageMatch) {
|
||||
const [, rawPackageName, rev] = unpublishPackageMatch;
|
||||
return {
|
||||
type: 'unpublishPackage',
|
||||
packageName: decodePackageName(rawPackageName),
|
||||
rev,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const unencodedScopedPackageMatch = path.match(/^\/@[^\/]+\/[^\/]+$/);
|
||||
if (unencodedScopedPackageMatch) {
|
||||
return {
|
||||
type: 'package',
|
||||
packageName: decodePackageName(path.substring(1)),
|
||||
};
|
||||
}
|
||||
|
||||
const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
|
||||
if (versionMatch) {
|
||||
const [, rawPackageName, version] = versionMatch;
|
||||
return {
|
||||
type: 'packageVersion',
|
||||
packageName: decodePackageName(rawPackageName),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
|
||||
if (packageMatch) {
|
||||
return {
|
||||
type: 'package',
|
||||
packageName: decodePackageName(packageMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user