@verdaccio/api 9.0.0-next-9.33 → 9.0.0-next-9.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ function dist_tags_default(route, auth, storage, logger, requireOtp = (_req, _re
13
13
  const tags = {};
14
14
  tags[tag] = req.body;
15
15
  try {
16
- await storage.mergeTagsNext(packageName, tags);
16
+ await storage.mergeTagsNext(packageName, tags, (0, _verdaccio_middleware.getRequestOptions)(req));
17
17
  res.status(_verdaccio_core.constants.HTTP_STATUS.CREATED);
18
18
  return next({ ok: _verdaccio_core.constants.API_MESSAGE.TAG_ADDED });
19
19
  } catch (err) {
@@ -28,7 +28,7 @@ function dist_tags_default(route, auth, storage, logger, requireOtp = (_req, _re
28
28
  const tags = {};
29
29
  tags[tag] = null;
30
30
  try {
31
- await storage.mergeTagsNext(packageName, tags);
31
+ await storage.mergeTagsNext(packageName, tags, (0, _verdaccio_middleware.getRequestOptions)(req));
32
32
  res.status(_verdaccio_core.constants.HTTP_STATUS.CREATED);
33
33
  return next({ ok: _verdaccio_core.constants.API_MESSAGE.TAG_REMOVED });
34
34
  } catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"dist-tags.js","names":[],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags);\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags);\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,OAAA,GAAM,sBAAA,MAAA,CAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,iCAAiC,eACrC,KACA,KACA,MAC2B;EAC3B,IAAI,OAAO,IAAI,SAAS,UACtB,OAAO,KAAK,gBAAA,WAAW,cAAc,oBAAoB,CAAC;EAE5D,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,gBAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO,IAAI;EAChB,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC7C,IAAI,OAAO,gBAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,gBAAA,UAAU,YAAY,UAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF;CAGA,MAAM,IACJ,sBAAA,wBAAwB,SACxB,IAAI,SAAS,GACb,aAAA,GACA,sBAAA,MAAA,CAAM,gBAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,IACJ,sBAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,aAAA,GACA,sBAAA,MAAA,CAAM,gBAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,OACJ,sBAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,gBAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO;EACZ,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC7C,IAAI,OAAO,gBAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,gBAAA,UAAU,YAAY,YAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAA,wBAAwB,eACxB,IAAI,QAAQ,GACZ,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,kBAAA,GAAiB,sBAAA,kBAAA,CAAkB,GAAG;EAC5C,IAAI;GAMF,MAAK,MALkB,QAAQ,oBAAoB;IACjD,MAAM;IACN,aAAa;IACb;GACF,CAAC,EAAA,CACa,gBAAA,UAAU,UAAU;EACpC,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;AACF"}
1
+ {"version":3,"file":"dist-tags.js","names":[],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,OAAA,GAAM,sBAAA,MAAA,CAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,iCAAiC,eACrC,KACA,KACA,MAC2B;EAC3B,IAAI,OAAO,IAAI,SAAS,UACtB,OAAO,KAAK,gBAAA,WAAW,cAAc,oBAAoB,CAAC;EAE5D,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,gBAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO,IAAI;EAChB,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,OAAA,GAAM,sBAAA,kBAAA,CAAkB,GAAG,CAAC;GACrE,IAAI,OAAO,gBAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,gBAAA,UAAU,YAAY,UAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF;CAGA,MAAM,IACJ,sBAAA,wBAAwB,SACxB,IAAI,SAAS,GACb,aAAA,GACA,sBAAA,MAAA,CAAM,gBAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,IACJ,sBAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,aAAA,GACA,sBAAA,MAAA,CAAM,gBAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,OACJ,sBAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,gBAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO;EACZ,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,OAAA,GAAM,sBAAA,kBAAA,CAAkB,GAAG,CAAC;GACrE,IAAI,OAAO,gBAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,gBAAA,UAAU,YAAY,YAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAA,wBAAwB,eACxB,IAAI,QAAQ,GACZ,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,kBAAA,GAAiB,sBAAA,kBAAA,CAAkB,GAAG;EAC5C,IAAI;GAMF,MAAK,MALkB,QAAQ,oBAAoB;IACjD,MAAM;IACN,aAAa;IACb;GACF,CAAC,EAAA,CACa,gBAAA,UAAU,UAAU;EACpC,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;AACF"}
@@ -13,7 +13,7 @@ function dist_tags_default(route, auth, storage, logger, requireOtp = (_req, _re
13
13
  const tags = {};
14
14
  tags[tag] = req.body;
15
15
  try {
16
- await storage.mergeTagsNext(packageName, tags);
16
+ await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));
17
17
  res.status(constants.HTTP_STATUS.CREATED);
18
18
  return next({ ok: constants.API_MESSAGE.TAG_ADDED });
19
19
  } catch (err) {
@@ -28,7 +28,7 @@ function dist_tags_default(route, auth, storage, logger, requireOtp = (_req, _re
28
28
  const tags = {};
29
29
  tags[tag] = null;
30
30
  try {
31
- await storage.mergeTagsNext(packageName, tags);
31
+ await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));
32
32
  res.status(constants.HTTP_STATUS.CREATED);
33
33
  return next({ ok: constants.API_MESSAGE.TAG_REMOVED });
34
34
  } catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"dist-tags.mjs","names":[],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags);\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags);\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,MAAM,MAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,iCAAiC,eACrC,KACA,KACA,MAC2B;EAC3B,IAAI,OAAO,IAAI,SAAS,UACtB,OAAO,KAAK,WAAW,cAAc,oBAAoB,CAAC;EAE5D,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO,IAAI;EAChB,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC7C,IAAI,OAAO,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,UAAU,YAAY,UAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF;CAGA,MAAM,IACJ,wBAAwB,SACxB,IAAI,SAAS,GACb,YACA,MAAM,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,IACJ,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,MAAM,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,OACJ,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO;EACZ,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC7C,IAAI,OAAO,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,UAAU,YAAY,YAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,wBAAwB,eACxB,IAAI,QAAQ,GACZ,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,iBAAiB,kBAAkB,GAAG;EAC5C,IAAI;GAMF,MAAK,MALkB,QAAQ,oBAAoB;IACjD,MAAM;IACN,aAAa;IACb;GACF,CAAC,EAAA,CACa,UAAU,UAAU;EACpC,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;AACF"}
1
+ {"version":3,"file":"dist-tags.mjs","names":[],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,MAAM,MAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,iCAAiC,eACrC,KACA,KACA,MAC2B;EAC3B,IAAI,OAAO,IAAI,SAAS,UACtB,OAAO,KAAK,WAAW,cAAc,oBAAoB,CAAC;EAE5D,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO,IAAI;EAChB,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,MAAM,kBAAkB,GAAG,CAAC;GACrE,IAAI,OAAO,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,UAAU,YAAY,UAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF;CAGA,MAAM,IACJ,wBAAwB,SACxB,IAAI,SAAS,GACb,YACA,MAAM,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,IACJ,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,MAAM,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,OACJ,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO;EACZ,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,MAAM,kBAAkB,GAAG,CAAC;GACrE,IAAI,OAAO,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAI,UAAU,YAAY,YAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,wBAAwB,eACxB,IAAI,QAAQ,GACZ,eACE,KACA,KACA,MACe;EACf,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,iBAAiB,kBAAkB,GAAG;EAC5C,IAAI;GAMF,MAAK,MALkB,QAAQ,oBAAoB;IACjD,MAAM;IACN,aAAa;IACb;GACF,CAAC,EAAA,CACa,UAAU,UAAU;EACpC,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;AACF"}
package/build/user.js CHANGED
@@ -25,6 +25,15 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
25
25
  route.get(_verdaccio_middleware.USER_API_ENDPOINTS.get_user, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), function(req, res, next) {
26
26
  debug$1("verifying user");
27
27
  if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
28
+ if (req.remote_user?.error) {
29
+ debug$1("user authentication failed: %o", req.remote_user.error);
30
+ if (req.remote_user.error === _verdaccio_core.API_ERROR.BAD_AUTH_HEADER) return next(_verdaccio_core.errorUtils.getBadRequest(_verdaccio_core.API_ERROR.BAD_AUTH_HEADER));
31
+ return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
32
+ }
33
+ if (req.headers.authorization) {
34
+ debug$1("credentials were provided but rejected");
35
+ return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
36
+ }
28
37
  debug$1("user not logged in");
29
38
  res.status(_verdaccio_core.HTTP_STATUS.OK);
30
39
  return next({ ok: false });
@@ -70,7 +79,7 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
70
79
  name,
71
80
  err
72
81
  }, "authenticating for user @{username} failed. Error: @{err.message}");
73
- return next(_verdaccio_core.errorUtils.getCode(_verdaccio_core.HTTP_STATUS.UNAUTHORIZED, _verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
82
+ return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
74
83
  }
75
84
  Promise.resolve(requireOtp(req, res, (otpError) => {
76
85
  if (otpError) return next(otpError);
package/build/user.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"user.js","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"user.js","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
package/build/user.mjs CHANGED
@@ -23,6 +23,15 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
23
23
  route.get(USER_API_ENDPOINTS.get_user, rateLimit(config?.userRateLimit), function(req, res, next) {
24
24
  debug("verifying user");
25
25
  if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
26
+ if (req.remote_user?.error) {
27
+ debug("user authentication failed: %o", req.remote_user.error);
28
+ if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));
29
+ return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));
30
+ }
31
+ if (req.headers.authorization) {
32
+ debug("credentials were provided but rejected");
33
+ return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));
34
+ }
26
35
  debug("user not logged in");
27
36
  res.status(HTTP_STATUS.OK);
28
37
  return next({ ok: false });
@@ -68,7 +77,7 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
68
77
  name,
69
78
  err
70
79
  }, "authenticating for user @{username} failed. Error: @{err.message}");
71
- return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD));
80
+ return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));
72
81
  }
73
82
  Promise.resolve(requireOtp(req, res, (otpError) => {
74
83
  if (otpError) return next(otpError);
@@ -1 +1 @@
1
- {"version":3,"file":"user.mjs","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"user.mjs","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,8 @@ var MAX_FROM = 1e4;
7
7
  var CHECK_ACCESS_BATCH_SIZE = 50;
8
8
  var SEARCH_TIMEOUT_MS = 3e4;
9
9
  function parseQueryInt(value, defaultValue, max) {
10
- const parsed = Number.parseInt(String(value), 10);
10
+ if (typeof value !== "string") return defaultValue;
11
+ const parsed = Number.parseInt(value, 10);
11
12
  if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
12
13
  return Math.min(parsed, max);
13
14
  }
@@ -25,6 +26,14 @@ function search_default(route, auth, storage, config, logger) {
25
26
  }
26
27
  route.get(_verdaccio_middleware.SEARCH_API_ENDPOINTS.search, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), async (req, res, next) => {
27
28
  const { query, url } = req;
29
+ if (typeof query.text !== "string" || query.text.trim().length === 0) {
30
+ const error = _verdaccio_core.errorUtils.getBadRequest("'text' query parameter is required");
31
+ res.status(error.status).json({
32
+ error: error.message,
33
+ code: "ERR_TEXT_MISSING"
34
+ });
35
+ return;
36
+ }
28
37
  const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);
29
38
  const from = parseQueryInt(query.from, 0, MAX_FROM);
30
39
  const safeQuery = {
@@ -32,6 +41,11 @@ function search_default(route, auth, storage, config, logger) {
32
41
  size,
33
42
  from
34
43
  };
44
+ const searchParams = new URLSearchParams();
45
+ for (const [key, value] of Object.entries(query)) if (typeof value === "string") searchParams.set(key, value);
46
+ searchParams.set("size", String(size));
47
+ searchParams.set("from", String(from));
48
+ const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
35
49
  const abort = new AbortController();
36
50
  const onClientClose = () => {
37
51
  if (!res.writableEnded) abort.abort(/* @__PURE__ */ new Error("search client disconnected"));
@@ -54,7 +68,7 @@ function search_default(route, auth, storage, config, logger) {
54
68
  let allowed = [];
55
69
  for await (const data of storage.searchPages({
56
70
  query: safeQuery,
57
- url,
71
+ url: safeUrl,
58
72
  abort
59
73
  })) {
60
74
  abort.signal.throwIfAborted();
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n const parsed = Number.parseInt(String(value), 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"search.js","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n if (typeof value !== 'string') {\n return defaultValue;\n }\n const parsed = Number.parseInt(value, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n if (typeof query.text !== 'string' || query.text.trim().length === 0) {\n const error = errorUtils.getBadRequest(\"'text' query parameter is required\");\n res.status(error.status).json({\n error: error.message,\n code: 'ERR_TEXT_MISSING',\n });\n return;\n }\n\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n // forward only plain string parameters to the uplinks, with the clamped pagination\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (typeof value === 'string') {\n searchParams.set(key, value);\n }\n }\n searchParams.set('size', String(size));\n searchParams.set('from', String(from));\n const safeUrl = `${url.split('?')[0]}?${searchParams.toString()}`;\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url: safeUrl, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,8 @@ var MAX_FROM = 1e4;
7
7
  var CHECK_ACCESS_BATCH_SIZE = 50;
8
8
  var SEARCH_TIMEOUT_MS = 3e4;
9
9
  function parseQueryInt(value, defaultValue, max) {
10
- const parsed = Number.parseInt(String(value), 10);
10
+ if (typeof value !== "string") return defaultValue;
11
+ const parsed = Number.parseInt(value, 10);
11
12
  if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
12
13
  return Math.min(parsed, max);
13
14
  }
@@ -25,6 +26,14 @@ function search_default(route, auth, storage, config, logger) {
25
26
  }
26
27
  route.get(SEARCH_API_ENDPOINTS.search, rateLimit(config?.userRateLimit), async (req, res, next) => {
27
28
  const { query, url } = req;
29
+ if (typeof query.text !== "string" || query.text.trim().length === 0) {
30
+ const error = errorUtils.getBadRequest("'text' query parameter is required");
31
+ res.status(error.status).json({
32
+ error: error.message,
33
+ code: "ERR_TEXT_MISSING"
34
+ });
35
+ return;
36
+ }
28
37
  const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);
29
38
  const from = parseQueryInt(query.from, 0, MAX_FROM);
30
39
  const safeQuery = {
@@ -32,6 +41,11 @@ function search_default(route, auth, storage, config, logger) {
32
41
  size,
33
42
  from
34
43
  };
44
+ const searchParams = new URLSearchParams();
45
+ for (const [key, value] of Object.entries(query)) if (typeof value === "string") searchParams.set(key, value);
46
+ searchParams.set("size", String(size));
47
+ searchParams.set("from", String(from));
48
+ const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
35
49
  const abort = new AbortController();
36
50
  const onClientClose = () => {
37
51
  if (!res.writableEnded) abort.abort(/* @__PURE__ */ new Error("search client disconnected"));
@@ -54,7 +68,7 @@ function search_default(route, auth, storage, config, logger) {
54
68
  let allowed = [];
55
69
  for await (const data of storage.searchPages({
56
70
  query: safeQuery,
57
- url,
71
+ url: safeUrl,
58
72
  abort
59
73
  })) {
60
74
  abort.signal.throwIfAborted();
@@ -1 +1 @@
1
- {"version":3,"file":"search.mjs","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n const parsed = Number.parseInt(String(value), 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"search.mjs","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n if (typeof value !== 'string') {\n return defaultValue;\n }\n const parsed = Number.parseInt(value, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n if (typeof query.text !== 'string' || query.text.trim().length === 0) {\n const error = errorUtils.getBadRequest(\"'text' query parameter is required\");\n res.status(error.status).json({\n error: error.message,\n code: 'ERR_TEXT_MISSING',\n });\n return;\n }\n\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n // forward only plain string parameters to the uplinks, with the clamped pagination\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (typeof value === 'string') {\n searchParams.set(key, value);\n }\n }\n searchParams.set('size', String(size));\n searchParams.set('from', String(from));\n const safeUrl = `${url.split('?')[0]}?${searchParams.toString()}`;\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url: safeUrl, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/api",
3
- "version": "9.0.0-next-9.33",
3
+ "version": "9.0.0-next-9.35",
4
4
  "description": "Verdaccio Registry API",
5
5
  "main": "./build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,20 +33,20 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@verdaccio/auth": "9.0.0-next-9.33",
37
- "@verdaccio/config": "9.0.0-next-9.33",
38
- "@verdaccio/core": "9.0.0-next-9.33",
39
- "@verdaccio/hooks": "9.0.0-next-9.33",
40
- "@verdaccio/logger": "9.0.0-next-9.33",
41
- "@verdaccio/middleware": "9.0.0-next-9.33",
42
- "@verdaccio/store": "9.0.0-next-9.33",
36
+ "@verdaccio/auth": "9.0.0-next-9.35",
37
+ "@verdaccio/config": "9.0.0-next-9.35",
38
+ "@verdaccio/core": "9.0.0-next-9.35",
39
+ "@verdaccio/hooks": "9.0.0-next-9.35",
40
+ "@verdaccio/logger": "9.0.0-next-9.35",
41
+ "@verdaccio/middleware": "9.0.0-next-9.35",
42
+ "@verdaccio/store": "9.0.0-next-9.35",
43
43
  "debug": "4.4.3",
44
44
  "express": "5.2.1",
45
45
  "lodash-es": "4.18.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/lodash-es": "4.17.12",
49
- "@verdaccio/test-helper": "5.0.0-next-9.34",
49
+ "@verdaccio/test-helper": "5.0.0-next-9.36",
50
50
  "@verdaccio/types": "14.0.0-next-9.16",
51
51
  "mockdate": "3.0.5",
52
52
  "nock": "13.5.6",