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

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
@@ -51,6 +51,12 @@ function package_default(route, auth, storage, logger) {
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 res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;;;AAWA,IAAM,WAAA,GAAQ,MAAA,QAAA,CAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,MAAM,OAAA,GAAM,sBAAA,MAAA,CAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,IACJ,sBAAA,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,QAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,gBAAA,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,iBAAA,QAAQ;EACvE,QAAM,mBAAmB,WAAW;EAEpC,MAAM,kBAAA,GAAiB,sBAAA,kBAAA,CAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,QAAM,sBAAsB;IAC5B,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,oBAAoB;GACvE,OAAO;IACL,QAAM,eAAe;IACrB,KAAK,UAAU,gBAAA,YAAY,cAAc,gBAAA,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAA,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,gBAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,gBAAA,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,QAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,QAAM,mBAAmB,IAAI;IAC7B,IAAI,OAAO,gBAAA,YAAY,gBAAgB,IAAI;GAC7C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,QAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,QAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,gBAAA,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,QAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,gBAAA,QAAQ,cAAc,gBAAA,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
package/build/package.mjs CHANGED
@@ -49,6 +49,12 @@ function package_default(route, auth, storage, logger) {
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 res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;AAWA,IAAM,QAAQ,WAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,MAAM,MAAM,MAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,IACJ,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAO,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,MAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJ,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAM,QAAQ;EACvE,MAAM,mBAAmB,WAAW;EAEpC,MAAM,iBAAiB,kBAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,MAAM,sBAAsB;IAC5B,KAAK,UAAU,YAAY,cAAc,QAAQ,oBAAoB;GACvE,OAAO;IACL,MAAM,eAAe;IACrB,KAAK,UAAU,YAAY,cAAc,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJ,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAW,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,MAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,MAAM,mBAAmB,IAAI;IAC7B,IAAI,OAAO,YAAY,gBAAgB,IAAI;GAC7C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,MAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,MAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAa,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,MAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAO,QAAQ,cAAc,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/api",
3
- "version": "9.0.0-next-9.29",
3
+ "version": "9.0.0-next-9.31",
4
4
  "description": "Verdaccio Registry API",
5
5
  "main": "./build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,27 +33,27 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@verdaccio/auth": "9.0.0-next-9.29",
37
- "@verdaccio/config": "9.0.0-next-9.29",
38
- "@verdaccio/core": "9.0.0-next-9.29",
39
- "@verdaccio/hooks": "9.0.0-next-9.29",
40
- "@verdaccio/logger": "9.0.0-next-9.29",
41
- "@verdaccio/middleware": "9.0.0-next-9.29",
42
- "@verdaccio/store": "9.0.0-next-9.29",
36
+ "@verdaccio/auth": "9.0.0-next-9.31",
37
+ "@verdaccio/config": "9.0.0-next-9.31",
38
+ "@verdaccio/core": "9.0.0-next-9.31",
39
+ "@verdaccio/hooks": "9.0.0-next-9.31",
40
+ "@verdaccio/logger": "9.0.0-next-9.31",
41
+ "@verdaccio/middleware": "9.0.0-next-9.31",
42
+ "@verdaccio/store": "9.0.0-next-9.31",
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.30",
50
- "@verdaccio/types": "14.0.0-next-9.13",
49
+ "@verdaccio/test-helper": "5.0.0-next-9.32",
50
+ "@verdaccio/types": "14.0.0-next-9.14",
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",
56
+ "vite": "8.2.2",
57
57
  "vitest": "4.1.10"
58
58
  },
59
59
  "funding": {