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

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.
package/build/package.js CHANGED
@@ -47,7 +47,7 @@ function package_default(route, auth, storage, logger) {
47
47
  const stream = await storage.getTarball(pkgName, filename, { signal: abort.signal });
48
48
  stream.on("content-length", (size) => {
49
49
  debug$1("tarball size %o", size);
50
- res.header(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, size);
50
+ if (!res.headersSent) res.header(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, size);
51
51
  });
52
52
  stream.once("error", (err) => {
53
53
  debug$1("error on download tarball %o", err);
@@ -1 +1 @@
1
- {"version":3,"file":"package.js","names":[],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;;;AAWA,IAAM,WAAA,GAAQ,MAAA,QAAA,CAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,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,IACJ,sBAAA,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,QAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,gBAAA,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,iBAAA,QAAQ;EACvE,QAAM,mBAAmB,WAAW;EAEpC,MAAM,kBAAA,GAAiB,sBAAA,kBAAA,CAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,QAAM,sBAAsB;IAC5B,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,oBAAoB;GACvE,OAAO;IACL,QAAM,eAAe;IACrB,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAA,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,gBAAA,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,QAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,QAAM,mBAAmB,IAAI;IAC7B,IAAI,OAAO,gBAAA,YAAY,gBAAgB,IAAI;GAC7C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,QAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,QAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,gBAAA,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,QAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,gBAAA,QAAQ,cAAc,gBAAA,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
1
+ {"version":3,"file":"package.js","names":[],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n // the size event races against the first data chunk; setting a\n // header after they are flushed would throw and kill the process\n if (!res.headersSent) {\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n }\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;;;AAWA,IAAM,WAAA,GAAQ,MAAA,QAAA,CAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,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,IACJ,sBAAA,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,QAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,gBAAA,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,iBAAA,QAAQ;EACvE,QAAM,mBAAmB,WAAW;EAEpC,MAAM,kBAAA,GAAiB,sBAAA,kBAAA,CAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,QAAM,sBAAsB;IAC5B,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,oBAAoB;GACvE,OAAO;IACL,QAAM,eAAe;IACrB,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAA,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,gBAAA,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,QAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,QAAM,mBAAmB,IAAI;IAG7B,IAAI,CAAC,IAAI,aACP,IAAI,OAAO,gBAAA,YAAY,gBAAgB,IAAI;GAE/C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,QAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,QAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,gBAAA,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,QAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,gBAAA,QAAQ,cAAc,gBAAA,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
package/build/package.mjs CHANGED
@@ -45,7 +45,7 @@ function package_default(route, auth, storage, logger) {
45
45
  const stream = await storage.getTarball(pkgName, filename, { signal: abort.signal });
46
46
  stream.on("content-length", (size) => {
47
47
  debug("tarball size %o", size);
48
- res.header(HEADER_TYPE.CONTENT_LENGTH, size);
48
+ if (!res.headersSent) res.header(HEADER_TYPE.CONTENT_LENGTH, size);
49
49
  });
50
50
  stream.once("error", (err) => {
51
51
  debug("error on download tarball %o", err);
@@ -1 +1 @@
1
- {"version":3,"file":"package.mjs","names":[],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;AAWA,IAAM,QAAQ,WAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,MAAM,MAAM,MAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,IACJ,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,MAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,QAAQ;EACvE,MAAM,mBAAmB,WAAW;EAEpC,MAAM,iBAAiB,kBAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,MAAM,sBAAsB;IAC5B,KAAK,UAAU,YAAY,cAAc,QAAQ,oBAAoB;GACvE,OAAO;IACL,MAAM,eAAe;IACrB,KAAK,UAAU,YAAY,cAAc,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,MAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,MAAM,mBAAmB,IAAI;IAC7B,IAAI,OAAO,YAAY,gBAAgB,IAAI;GAC7C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,MAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,MAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,MAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,QAAQ,cAAc,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
1
+ {"version":3,"file":"package.mjs","names":[],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n // the size event races against the first data chunk; setting a\n // header after they are flushed would throw and kill the process\n if (!res.headersSent) {\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n }\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;AAWA,IAAM,QAAQ,WAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,MAAM,MAAM,MAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,IACJ,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,MAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,QAAQ;EACvE,MAAM,mBAAmB,WAAW;EAEpC,MAAM,iBAAiB,kBAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,MAAM,sBAAsB;IAC5B,KAAK,UAAU,YAAY,cAAc,QAAQ,oBAAoB;GACvE,OAAO;IACL,MAAM,eAAe;IACrB,KAAK,UAAU,YAAY,cAAc,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,MAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,MAAM,mBAAmB,IAAI;IAG7B,IAAI,CAAC,IAAI,aACP,IAAI,OAAO,YAAY,gBAAgB,IAAI;GAE/C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,MAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,MAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,MAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,QAAQ,cAAc,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
@@ -1,10 +1,5 @@
1
1
  import type { Auth } from '@verdaccio/auth';
2
2
  import type { Storage } from '@verdaccio/store';
3
3
  import type { Config, Logger } from '@verdaccio/types';
4
- /**
5
- * Endpoint for npm search v1
6
- * Empty value
7
- * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
8
- * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
9
- */
4
+ /** Search v1 includes the response time as an ISO 8601 UTC timestamp. */
10
5
  export default function (route: any, auth: Auth, storage: Storage, config: Config, logger: Logger): void;
@@ -1,25 +1,17 @@
1
- const require_runtime = require("../_virtual/_rolldown/runtime.js");
2
1
  let _verdaccio_middleware = require("@verdaccio/middleware");
3
2
  let _verdaccio_core = require("@verdaccio/core");
4
- let debug = require("debug");
5
- debug = require_runtime.__toESM(debug);
6
3
  //#region src/v1/search.ts
7
- var debug$1 = (0, debug.default)("verdaccio:api:search");
8
4
  var DEFAULT_SIZE = 20;
9
5
  var MAX_SIZE = 250;
10
6
  var MAX_FROM = 1e4;
11
7
  var CHECK_ACCESS_BATCH_SIZE = 50;
8
+ var SEARCH_TIMEOUT_MS = 3e4;
12
9
  function parseQueryInt(value, defaultValue, max) {
13
10
  const parsed = Number.parseInt(String(value), 10);
14
11
  if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
15
12
  return Math.min(parsed, max);
16
13
  }
17
- /**
18
- * Endpoint for npm search v1
19
- * Empty value
20
- * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
21
- * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
22
- */
14
+ /** Search v1 includes the response time as an ISO 8601 UTC timestamp. */
23
15
  function search_default(route, auth, storage, config, logger) {
24
16
  function checkAccess(pkg, auth, remoteUser) {
25
17
  return new Promise((resolve, reject) => {
@@ -40,44 +32,69 @@ function search_default(route, auth, storage, config, logger) {
40
32
  size,
41
33
  from
42
34
  };
43
- const searchParams = new URLSearchParams(query);
44
- searchParams.set("size", String(size));
45
- searchParams.set("from", String(from));
46
- const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
47
35
  const abort = new AbortController();
48
36
  const onClientClose = () => {
49
- debug$1("search web aborted");
50
- abort.abort();
37
+ if (!res.writableEnded) abort.abort(/* @__PURE__ */ new Error("search client disconnected"));
51
38
  };
52
- req.socket.on("close", onClientClose);
39
+ res.on("close", onClientClose);
40
+ const timeout = setTimeout(() => {
41
+ abort.abort(_verdaccio_core.errorUtils.getServiceUnavailable("search pagination timed out"));
42
+ }, SEARCH_TIMEOUT_MS);
43
+ timeout.unref();
44
+ let onAbort;
45
+ const cancelled = new Promise((_resolve, reject) => {
46
+ onAbort = () => reject(abort.signal.reason);
47
+ abort.signal.addEventListener("abort", onAbort, { once: true });
48
+ });
53
49
  try {
54
- debug$1("storage search initiated");
55
- const data = await storage.search({
56
- query: safeQuery,
57
- url: safeUrl,
58
- abort
59
- });
60
- debug$1("storage items total: %o", data.length);
61
50
  const requested = from + size;
62
- const allowed = [];
63
- for (let i = 0; i < data.length && allowed.length < requested && !abort.signal.aborted; i += CHECK_ACCESS_BATCH_SIZE) {
64
- const batch = await Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user)));
65
- for (const item of batch) if (item !== null) allowed.push(item);
66
- }
67
- const final = allowed.slice(from, requested);
51
+ const access = /* @__PURE__ */ new Map();
52
+ const collect = async () => {
53
+ if (size === 0) return [];
54
+ let allowed = [];
55
+ for await (const data of storage.searchPages({
56
+ query: safeQuery,
57
+ url,
58
+ abort
59
+ })) {
60
+ abort.signal.throwIfAborted();
61
+ allowed = [];
62
+ for (let i = 0; i < data.length && allowed.length < requested; i += CHECK_ACCESS_BATCH_SIZE) {
63
+ abort.signal.throwIfAborted();
64
+ const batch = await Promise.race([Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {
65
+ const name = item.package.name;
66
+ let permission = access.get(name);
67
+ if (!permission) {
68
+ permission = checkAccess(item, auth, req.remote_user).then((result) => result !== null);
69
+ access.set(name, permission);
70
+ }
71
+ return await permission ? item : null;
72
+ })), cancelled]);
73
+ for (const item of batch) if (item !== null) allowed.push(item);
74
+ }
75
+ if (allowed.length >= requested) break;
76
+ }
77
+ abort.signal.throwIfAborted();
78
+ return allowed.slice(from, requested);
79
+ };
80
+ const final = await Promise.race([collect(), cancelled]);
68
81
  logger.debug(`search results ${final?.length}`);
69
82
  const response = {
70
83
  objects: final,
71
84
  total: final.length,
72
- time: (/* @__PURE__ */ new Date()).toUTCString()
85
+ time: (/* @__PURE__ */ new Date()).toISOString()
73
86
  };
74
87
  res.status(_verdaccio_core.HTTP_STATUS.OK).json(response);
75
88
  } catch (error) {
89
+ if (res.destroyed) return;
76
90
  logger.error({ error }, "search endpoint has failed @{error.message}");
77
91
  next(error);
78
92
  return;
79
93
  } finally {
80
- req.socket.off("close", onClientClose);
94
+ clearTimeout(timeout);
95
+ abort.signal.removeEventListener("abort", onAbort);
96
+ res.off("close", onClientClose);
97
+ abort.abort();
81
98
  }
82
99
  });
83
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import buildDebug from 'debug';\n\nimport type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS } 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 debug = buildDebug('verdaccio:api:search');\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;\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/**\n * Endpoint for npm search v1\n * Empty value\n * - {\"objects\":[],\"total\":0,\"time\":\"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)\"}\n * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'\n */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(pkg: any, auth: any, remoteUser): Promise<searchUtils.SearchItemPkg | 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 // rebuild the query and relative url from the clamped values so the\n // bounds hold end-to-end: storage plugins read `query.size`/`query.from`\n // and the uplink proxy forwards `url` verbatim to the remote registry\n const safeQuery = { ...query, size, from };\n const searchParams = new URLSearchParams(query);\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 debug('search web aborted');\n abort.abort();\n };\n req.socket.on('close', onClientClose);\n\n try {\n debug('storage search initiated');\n const data = await storage.search({\n query: safeQuery,\n url: safeUrl,\n abort,\n });\n debug('storage items total: %o', data.length);\n\n // evaluate access in batches and stop as soon as the requested page\n // is filled, instead of running an auth check on every result\n const requested = from + size;\n const allowed: searchUtils.SearchItemPkg[] = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested && !abort.signal.aborted;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n const batch = await Promise.all(\n data\n .slice(i, i + CHECK_ACCESS_BATCH_SIZE)\n .map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user))\n );\n for (const item of batch) {\n if (item !== null) {\n allowed.push(item);\n }\n }\n }\n\n const final: searchUtils.SearchItemPkg[] = allowed.slice(from, requested);\n logger.debug(`search results ${final?.length}`);\n\n const response: searchUtils.SearchResults = {\n objects: final,\n total: final.length,\n time: new Date().toUTCString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n req.socket.off('close', onClientClose);\n }\n }\n );\n}\n"],"mappings":";;;;;;AASA,IAAM,WAAA,GAAQ,MAAA,QAAA,CAAW,sBAAsB;AAE/C,IAAM,eAAe;AAErB,IAAM,WAAW;AAGjB,IAAM,WAAW;AAGjB,IAAM,0BAA0B;AAEhC,SAAS,cAAc,OAAgB,cAAsB,KAAqB;CAChF,MAAM,SAAS,OAAO,SAAS,OAAO,KAAK,GAAG,EAAE;CAChD,IAAI,OAAO,MAAM,MAAM,KAAK,SAAS,GACnC,OAAO;CAET,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC7B;;;;;;;AAQA,SAAA,eACE,OACA,MACA,SACA,QACA,QACM;CACN,SAAS,YAAY,KAAU,MAAW,YAAuD;EAC/F,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,KAAK,aAAa,EAAE,aAAa,KAAK,SAAS,KAAK,GAAG,YAAY,SAAU,KAAK,SAAS;IACzF,IAAI,KAAK;KACP,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,CAAC,CAAC,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;UAEnB,OAAO,GAAG;IAEd,OACE,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,IACJ,sBAAA,qBAAqB,SAAA,GACrB,sBAAA,UAAA,CAAU,QAAQ,aAAa,GAC/B,OAAO,KAAK,KAAK,SAAS;EACxB,MAAM,EAAE,OAAO,QAAQ;EAGvB,MAAM,OAAO,cAAc,MAAM,MAAM,cAAc,QAAQ;EAC7D,MAAM,OAAO,cAAc,MAAM,MAAM,GAAG,QAAQ;EAIlD,MAAM,YAAY;GAAE,GAAG;GAAO;GAAM;EAAK;EACzC,MAAM,eAAe,IAAI,gBAAgB,KAAK;EAC9C,aAAa,IAAI,QAAQ,OAAO,IAAI,CAAC;EACrC,aAAa,IAAI,QAAQ,OAAO,IAAI,CAAC;EACrC,MAAM,UAAU,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,aAAa,SAAS;EAC9D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,sBAA4B;GAChC,QAAM,oBAAoB;GAC1B,MAAM,MAAM;EACd;EACA,IAAI,OAAO,GAAG,SAAS,aAAa;EAEpC,IAAI;GACF,QAAM,0BAA0B;GAChC,MAAM,OAAO,MAAM,QAAQ,OAAO;IAChC,OAAO;IACP,KAAK;IACL;GACF,CAAC;GACD,QAAM,2BAA2B,KAAK,MAAM;GAI5C,MAAM,YAAY,OAAO;GACzB,MAAM,UAAuC,CAAC;GAC9C,KACE,IAAI,IAAI,GACR,IAAI,KAAK,UAAU,QAAQ,SAAS,aAAa,CAAC,MAAM,OAAO,SAC/D,KAAK,yBACL;IACA,MAAM,QAAQ,MAAM,QAAQ,IAC1B,KACG,MAAM,GAAG,IAAI,uBAAuB,CAAC,CACrC,KAAK,YAAY,YAAY,SAAS,MAAM,IAAI,WAAW,CAAC,CACjE;IACA,KAAK,MAAM,QAAQ,OACjB,IAAI,SAAS,MACX,QAAQ,KAAK,IAAI;GAGvB;GAEA,MAAM,QAAqC,QAAQ,MAAM,MAAM,SAAS;GACxE,OAAO,MAAM,kBAAkB,OAAO,QAAQ;GAE9C,MAAM,WAAsC;IAC1C,SAAS;IACT,OAAO,MAAM;IACb,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,gBAAA,YAAY,EAAE,CAAC,CAAC,KAAK,QAAQ;EAC1C,SAAS,OAAO;GACd,OAAO,MAAM,EAAE,MAAM,GAAG,6CAA6C;GACrE,KAAK,KAAK;GACV;EACF,UAAU;GACR,IAAI,OAAO,IAAI,SAAS,aAAa;EACvC;CACF,CACF;AACF"}
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,23 +1,17 @@
1
1
  import { SEARCH_API_ENDPOINTS, rateLimit } from "@verdaccio/middleware";
2
- import { HTTP_STATUS } from "@verdaccio/core";
3
- import buildDebug from "debug";
2
+ import { HTTP_STATUS, errorUtils } from "@verdaccio/core";
4
3
  //#region src/v1/search.ts
5
- var debug = buildDebug("verdaccio:api:search");
6
4
  var DEFAULT_SIZE = 20;
7
5
  var MAX_SIZE = 250;
8
6
  var MAX_FROM = 1e4;
9
7
  var CHECK_ACCESS_BATCH_SIZE = 50;
8
+ var SEARCH_TIMEOUT_MS = 3e4;
10
9
  function parseQueryInt(value, defaultValue, max) {
11
10
  const parsed = Number.parseInt(String(value), 10);
12
11
  if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
13
12
  return Math.min(parsed, max);
14
13
  }
15
- /**
16
- * Endpoint for npm search v1
17
- * Empty value
18
- * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
19
- * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
20
- */
14
+ /** Search v1 includes the response time as an ISO 8601 UTC timestamp. */
21
15
  function search_default(route, auth, storage, config, logger) {
22
16
  function checkAccess(pkg, auth, remoteUser) {
23
17
  return new Promise((resolve, reject) => {
@@ -38,44 +32,69 @@ function search_default(route, auth, storage, config, logger) {
38
32
  size,
39
33
  from
40
34
  };
41
- const searchParams = new URLSearchParams(query);
42
- searchParams.set("size", String(size));
43
- searchParams.set("from", String(from));
44
- const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
45
35
  const abort = new AbortController();
46
36
  const onClientClose = () => {
47
- debug("search web aborted");
48
- abort.abort();
37
+ if (!res.writableEnded) abort.abort(/* @__PURE__ */ new Error("search client disconnected"));
49
38
  };
50
- req.socket.on("close", onClientClose);
39
+ res.on("close", onClientClose);
40
+ const timeout = setTimeout(() => {
41
+ abort.abort(errorUtils.getServiceUnavailable("search pagination timed out"));
42
+ }, SEARCH_TIMEOUT_MS);
43
+ timeout.unref();
44
+ let onAbort;
45
+ const cancelled = new Promise((_resolve, reject) => {
46
+ onAbort = () => reject(abort.signal.reason);
47
+ abort.signal.addEventListener("abort", onAbort, { once: true });
48
+ });
51
49
  try {
52
- debug("storage search initiated");
53
- const data = await storage.search({
54
- query: safeQuery,
55
- url: safeUrl,
56
- abort
57
- });
58
- debug("storage items total: %o", data.length);
59
50
  const requested = from + size;
60
- const allowed = [];
61
- for (let i = 0; i < data.length && allowed.length < requested && !abort.signal.aborted; i += CHECK_ACCESS_BATCH_SIZE) {
62
- const batch = await Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user)));
63
- for (const item of batch) if (item !== null) allowed.push(item);
64
- }
65
- const final = allowed.slice(from, requested);
51
+ const access = /* @__PURE__ */ new Map();
52
+ const collect = async () => {
53
+ if (size === 0) return [];
54
+ let allowed = [];
55
+ for await (const data of storage.searchPages({
56
+ query: safeQuery,
57
+ url,
58
+ abort
59
+ })) {
60
+ abort.signal.throwIfAborted();
61
+ allowed = [];
62
+ for (let i = 0; i < data.length && allowed.length < requested; i += CHECK_ACCESS_BATCH_SIZE) {
63
+ abort.signal.throwIfAborted();
64
+ const batch = await Promise.race([Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {
65
+ const name = item.package.name;
66
+ let permission = access.get(name);
67
+ if (!permission) {
68
+ permission = checkAccess(item, auth, req.remote_user).then((result) => result !== null);
69
+ access.set(name, permission);
70
+ }
71
+ return await permission ? item : null;
72
+ })), cancelled]);
73
+ for (const item of batch) if (item !== null) allowed.push(item);
74
+ }
75
+ if (allowed.length >= requested) break;
76
+ }
77
+ abort.signal.throwIfAborted();
78
+ return allowed.slice(from, requested);
79
+ };
80
+ const final = await Promise.race([collect(), cancelled]);
66
81
  logger.debug(`search results ${final?.length}`);
67
82
  const response = {
68
83
  objects: final,
69
84
  total: final.length,
70
- time: (/* @__PURE__ */ new Date()).toUTCString()
85
+ time: (/* @__PURE__ */ new Date()).toISOString()
71
86
  };
72
87
  res.status(HTTP_STATUS.OK).json(response);
73
88
  } catch (error) {
89
+ if (res.destroyed) return;
74
90
  logger.error({ error }, "search endpoint has failed @{error.message}");
75
91
  next(error);
76
92
  return;
77
93
  } finally {
78
- req.socket.off("close", onClientClose);
94
+ clearTimeout(timeout);
95
+ abort.signal.removeEventListener("abort", onAbort);
96
+ res.off("close", onClientClose);
97
+ abort.abort();
79
98
  }
80
99
  });
81
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.mjs","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import buildDebug from 'debug';\n\nimport type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS } 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 debug = buildDebug('verdaccio:api:search');\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;\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/**\n * Endpoint for npm search v1\n * Empty value\n * - {\"objects\":[],\"total\":0,\"time\":\"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)\"}\n * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'\n */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(pkg: any, auth: any, remoteUser): Promise<searchUtils.SearchItemPkg | 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 // rebuild the query and relative url from the clamped values so the\n // bounds hold end-to-end: storage plugins read `query.size`/`query.from`\n // and the uplink proxy forwards `url` verbatim to the remote registry\n const safeQuery = { ...query, size, from };\n const searchParams = new URLSearchParams(query);\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 debug('search web aborted');\n abort.abort();\n };\n req.socket.on('close', onClientClose);\n\n try {\n debug('storage search initiated');\n const data = await storage.search({\n query: safeQuery,\n url: safeUrl,\n abort,\n });\n debug('storage items total: %o', data.length);\n\n // evaluate access in batches and stop as soon as the requested page\n // is filled, instead of running an auth check on every result\n const requested = from + size;\n const allowed: searchUtils.SearchItemPkg[] = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested && !abort.signal.aborted;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n const batch = await Promise.all(\n data\n .slice(i, i + CHECK_ACCESS_BATCH_SIZE)\n .map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user))\n );\n for (const item of batch) {\n if (item !== null) {\n allowed.push(item);\n }\n }\n }\n\n const final: searchUtils.SearchItemPkg[] = allowed.slice(from, requested);\n logger.debug(`search results ${final?.length}`);\n\n const response: searchUtils.SearchResults = {\n objects: final,\n total: final.length,\n time: new Date().toUTCString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n req.socket.off('close', onClientClose);\n }\n }\n );\n}\n"],"mappings":";;;;AASA,IAAM,QAAQ,WAAW,sBAAsB;AAE/C,IAAM,eAAe;AAErB,IAAM,WAAW;AAGjB,IAAM,WAAW;AAGjB,IAAM,0BAA0B;AAEhC,SAAS,cAAc,OAAgB,cAAsB,KAAqB;CAChF,MAAM,SAAS,OAAO,SAAS,OAAO,KAAK,GAAG,EAAE;CAChD,IAAI,OAAO,MAAM,MAAM,KAAK,SAAS,GACnC,OAAO;CAET,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC7B;;;;;;;AAQA,SAAA,eACE,OACA,MACA,SACA,QACA,QACM;CACN,SAAS,YAAY,KAAU,MAAW,YAAuD;EAC/F,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,KAAK,aAAa,EAAE,aAAa,KAAK,SAAS,KAAK,GAAG,YAAY,SAAU,KAAK,SAAS;IACzF,IAAI,KAAK;KACP,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,CAAC,CAAC,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;UAEnB,OAAO,GAAG;IAEd,OACE,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,IACJ,qBAAqB,QACrB,UAAU,QAAQ,aAAa,GAC/B,OAAO,KAAK,KAAK,SAAS;EACxB,MAAM,EAAE,OAAO,QAAQ;EAGvB,MAAM,OAAO,cAAc,MAAM,MAAM,cAAc,QAAQ;EAC7D,MAAM,OAAO,cAAc,MAAM,MAAM,GAAG,QAAQ;EAIlD,MAAM,YAAY;GAAE,GAAG;GAAO;GAAM;EAAK;EACzC,MAAM,eAAe,IAAI,gBAAgB,KAAK;EAC9C,aAAa,IAAI,QAAQ,OAAO,IAAI,CAAC;EACrC,aAAa,IAAI,QAAQ,OAAO,IAAI,CAAC;EACrC,MAAM,UAAU,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,aAAa,SAAS;EAC9D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,sBAA4B;GAChC,MAAM,oBAAoB;GAC1B,MAAM,MAAM;EACd;EACA,IAAI,OAAO,GAAG,SAAS,aAAa;EAEpC,IAAI;GACF,MAAM,0BAA0B;GAChC,MAAM,OAAO,MAAM,QAAQ,OAAO;IAChC,OAAO;IACP,KAAK;IACL;GACF,CAAC;GACD,MAAM,2BAA2B,KAAK,MAAM;GAI5C,MAAM,YAAY,OAAO;GACzB,MAAM,UAAuC,CAAC;GAC9C,KACE,IAAI,IAAI,GACR,IAAI,KAAK,UAAU,QAAQ,SAAS,aAAa,CAAC,MAAM,OAAO,SAC/D,KAAK,yBACL;IACA,MAAM,QAAQ,MAAM,QAAQ,IAC1B,KACG,MAAM,GAAG,IAAI,uBAAuB,CAAC,CACrC,KAAK,YAAY,YAAY,SAAS,MAAM,IAAI,WAAW,CAAC,CACjE;IACA,KAAK,MAAM,QAAQ,OACjB,IAAI,SAAS,MACX,QAAQ,KAAK,IAAI;GAGvB;GAEA,MAAM,QAAqC,QAAQ,MAAM,MAAM,SAAS;GACxE,OAAO,MAAM,kBAAkB,OAAO,QAAQ;GAE9C,MAAM,WAAsC;IAC1C,SAAS;IACT,OAAO,MAAM;IACb,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,YAAY,EAAE,CAAC,CAAC,KAAK,QAAQ;EAC1C,SAAS,OAAO;GACd,OAAO,MAAM,EAAE,MAAM,GAAG,6CAA6C;GACrE,KAAK,KAAK;GACV;EACF,UAAU;GACR,IAAI,OAAO,IAAI,SAAS,aAAa;EACvC;CACF,CACF;AACF"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/api",
3
- "version": "9.0.0-next-9.31",
3
+ "version": "9.0.0-next-9.33",
4
4
  "description": "Verdaccio Registry API",
5
5
  "main": "./build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,28 +33,28 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@verdaccio/auth": "9.0.0-next-9.31",
37
- "@verdaccio/config": "9.0.0-next-9.31",
38
- "@verdaccio/core": "9.0.0-next-9.31",
39
- "@verdaccio/hooks": "9.0.0-next-9.31",
40
- "@verdaccio/logger": "9.0.0-next-9.31",
41
- "@verdaccio/middleware": "9.0.0-next-9.31",
42
- "@verdaccio/store": "9.0.0-next-9.31",
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",
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.32",
50
- "@verdaccio/types": "14.0.0-next-9.14",
49
+ "@verdaccio/test-helper": "5.0.0-next-9.34",
50
+ "@verdaccio/types": "14.0.0-next-9.16",
51
51
  "mockdate": "3.0.5",
52
52
  "nock": "13.5.6",
53
53
  "otpauth": "9.5.1",
54
54
  "rimraf": "6.1.3",
55
55
  "supertest": "7.1.4",
56
56
  "vite": "8.2.2",
57
- "vitest": "4.1.10"
57
+ "vitest": "4.1.11"
58
58
  },
59
59
  "funding": {
60
60
  "type": "opencollective",