mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-20 22:52:46 -05:00
8c730c0694
* refactor: max-len and printWidth to 100 * chore: ci specific pnpm version * fix: add types peer package literally get rid of types package
603 lines
15 KiB
TypeScript
603 lines
15 KiB
TypeScript
import fs from 'fs';
|
|
import assert from 'assert';
|
|
import URL from 'url';
|
|
import { IncomingHttpHeaders } from 'http';
|
|
import _ from 'lodash';
|
|
import semver from 'semver';
|
|
import YAML from 'js-yaml';
|
|
import { Request } from 'express';
|
|
|
|
import sanitizyReadme from '@verdaccio/readme';
|
|
import {
|
|
APP_ERROR,
|
|
DEFAULT_PORT,
|
|
DEFAULT_DOMAIN,
|
|
DEFAULT_PROTOCOL,
|
|
CHARACTER_ENCODING,
|
|
HEADERS,
|
|
DIST_TAGS,
|
|
DEFAULT_USER,
|
|
} from '@verdaccio/dev-commons';
|
|
|
|
import { Package, Version, Author, StringValue } from '@verdaccio/types';
|
|
|
|
import {
|
|
getConflict,
|
|
getBadData,
|
|
getBadRequest,
|
|
getInternalError,
|
|
getUnauthorized,
|
|
getForbidden,
|
|
getServiceUnavailable,
|
|
getNotFound,
|
|
getCode,
|
|
} from '@verdaccio/commons-api';
|
|
|
|
// FIXME: this is fixed, should pick the package.json or official version
|
|
const pkgVersion = '5.0.0';
|
|
const pkgName = 'verdaccio';
|
|
|
|
export function getUserAgent(): string {
|
|
assert(_.isString(pkgName));
|
|
assert(_.isString(pkgVersion));
|
|
return `${pkgName}/${pkgVersion}`;
|
|
}
|
|
|
|
export function convertPayloadToBase64(payload: string): Buffer {
|
|
return Buffer.from(payload, 'base64');
|
|
}
|
|
|
|
/**
|
|
* From normalize-package-data/lib/fixer.js
|
|
* @param {*} name the package name
|
|
* @return {Boolean} whether is valid or not
|
|
*/
|
|
export function validateName(name: string): boolean {
|
|
if (_.isString(name) === false) {
|
|
return false;
|
|
}
|
|
|
|
const normalizedName: string = name.toLowerCase();
|
|
|
|
/**
|
|
* Some context about the first regex
|
|
* - npm used to have a different tarball naming system.
|
|
* eg: http://registry.npmjs.com/thirty-two
|
|
* https://registry.npmjs.org/thirty-two/-/thirty-two@0.0.1.tgz
|
|
* The file name thirty-two@0.0.1.tgz, the version and the pkg name was separated by an at (@)
|
|
* while nowadays the naming system is based in dashes
|
|
* https://registry.npmjs.org/verdaccio/-/verdaccio-1.4.0.tgz
|
|
*
|
|
* more info here: https://github.com/rlidwka/sinopia/issues/75
|
|
*/
|
|
return !(
|
|
!normalizedName.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) ||
|
|
normalizedName.startsWith('.') || // ".bin", etc.
|
|
['node_modules', '__proto__', 'favicon.ico'].includes(normalizedName)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Validate a package.
|
|
* @return {Boolean} whether the package is valid or not
|
|
*/
|
|
export function validatePackage(name: string): boolean {
|
|
const nameList = name.split('/', 2);
|
|
if (nameList.length === 1) {
|
|
// normal package
|
|
return validateName(nameList[0]);
|
|
}
|
|
// scoped package
|
|
return nameList[0][0] === '@' && validateName(nameList[0].slice(1)) && validateName(nameList[1]);
|
|
}
|
|
|
|
/**
|
|
* Check whether an element is an Object
|
|
* @param {*} obj the element
|
|
* @return {Boolean}
|
|
*/
|
|
export function isObject(obj: any): boolean {
|
|
return _.isObject(obj) && _.isNull(obj) === false && _.isArray(obj) === false;
|
|
}
|
|
|
|
/**
|
|
* Validate the package metadata, add additional properties whether are missing within
|
|
* the metadata properties.
|
|
* @param {*} object
|
|
* @param {*} name
|
|
* @return {Object} the object with additional properties as dist-tags ad versions
|
|
*/
|
|
export function validateMetadata(object: Package, name: string): Package {
|
|
assert(isObject(object), 'not a json object');
|
|
assert.strictEqual(object.name, name);
|
|
|
|
if (!isObject(object[DIST_TAGS])) {
|
|
object[DIST_TAGS] = {};
|
|
}
|
|
|
|
if (!isObject(object['versions'])) {
|
|
object['versions'] = {};
|
|
}
|
|
|
|
if (!isObject(object['time'])) {
|
|
object['time'] = {};
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* Create base url for registry.
|
|
* @return {String} base registry url
|
|
*/
|
|
export function combineBaseUrl(
|
|
protocol: string,
|
|
host: string | void,
|
|
prefix?: string | void
|
|
): string {
|
|
const result = `${protocol}://${host}`;
|
|
|
|
const prefixOnlySlash = prefix === '/';
|
|
if (prefix && !prefixOnlySlash) {
|
|
if (prefix.endsWith('/')) {
|
|
prefix = prefix.slice(0, -1);
|
|
}
|
|
|
|
if (prefix.startsWith('/')) {
|
|
return `${result}${prefix}`;
|
|
}
|
|
|
|
return prefix;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function extractTarballFromUrl(url: string): string {
|
|
// @ts-ignore
|
|
return URL.parse(url).pathname.replace(/^.*\//, '');
|
|
}
|
|
|
|
/**
|
|
* Iterate a packages's versions and filter each original tarball url.
|
|
* @param {*} pkg
|
|
* @param {*} req
|
|
* @param {*} config
|
|
* @return {String} a filtered package
|
|
*/
|
|
export function convertDistRemoteToLocalTarballUrls(
|
|
pkg: Package,
|
|
req: Request,
|
|
urlPrefix: string | void
|
|
): Package {
|
|
for (const ver in pkg.versions) {
|
|
if (Object.prototype.hasOwnProperty.call(pkg.versions, ver)) {
|
|
const distName = pkg.versions[ver].dist;
|
|
|
|
if (_.isNull(distName) === false && _.isNull(distName.tarball) === false) {
|
|
distName.tarball = getLocalRegistryTarballUri(distName.tarball, pkg.name, req, urlPrefix);
|
|
}
|
|
}
|
|
}
|
|
return pkg;
|
|
}
|
|
|
|
/**
|
|
* Filter a tarball url.
|
|
* @param {*} uri
|
|
* @return {String} a parsed url
|
|
*/
|
|
export function getLocalRegistryTarballUri(
|
|
uri: string,
|
|
pkgName: string,
|
|
req: Request,
|
|
urlPrefix: string | void
|
|
): string {
|
|
const currentHost = req.headers.host;
|
|
|
|
if (!currentHost) {
|
|
return uri;
|
|
}
|
|
const tarballName = extractTarballFromUrl(uri);
|
|
const headers = req.headers as IncomingHttpHeaders;
|
|
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
|
const domainRegistry = combineBaseUrl(protocol, headers.host, urlPrefix);
|
|
|
|
return `${domainRegistry}/${encodeScopedUri(pkgName)}/-/${tarballName}`;
|
|
}
|
|
|
|
/**
|
|
* Create a tag for a package
|
|
* @param {*} data
|
|
* @param {*} version
|
|
* @param {*} tag
|
|
* @return {Boolean} whether a package has been tagged
|
|
*/
|
|
export function tagVersion(data: Package, version: string, tag: StringValue): boolean {
|
|
if (tag && data[DIST_TAGS][tag] !== version && semver.parse(version, true)) {
|
|
// valid version - store
|
|
data[DIST_TAGS][tag] = version;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets version from a package object taking into account semver weirdness.
|
|
* @return {String} return the semantic version of a package
|
|
*/
|
|
export function getVersion(pkg: Package, version: any): Version | void {
|
|
// this condition must allow cast
|
|
if (_.isNil(pkg.versions[version]) === false) {
|
|
return pkg.versions[version];
|
|
}
|
|
|
|
try {
|
|
version = semver.parse(version, true);
|
|
for (const versionItem in pkg.versions) {
|
|
if (version.compare(semver.parse(versionItem, true)) === 0) {
|
|
return pkg.versions[versionItem];
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse an internet address
|
|
* Allow:
|
|
- https:localhost:1234 - protocol + host + port
|
|
- localhost:1234 - host + port
|
|
- 1234 - port
|
|
- http::1234 - protocol + port
|
|
- https://localhost:443/ - full url + https
|
|
- http://[::1]:443/ - ipv6
|
|
- unix:/tmp/http.sock - unix sockets
|
|
- https://unix:/tmp/http.sock - unix sockets (https)
|
|
* @param {*} urlAddress the internet address definition
|
|
* @return {Object|Null} literal object that represent the address parsed
|
|
*/
|
|
export function parseAddress(urlAddress: any): any {
|
|
//
|
|
// TODO: refactor it to something more reasonable?
|
|
//
|
|
// protocol : // ( host )|( ipv6 ): port /
|
|
let urlPattern = /^((https?):(\/\/)?)?((([^\/:]*)|\[([^\[\]]+)\]):)?(\d+)\/?$/.exec(urlAddress);
|
|
|
|
if (urlPattern) {
|
|
return {
|
|
proto: urlPattern[2] || DEFAULT_PROTOCOL,
|
|
host: urlPattern[6] || urlPattern[7] || DEFAULT_DOMAIN,
|
|
port: urlPattern[8] || DEFAULT_PORT,
|
|
};
|
|
}
|
|
|
|
urlPattern = /^((https?):(\/\/)?)?unix:(.*)$/.exec(urlAddress);
|
|
|
|
if (urlPattern) {
|
|
return {
|
|
proto: urlPattern[2] || DEFAULT_PROTOCOL,
|
|
path: urlPattern[4],
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Function filters out bad semver versions and sorts the array.
|
|
* @return {Array} sorted Array
|
|
*/
|
|
export function semverSort(listVersions: string[] /* logger */): string[] {
|
|
return (
|
|
listVersions
|
|
.filter(function (x): boolean {
|
|
if (!semver.parse(x, true)) {
|
|
// FIXME: logger is always undefined
|
|
// logger.warn({ ver: x }, 'ignoring bad version @{ver}');
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
// FIXME: it seems the @types/semver do not handle a legitimate method named 'compareLoose'
|
|
// @ts-ignore
|
|
.sort(semver.compareLoose)
|
|
.map(String)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Flatten arrays of tags.
|
|
* @param {*} data
|
|
*/
|
|
export function normalizeDistTags(pkg: Package): void {
|
|
let sorted;
|
|
if (!pkg[DIST_TAGS].latest) {
|
|
// overwrite latest with highest known version based on semver sort
|
|
sorted = semverSort(Object.keys(pkg.versions));
|
|
if (sorted?.length) {
|
|
pkg[DIST_TAGS].latest = sorted.pop();
|
|
}
|
|
}
|
|
|
|
for (const tag in pkg[DIST_TAGS]) {
|
|
if (_.isArray(pkg[DIST_TAGS][tag])) {
|
|
if (pkg[DIST_TAGS][tag].length) {
|
|
// sort array
|
|
// FIXME: this is clearly wrong, we need to research why this is like this.
|
|
// @ts-ignore
|
|
sorted = semverSort(pkg[DIST_TAGS][tag]);
|
|
if (sorted.length) {
|
|
// use highest version based on semver sort
|
|
pkg[DIST_TAGS][tag] = sorted.pop();
|
|
}
|
|
} else {
|
|
delete pkg[DIST_TAGS][tag];
|
|
}
|
|
} else if (_.isString(pkg[DIST_TAGS][tag])) {
|
|
if (!semver.parse(pkg[DIST_TAGS][tag], true)) {
|
|
// if the version is invalid, delete the dist-tag entry
|
|
delete pkg[DIST_TAGS][tag];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const parseIntervalTable = {
|
|
'': 1000,
|
|
ms: 1,
|
|
s: 1000,
|
|
m: 60 * 1000,
|
|
h: 60 * 60 * 1000,
|
|
d: 86400000,
|
|
w: 7 * 86400000,
|
|
M: 30 * 86400000,
|
|
y: 365 * 86400000,
|
|
};
|
|
|
|
/**
|
|
* Parse an internal string to number
|
|
* @param {*} interval
|
|
* @return {Number}
|
|
*/
|
|
export function parseInterval(interval: any): number {
|
|
if (typeof interval === 'number') {
|
|
return interval * 1000;
|
|
}
|
|
let result = 0;
|
|
let last_suffix = Infinity;
|
|
interval.split(/\s+/).forEach(function (x): void {
|
|
if (!x) {
|
|
return;
|
|
}
|
|
const m = x.match(/^((0|[1-9][0-9]*)(\.[0-9]+)?)(ms|s|m|h|d|w|M|y|)$/);
|
|
if (
|
|
!m ||
|
|
parseIntervalTable[m[4]] >= last_suffix ||
|
|
(m[4] === '' && last_suffix !== Infinity)
|
|
) {
|
|
throw Error('invalid interval: ' + interval);
|
|
}
|
|
last_suffix = parseIntervalTable[m[4]];
|
|
result += Number(m[1]) * parseIntervalTable[m[4]];
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Detect running protocol (http or https)
|
|
*/
|
|
export function getWebProtocol(headerProtocol: string | void, protocol: string): string {
|
|
if (typeof headerProtocol === 'string' && headerProtocol !== '') {
|
|
const commaIndex = headerProtocol.indexOf(',');
|
|
return commaIndex > 0 ? headerProtocol.substr(0, commaIndex) : headerProtocol;
|
|
}
|
|
|
|
return protocol;
|
|
}
|
|
|
|
export function getLatestVersion(pkgInfo: Package): string {
|
|
return pkgInfo[DIST_TAGS].latest;
|
|
}
|
|
|
|
export const ErrorCode = {
|
|
getConflict,
|
|
getBadData,
|
|
getBadRequest,
|
|
getInternalError,
|
|
getUnauthorized,
|
|
getForbidden,
|
|
getServiceUnavailable,
|
|
getNotFound,
|
|
getCode,
|
|
};
|
|
|
|
export function parseConfigFile(configPath: string): any {
|
|
try {
|
|
if (/\.ya?ml$/i.test(configPath)) {
|
|
// @ts-ignore
|
|
return YAML.safeLoad(fs.readFileSync(configPath, CHARACTER_ENCODING.UTF8));
|
|
}
|
|
return require(configPath);
|
|
} catch (e) {
|
|
if (e.code !== 'MODULE_NOT_FOUND') {
|
|
e.message = APP_ERROR.CONFIG_NOT_VALID;
|
|
}
|
|
|
|
throw new Error(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the path already exist.
|
|
* @param {String} path
|
|
* @return {Boolean}
|
|
*/
|
|
export function folderExists(path: string): boolean {
|
|
try {
|
|
const stat = fs.statSync(path);
|
|
return stat.isDirectory();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the file already exist.
|
|
* @param {String} path
|
|
* @return {Boolean}
|
|
*/
|
|
export function fileExists(path: string): boolean {
|
|
try {
|
|
const stat = fs.statSync(path);
|
|
return stat.isFile();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function sortByName(packages: any[], orderAscending: boolean | void = true): string[] {
|
|
return packages.slice().sort(function (a, b): number {
|
|
const comparatorNames = a.name.toLowerCase() < b.name.toLowerCase();
|
|
|
|
return orderAscending ? (comparatorNames ? -1 : 1) : comparatorNames ? 1 : -1;
|
|
});
|
|
}
|
|
|
|
export function addScope(scope: string, packageName: string): string {
|
|
return `@${scope}/${packageName}`;
|
|
}
|
|
|
|
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
|
|
_.forEach(propertiesToDelete, (property): any => {
|
|
delete objectItem[property];
|
|
});
|
|
|
|
return objectItem;
|
|
}
|
|
|
|
/**
|
|
* parse package readme - markdown/ascii
|
|
* @param {String} packageName name of package
|
|
* @param {String} readme package readme
|
|
* @return {String} converted html template
|
|
*/
|
|
export function parseReadme(packageName: string, readme: string, logger): string | void {
|
|
if (_.isEmpty(readme) === false) {
|
|
return sanitizyReadme(readme);
|
|
}
|
|
|
|
// logs readme not found error
|
|
logger.error({ packageName }, '@{packageName}: No readme found');
|
|
|
|
return sanitizyReadme('ERROR: No README data found!');
|
|
}
|
|
|
|
export function buildToken(type: string, token: string): string {
|
|
return `${_.capitalize(type)} ${token}`;
|
|
}
|
|
|
|
/**
|
|
* return package version from tarball name
|
|
* @param {String} name
|
|
* @returns {String}
|
|
*/
|
|
export function getVersionFromTarball(name: string): string | void {
|
|
// FIXME: we know the regex is valid, but we should improve this part as ts suggest
|
|
// @ts-ignore
|
|
return /.+-(\d.+)\.tgz/.test(name) ? name.match(/.+-(\d.+)\.tgz/)[1] : undefined;
|
|
}
|
|
|
|
export type AuthorFormat = Author | string | null | object | void;
|
|
|
|
/**
|
|
* Formats author field for webui.
|
|
* @see https://docs.npmjs.com/files/package.json#author
|
|
* @param {string|object|undefined} author
|
|
*/
|
|
export function formatAuthor(author: AuthorFormat): any {
|
|
let authorDetails = {
|
|
name: DEFAULT_USER,
|
|
email: '',
|
|
url: '',
|
|
};
|
|
|
|
if (_.isNil(author)) {
|
|
return authorDetails;
|
|
}
|
|
|
|
if (_.isString(author)) {
|
|
authorDetails = {
|
|
...authorDetails,
|
|
name: author as string,
|
|
};
|
|
}
|
|
|
|
if (_.isObject(author)) {
|
|
authorDetails = {
|
|
...authorDetails,
|
|
...(author as Author),
|
|
};
|
|
}
|
|
|
|
return authorDetails;
|
|
}
|
|
|
|
/**
|
|
* Check if URI is starting with "http://", "https://" or "//"
|
|
* @param {string} uri
|
|
*/
|
|
export function isHTTPProtocol(uri: string): boolean {
|
|
return /^(https?:)?\/\//.test(uri);
|
|
}
|
|
|
|
/**
|
|
* Apply whitespaces based on the length
|
|
* @param {*} str the log message
|
|
* @return {String}
|
|
*/
|
|
export function pad(str, max): string {
|
|
if (str.length < max) {
|
|
return str + ' '.repeat(max - str.length);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* return a masquerade string with its first and last {charNum} and three dots in between.
|
|
* @param {String} str
|
|
* @param {Number} charNum
|
|
* @returns {String}
|
|
*/
|
|
export function mask(str: string, charNum = 3): string {
|
|
return `${str.substr(0, charNum)}...${str.substr(-charNum)}`;
|
|
}
|
|
|
|
export function encodeScopedUri(packageName): string {
|
|
return packageName.replace(/\//g, '%2f');
|
|
}
|
|
|
|
export function hasDiffOneKey(versions): boolean {
|
|
return Object.keys(versions).length !== 1;
|
|
}
|
|
|
|
export function isVersionValid(packageMeta, packageVersion): boolean {
|
|
const hasVersion = typeof packageVersion !== 'undefined';
|
|
if (!hasVersion) {
|
|
return false;
|
|
}
|
|
|
|
const hasMatchVersion = Object.keys(packageMeta.versions).includes(packageVersion);
|
|
return hasMatchVersion;
|
|
}
|
|
|
|
export function isRelatedToDeprecation(pkgInfo: Package): boolean {
|
|
const { versions } = pkgInfo;
|
|
for (const version in versions) {
|
|
if (Object.prototype.hasOwnProperty.call(versions[version], 'deprecated')) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|