@verdaccio/api 9.0.0-next-9.30 → 9.0.0-next-9.32

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,10 +47,16 @@ 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);
54
+ if (res.headersSent) {
55
+ debug$1("headers already sent, destroying response for %o", req.url);
56
+ res.destroy(err);
57
+ return;
58
+ }
59
+ res.removeHeader(_verdaccio_core.HEADERS.CONTENT_TYPE);
54
60
  res.locals.report_error(err);
55
61
  next(err);
56
62
  });
@@ -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 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,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,10 +45,16 @@ 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);
52
+ if (res.headersSent) {
53
+ debug("headers already sent, destroying response for %o", req.url);
54
+ res.destroy(err);
55
+ return;
56
+ }
57
+ res.removeHeader(HEADERS.CONTENT_TYPE);
52
58
  res.locals.report_error(err);
53
59
  next(err);
54
60
  });
@@ -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 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,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"}
package/build/stage.js CHANGED
@@ -173,6 +173,11 @@ function stage(router, auth, storage, config, logger, requireOtp = (_req, _res,
173
173
  res.header(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, String(size));
174
174
  });
175
175
  stream.once("error", (err) => {
176
+ if (res.headersSent) {
177
+ res.destroy(err);
178
+ return;
179
+ }
180
+ res.removeHeader(_verdaccio_core.HEADERS.CONTENT_TYPE);
176
181
  res.locals.report_error(err);
177
182
  next(err);
178
183
  });
@@ -1 +1 @@
1
- {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\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 stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\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 username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\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 return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\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 stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\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 username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n if (res.headersSent) {\n // The 200 + headers are already on the wire; destroying the\n // response is the only way to signal the failure — and the error\n // middleware must not run again on the terminated connection.\n res.destroy(err);\n return;\n }\n // Error bodies are JSON — drop the optimistic octet-stream header.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\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 return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
package/build/stage.mjs CHANGED
@@ -171,6 +171,11 @@ function stage(router, auth, storage, config, logger, requireOtp = (_req, _res,
171
171
  res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));
172
172
  });
173
173
  stream.once("error", (err) => {
174
+ if (res.headersSent) {
175
+ res.destroy(err);
176
+ return;
177
+ }
178
+ res.removeHeader(HEADERS.CONTENT_TYPE);
174
179
  res.locals.report_error(err);
175
180
  next(err);
176
181
  });
@@ -1 +1 @@
1
- {"version":3,"file":"stage.mjs","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\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 stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\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 username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\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 return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stage.mjs","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\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 stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\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 username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n if (res.headersSent) {\n // The 200 + headers are already on the wire; destroying the\n // response is the only way to signal the failure — and the error\n // middleware must not run again on the terminated connection.\n res.destroy(err);\n return;\n }\n // Error bodies are JSON — drop the optimistic octet-stream header.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\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 return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
@@ -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.30",
3
+ "version": "9.0.0-next-9.32",
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.30",
37
- "@verdaccio/config": "9.0.0-next-9.30",
38
- "@verdaccio/core": "9.0.0-next-9.30",
39
- "@verdaccio/hooks": "9.0.0-next-9.30",
40
- "@verdaccio/logger": "9.0.0-next-9.30",
41
- "@verdaccio/middleware": "9.0.0-next-9.30",
42
- "@verdaccio/store": "9.0.0-next-9.30",
36
+ "@verdaccio/auth": "9.0.0-next-9.32",
37
+ "@verdaccio/config": "9.0.0-next-9.32",
38
+ "@verdaccio/core": "9.0.0-next-9.32",
39
+ "@verdaccio/hooks": "9.0.0-next-9.32",
40
+ "@verdaccio/logger": "9.0.0-next-9.32",
41
+ "@verdaccio/middleware": "9.0.0-next-9.32",
42
+ "@verdaccio/store": "9.0.0-next-9.32",
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.31",
50
- "@verdaccio/types": "14.0.0-next-9.13",
49
+ "@verdaccio/test-helper": "5.0.0-next-9.33",
50
+ "@verdaccio/types": "14.0.0-next-9.15",
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
- "vite": "8.2.1",
57
- "vitest": "4.1.10"
56
+ "vite": "8.2.2",
57
+ "vitest": "4.1.11"
58
58
  },
59
59
  "funding": {
60
60
  "type": "opencollective",