multi registry support v3
This commit is contained in:
707
ts/npm/classes.npmregistry.ts
Normal file
707
ts/npm/classes.npmregistry.ts
Normal file
@@ -0,0 +1,707 @@
|
||||
import { BaseRegistry } from '../core/classes.baseregistry.js';
|
||||
import { RegistryStorage } from '../core/classes.registrystorage.js';
|
||||
import { AuthManager } from '../core/classes.authmanager.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
||||
import type {
|
||||
IPackument,
|
||||
INpmVersion,
|
||||
IPublishRequest,
|
||||
ISearchResponse,
|
||||
ISearchResult,
|
||||
ITokenListResponse,
|
||||
ITokenCreateRequest,
|
||||
IUserAuthRequest,
|
||||
INpmError,
|
||||
} from './interfaces.npm.js';
|
||||
|
||||
/**
|
||||
* NPM Registry implementation
|
||||
* Compliant with npm registry API
|
||||
*/
|
||||
export class NpmRegistry extends BaseRegistry {
|
||||
private storage: RegistryStorage;
|
||||
private authManager: AuthManager;
|
||||
private basePath: string = '/npm';
|
||||
private registryUrl: string;
|
||||
|
||||
constructor(
|
||||
storage: RegistryStorage,
|
||||
authManager: AuthManager,
|
||||
basePath: string = '/npm',
|
||||
registryUrl: string = 'http://localhost:5000/npm'
|
||||
) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.authManager = authManager;
|
||||
this.basePath = basePath;
|
||||
this.registryUrl = registryUrl;
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
// NPM registry initialization
|
||||
}
|
||||
|
||||
public getBasePath(): string {
|
||||
return this.basePath;
|
||||
}
|
||||
|
||||
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 token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
|
||||
|
||||
// Registry root
|
||||
if (path === '/' || path === '') {
|
||||
return this.handleRegistryInfo();
|
||||
}
|
||||
|
||||
// Search: /-/v1/search
|
||||
if (path.startsWith('/-/v1/search')) {
|
||||
return this.handleSearch(context.query);
|
||||
}
|
||||
|
||||
// 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 [, packageName, tag] = distTagsMatch;
|
||||
return this.handleDistTags(context.method, packageName, tag, context.body, token);
|
||||
}
|
||||
|
||||
// Tarball download: /{package}/-/{filename}.tgz
|
||||
const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
|
||||
if (tarballMatch) {
|
||||
const [, packageName, filename] = tarballMatch;
|
||||
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);
|
||||
}
|
||||
|
||||
// Package version: /{package}/{version}
|
||||
const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
|
||||
if (versionMatch) {
|
||||
const [, packageName, version] = versionMatch;
|
||||
return this.handlePackageVersion(packageName, version, token);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: this.createError('E404', 'Not found'),
|
||||
};
|
||||
}
|
||||
|
||||
protected async checkPermission(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
if (!token) return false;
|
||||
return this.authManager.authorize(token, `npm:package:${resource}`, action);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// REQUEST HANDLERS
|
||||
// ========================================================================
|
||||
|
||||
private handleRegistryInfo(): IResponse {
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
db_name: 'registry',
|
||||
doc_count: 0,
|
||||
doc_del_count: 0,
|
||||
update_seq: 0,
|
||||
purge_seq: 0,
|
||||
compact_running: false,
|
||||
disk_size: 0,
|
||||
data_size: 0,
|
||||
instance_start_time: Date.now().toString(),
|
||||
disk_format_version: 0,
|
||||
committed_update_seq: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async handlePackage(
|
||||
method: string,
|
||||
packageName: string,
|
||||
body: any,
|
||||
query: Record<string, string>,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return this.getPackument(packageName, token, query);
|
||||
case 'PUT':
|
||||
return this.publishPackage(packageName, body, token);
|
||||
case 'DELETE':
|
||||
return this.unpublishPackage(packageName, token);
|
||||
default:
|
||||
return {
|
||||
status: 405,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Method not allowed'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getPackument(
|
||||
packageName: string,
|
||||
token: IAuthToken | null,
|
||||
query: Record<string, string>
|
||||
): Promise<IResponse> {
|
||||
const packument = await this.storage.getNpmPackument(packageName);
|
||||
if (!packument) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: this.createError('E404', `Package '${packageName}' not found`),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if abbreviated version requested
|
||||
const accept = query['accept'] || '';
|
||||
if (accept.includes('application/vnd.npm.install-v1+json')) {
|
||||
// Return abbreviated packument
|
||||
const abbreviated = {
|
||||
name: packument.name,
|
||||
modified: packument.time?.modified || new Date().toISOString(),
|
||||
'dist-tags': packument['dist-tags'],
|
||||
versions: packument.versions,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/vnd.npm.install-v1+json' },
|
||||
body: abbreviated,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: packument,
|
||||
};
|
||||
}
|
||||
|
||||
private async handlePackageVersion(
|
||||
packageName: string,
|
||||
version: string,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
const packument = await this.storage.getNpmPackument(packageName);
|
||||
if (!packument) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: this.createError('E404', 'Package not found'),
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve version (could be "latest" or actual version)
|
||||
let actualVersion = version;
|
||||
if (version === 'latest') {
|
||||
actualVersion = packument['dist-tags']?.latest;
|
||||
if (!actualVersion) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'No latest version'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const versionData = packument.versions[actualVersion];
|
||||
if (!versionData) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'Version not found'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: versionData,
|
||||
};
|
||||
}
|
||||
|
||||
private async publishPackage(
|
||||
packageName: string,
|
||||
body: IPublishRequest,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
if (!await this.checkPermission(token, packageName, 'write')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {},
|
||||
body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!body || !body.versions || !body._attachments) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Invalid publish request'),
|
||||
};
|
||||
}
|
||||
|
||||
// Get existing packument or create new one
|
||||
let packument = await this.storage.getNpmPackument(packageName);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// Process each new version
|
||||
for (const [version, versionData] of Object.entries(body.versions)) {
|
||||
// Check if version already exists
|
||||
if (packument.versions[version]) {
|
||||
return {
|
||||
status: 403,
|
||||
headers: {},
|
||||
body: this.createError('EPUBLISHCONFLICT', `Version ${version} already exists`),
|
||||
};
|
||||
}
|
||||
|
||||
// Find attachment for this version
|
||||
const attachmentKey = Object.keys(body._attachments).find(key =>
|
||||
key.includes(version)
|
||||
);
|
||||
|
||||
if (!attachmentKey) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', `No tarball for version ${version}`),
|
||||
};
|
||||
}
|
||||
|
||||
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')}`;
|
||||
|
||||
// Store tarball
|
||||
await this.storage.putNpmTarball(packageName, version, 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Update dist-tags
|
||||
if (body['dist-tags']) {
|
||||
packument['dist-tags'] = { ...packument['dist-tags'], ...body['dist-tags'] };
|
||||
}
|
||||
|
||||
// Save packument
|
||||
await this.storage.putNpmPackument(packageName, packument);
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { ok: true, id: packageName, rev: packument._rev || '1-' + Date.now() },
|
||||
};
|
||||
}
|
||||
|
||||
private async unpublishPackage(
|
||||
packageName: 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'),
|
||||
};
|
||||
}
|
||||
|
||||
// Delete all tarballs
|
||||
for (const version of Object.keys(packument.versions)) {
|
||||
await this.storage.deleteNpmTarball(packageName, version);
|
||||
}
|
||||
|
||||
// Delete packument
|
||||
await this.storage.deleteNpmPackument(packageName);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { ok: true },
|
||||
};
|
||||
}
|
||||
|
||||
private async handleTarballDownload(
|
||||
packageName: string,
|
||||
filename: string,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
// Extract version from filename: package-name-1.0.0.tgz
|
||||
const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/);
|
||||
if (!versionMatch) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Invalid tarball filename'),
|
||||
};
|
||||
}
|
||||
|
||||
const version = versionMatch[1];
|
||||
const tarball = await this.storage.getNpmTarball(packageName, version);
|
||||
|
||||
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 handleSearch(query: Record<string, string>): Promise<IResponse> {
|
||||
const text = query.text || '';
|
||||
const size = parseInt(query.size || '20', 10);
|
||||
const from = parseInt(query.from || '0', 10);
|
||||
|
||||
// Simple search implementation (in production, use proper search index)
|
||||
const results: ISearchResult[] = [];
|
||||
|
||||
// For now, return empty results
|
||||
// In production, implement full-text search across packuments
|
||||
|
||||
const response: ISearchResponse = {
|
||||
objects: results,
|
||||
total: results.length,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: response,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleUserAuth(
|
||||
method: string,
|
||||
username: string,
|
||||
body: IUserAuthRequest,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
if (method !== 'PUT') {
|
||||
return {
|
||||
status: 405,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Method not allowed'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!body || !body.name || !body.password) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Invalid request'),
|
||||
};
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
const userId = await this.authManager.authenticate({
|
||||
username: body.name,
|
||||
password: body.password,
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {},
|
||||
body: this.createError('EUNAUTHORIZED', 'Invalid credentials'),
|
||||
};
|
||||
}
|
||||
|
||||
// Create NPM token
|
||||
const npmToken = await this.authManager.createNpmToken(userId, false);
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
ok: true,
|
||||
id: `org.couchdb.user:${username}`,
|
||||
rev: '1-' + Date.now(),
|
||||
token: npmToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async handleTokens(
|
||||
method: string,
|
||||
path: string,
|
||||
body: any,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
if (!token) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {},
|
||||
body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
|
||||
};
|
||||
}
|
||||
|
||||
// List tokens: GET /-/npm/v1/tokens
|
||||
if (path === '/-/npm/v1/tokens' && method === 'GET') {
|
||||
return this.listTokens(token);
|
||||
}
|
||||
|
||||
// Create token: POST /-/npm/v1/tokens
|
||||
if (path === '/-/npm/v1/tokens' && method === 'POST') {
|
||||
return this.createToken(body, token);
|
||||
}
|
||||
|
||||
// Delete token: DELETE /-/npm/v1/tokens/token/{key}
|
||||
const deleteMatch = path.match(/^\/-\/npm\/v1\/tokens\/token\/(.+)$/);
|
||||
if (deleteMatch && method === 'DELETE') {
|
||||
return this.deleteToken(deleteMatch[1], token);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'Not found'),
|
||||
};
|
||||
}
|
||||
|
||||
private async listTokens(token: IAuthToken): Promise<IResponse> {
|
||||
const tokens = await this.authManager.listUserTokens(token.userId);
|
||||
|
||||
const response: ITokenListResponse = {
|
||||
objects: tokens.map(t => ({
|
||||
token: '********',
|
||||
key: t.key,
|
||||
readonly: t.readonly,
|
||||
created: t.created,
|
||||
updated: t.created,
|
||||
})),
|
||||
total: tokens.length,
|
||||
urls: {},
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: response,
|
||||
};
|
||||
}
|
||||
|
||||
private async createToken(body: ITokenCreateRequest, token: IAuthToken): Promise<IResponse> {
|
||||
if (!body || !body.password) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Password required'),
|
||||
};
|
||||
}
|
||||
|
||||
// Verify password (simplified - in production, verify against stored password)
|
||||
const readonly = body.readonly || false;
|
||||
const newToken = await this.authManager.createNpmToken(token.userId, readonly);
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
token: newToken,
|
||||
key: 'sha512-' + newToken.substring(0, 16) + '...',
|
||||
cidr_whitelist: body.cidr_whitelist || [],
|
||||
readonly,
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteToken(key: string, token: IAuthToken): Promise<IResponse> {
|
||||
// In production, lookup token by key hash and delete
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { ok: true },
|
||||
};
|
||||
}
|
||||
|
||||
private async handleDistTags(
|
||||
method: string,
|
||||
packageName: string,
|
||||
tag: string | undefined,
|
||||
body: any,
|
||||
token: IAuthToken | null
|
||||
): Promise<IResponse> {
|
||||
const packument = await this.storage.getNpmPackument(packageName);
|
||||
if (!packument) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {},
|
||||
body: this.createError('E404', 'Package not found'),
|
||||
};
|
||||
}
|
||||
|
||||
// GET /-/package/{package}/dist-tags
|
||||
if (method === 'GET' && !tag) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: packument['dist-tags'] || {},
|
||||
};
|
||||
}
|
||||
|
||||
// PUT /-/package/{package}/dist-tags/{tag}
|
||||
if (method === 'PUT' && tag) {
|
||||
if (!await this.checkPermission(token, packageName, 'write')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {},
|
||||
body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof body !== 'string') {
|
||||
return {
|
||||
status: 400,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Version string required'),
|
||||
};
|
||||
}
|
||||
|
||||
packument['dist-tags'] = packument['dist-tags'] || {};
|
||||
packument['dist-tags'][tag] = body;
|
||||
await this.storage.putNpmPackument(packageName, packument);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { ok: true },
|
||||
};
|
||||
}
|
||||
|
||||
// DELETE /-/package/{package}/dist-tags/{tag}
|
||||
if (method === 'DELETE' && tag) {
|
||||
if (!await this.checkPermission(token, packageName, 'write')) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {},
|
||||
body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
|
||||
};
|
||||
}
|
||||
|
||||
if (tag === 'latest') {
|
||||
return {
|
||||
status: 403,
|
||||
headers: {},
|
||||
body: this.createError('EFORBIDDEN', 'Cannot delete latest tag'),
|
||||
};
|
||||
}
|
||||
|
||||
if (packument['dist-tags'] && packument['dist-tags'][tag]) {
|
||||
delete packument['dist-tags'][tag];
|
||||
await this.storage.putNpmPackument(packageName, packument);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { ok: true },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 405,
|
||||
headers: {},
|
||||
body: this.createError('EBADREQUEST', 'Method not allowed'),
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// HELPER METHODS
|
||||
// ========================================================================
|
||||
|
||||
private createError(code: string, message: string): INpmError {
|
||||
return {
|
||||
error: code,
|
||||
reason: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
6
ts/npm/index.ts
Normal file
6
ts/npm/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* NPM Registry module exports
|
||||
*/
|
||||
|
||||
export { NpmRegistry } from './classes.npmregistry.js';
|
||||
export * from './interfaces.npm.js';
|
||||
Reference in New Issue
Block a user