@verdaccio/api 9.0.0-next-9.19 → 9.0.0-next-9.21

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/index.js CHANGED
@@ -45,7 +45,7 @@ function src_default(config, auth, storage, logger) {
45
45
  require_dist_tags.default(app, auth, storage, logger);
46
46
  require_publish.default(app, auth, storage, config, logger);
47
47
  require_ping.default(app);
48
- require_search$1.default(app, auth, storage, logger);
48
+ require_search$1.default(app, auth, storage, config, logger);
49
49
  require_token.default(app, auth, storage, config, logger);
50
50
  require_package.default(app, auth, storage, logger);
51
51
  if (config.flags?.webLogin) require_login.default(app, auth, storage, config, logger);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Router } from 'express';\nimport express from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n antiLoop,\n encodeScopePackage,\n enforceGeneratedTokenMetadata,\n makeURLrelative,\n match,\n registerBodyParser,\n validateName,\n validatePackage,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport search from './search';\nimport user from './user';\nimport login from './v1/login';\nimport profile from './v1/profile';\nimport v1Search from './v1/search';\nimport token from './v1/token';\nimport whoami from './whoami';\n\nexport default function (config: Config, auth: Auth, storage: Storage, logger: Logger): Router {\n /* eslint new-cap:off */\n const app = express.Router();\n\n // validate all of these params as a package name\n // this might be too harsh, so ask if it causes trouble\n app.param('package', validatePackage);\n app.param('filename', validateName);\n app.param('tag', validateName);\n app.param('version', validateName);\n app.param('revision', validateName);\n app.param('token', validateName);\n\n // Express route parameter names must be valid JavaScript identifiers, which means\n // they cannot start with a hyphen (-) or contain special characters like dots (.)\n app.param('_rev', match(/^-rev$/));\n app.param('org_couchdb_user', match(/^org\\.couchdb\\.user:/));\n\n // Body parser must be registered before JWT middleware which pauses/resumes the stream\n registerBodyParser(app, config);\n\n // Avoid executing JWT twice when the parent app already registered the JWT middleware\n const apiJwtMiddleware = auth.apiJWTmiddleware();\n app.use((req, res, next) => {\n const remoteUser = (req as any).remote_user ?? (res.locals as any).remote_user;\n if (remoteUser) {\n return next();\n }\n return apiJwtMiddleware(req, res, next);\n });\n\n app.use(enforceGeneratedTokenMetadata(storage, logger));\n app.use(antiLoop(config));\n app.use(makeURLrelative);\n // encode / in a scoped package name to be matched as a single parameter in routes\n app.use(encodeScopePackage);\n // for \"npm whoami\"\n whoami(app);\n profile(app, auth, config);\n search(app, logger);\n user(app, auth, config, logger);\n distTags(app, auth, storage, logger);\n publish(app, auth, storage, config, logger);\n ping(app);\n v1Search(app, auth, storage, logger);\n token(app, auth, storage, config, logger);\n pkg(app, auth, storage, logger);\n if (config.flags?.webLogin) {\n login(app, auth, storage, config, logger);\n }\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,SAAA,YAAyB,QAAgB,MAAY,SAAkB,QAAwB;CAE7F,MAAM,MAAM,QAAA,QAAQ,OAAO;CAI3B,IAAI,MAAM,WAAW,sBAAA,eAAe;CACpC,IAAI,MAAM,YAAY,sBAAA,YAAY;CAClC,IAAI,MAAM,OAAO,sBAAA,YAAY;CAC7B,IAAI,MAAM,WAAW,sBAAA,YAAY;CACjC,IAAI,MAAM,YAAY,sBAAA,YAAY;CAClC,IAAI,MAAM,SAAS,sBAAA,YAAY;CAI/B,IAAI,MAAM,SAAA,GAAA,sBAAA,OAAc,QAAQ,CAAC;CACjC,IAAI,MAAM,qBAAA,GAAA,sBAAA,OAA0B,sBAAsB,CAAC;CAG3D,CAAA,GAAA,sBAAA,oBAAmB,KAAK,MAAM;CAG9B,MAAM,mBAAmB,KAAK,iBAAiB;CAC/C,IAAI,KAAK,KAAK,KAAK,SAAS;EAE1B,IADoB,IAAY,eAAgB,IAAI,OAAe,aAEjE,OAAO,KAAK;EAEd,OAAO,iBAAiB,KAAK,KAAK,IAAI;CACxC,CAAC;CAED,IAAI,KAAA,GAAA,sBAAA,+BAAkC,SAAS,MAAM,CAAC;CACtD,IAAI,KAAA,GAAA,sBAAA,UAAa,MAAM,CAAC;CACxB,IAAI,IAAI,sBAAA,eAAe;CAEvB,IAAI,IAAI,sBAAA,kBAAkB;CAE1B,eAAA,QAAO,GAAG;CACV,gBAAA,QAAQ,KAAK,MAAM,MAAM;CACzB,eAAA,QAAO,KAAK,MAAM;CAClB,aAAA,QAAK,KAAK,MAAM,QAAQ,MAAM;CAC9B,kBAAA,QAAS,KAAK,MAAM,SAAS,MAAM;CACnC,gBAAA,QAAQ,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC1C,aAAA,QAAK,GAAG;CACR,iBAAA,QAAS,KAAK,MAAM,SAAS,MAAM;CACnC,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CACxC,gBAAA,QAAI,KAAK,MAAM,SAAS,MAAM;CAC9B,IAAI,OAAO,OAAO,UAChB,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CAE1C,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Router } from 'express';\nimport express from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n antiLoop,\n encodeScopePackage,\n enforceGeneratedTokenMetadata,\n makeURLrelative,\n match,\n registerBodyParser,\n validateName,\n validatePackage,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport search from './search';\nimport user from './user';\nimport login from './v1/login';\nimport profile from './v1/profile';\nimport v1Search from './v1/search';\nimport token from './v1/token';\nimport whoami from './whoami';\n\nexport default function (config: Config, auth: Auth, storage: Storage, logger: Logger): Router {\n /* eslint new-cap:off */\n const app = express.Router();\n\n // validate all of these params as a package name\n // this might be too harsh, so ask if it causes trouble\n app.param('package', validatePackage);\n app.param('filename', validateName);\n app.param('tag', validateName);\n app.param('version', validateName);\n app.param('revision', validateName);\n app.param('token', validateName);\n\n // Express route parameter names must be valid JavaScript identifiers, which means\n // they cannot start with a hyphen (-) or contain special characters like dots (.)\n app.param('_rev', match(/^-rev$/));\n app.param('org_couchdb_user', match(/^org\\.couchdb\\.user:/));\n\n // Body parser must be registered before JWT middleware which pauses/resumes the stream\n registerBodyParser(app, config);\n\n // Avoid executing JWT twice when the parent app already registered the JWT middleware\n const apiJwtMiddleware = auth.apiJWTmiddleware();\n app.use((req, res, next) => {\n const remoteUser = (req as any).remote_user ?? (res.locals as any).remote_user;\n if (remoteUser) {\n return next();\n }\n return apiJwtMiddleware(req, res, next);\n });\n\n app.use(enforceGeneratedTokenMetadata(storage, logger));\n app.use(antiLoop(config));\n app.use(makeURLrelative);\n // encode / in a scoped package name to be matched as a single parameter in routes\n app.use(encodeScopePackage);\n // for \"npm whoami\"\n whoami(app);\n profile(app, auth, config);\n search(app, logger);\n user(app, auth, config, logger);\n distTags(app, auth, storage, logger);\n publish(app, auth, storage, config, logger);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger);\n pkg(app, auth, storage, logger);\n if (config.flags?.webLogin) {\n login(app, auth, storage, config, logger);\n }\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,SAAA,YAAyB,QAAgB,MAAY,SAAkB,QAAwB;CAE7F,MAAM,MAAM,QAAA,QAAQ,OAAO;CAI3B,IAAI,MAAM,WAAW,sBAAA,eAAe;CACpC,IAAI,MAAM,YAAY,sBAAA,YAAY;CAClC,IAAI,MAAM,OAAO,sBAAA,YAAY;CAC7B,IAAI,MAAM,WAAW,sBAAA,YAAY;CACjC,IAAI,MAAM,YAAY,sBAAA,YAAY;CAClC,IAAI,MAAM,SAAS,sBAAA,YAAY;CAI/B,IAAI,MAAM,SAAA,GAAA,sBAAA,OAAc,QAAQ,CAAC;CACjC,IAAI,MAAM,qBAAA,GAAA,sBAAA,OAA0B,sBAAsB,CAAC;CAG3D,CAAA,GAAA,sBAAA,oBAAmB,KAAK,MAAM;CAG9B,MAAM,mBAAmB,KAAK,iBAAiB;CAC/C,IAAI,KAAK,KAAK,KAAK,SAAS;EAE1B,IADoB,IAAY,eAAgB,IAAI,OAAe,aAEjE,OAAO,KAAK;EAEd,OAAO,iBAAiB,KAAK,KAAK,IAAI;CACxC,CAAC;CAED,IAAI,KAAA,GAAA,sBAAA,+BAAkC,SAAS,MAAM,CAAC;CACtD,IAAI,KAAA,GAAA,sBAAA,UAAa,MAAM,CAAC;CACxB,IAAI,IAAI,sBAAA,eAAe;CAEvB,IAAI,IAAI,sBAAA,kBAAkB;CAE1B,eAAA,QAAO,GAAG;CACV,gBAAA,QAAQ,KAAK,MAAM,MAAM;CACzB,eAAA,QAAO,KAAK,MAAM;CAClB,aAAA,QAAK,KAAK,MAAM,QAAQ,MAAM;CAC9B,kBAAA,QAAS,KAAK,MAAM,SAAS,MAAM;CACnC,gBAAA,QAAQ,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC1C,aAAA,QAAK,GAAG;CACR,iBAAA,QAAS,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC3C,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CACxC,gBAAA,QAAI,KAAK,MAAM,SAAS,MAAM;CAC9B,IAAI,OAAO,OAAO,UAChB,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CAE1C,OAAO;AACT"}
package/build/index.mjs CHANGED
@@ -39,7 +39,7 @@ function src_default(config, auth, storage, logger) {
39
39
  dist_tags_default(app, auth, storage, logger);
40
40
  publish(app, auth, storage, config, logger);
41
41
  ping_default(app);
42
- search_default$1(app, auth, storage, logger);
42
+ search_default$1(app, auth, storage, config, logger);
43
43
  token_default(app, auth, storage, config, logger);
44
44
  package_default(app, auth, storage, logger);
45
45
  if (config.flags?.webLogin) login_default(app, auth, storage, config, logger);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Router } from 'express';\nimport express from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n antiLoop,\n encodeScopePackage,\n enforceGeneratedTokenMetadata,\n makeURLrelative,\n match,\n registerBodyParser,\n validateName,\n validatePackage,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport search from './search';\nimport user from './user';\nimport login from './v1/login';\nimport profile from './v1/profile';\nimport v1Search from './v1/search';\nimport token from './v1/token';\nimport whoami from './whoami';\n\nexport default function (config: Config, auth: Auth, storage: Storage, logger: Logger): Router {\n /* eslint new-cap:off */\n const app = express.Router();\n\n // validate all of these params as a package name\n // this might be too harsh, so ask if it causes trouble\n app.param('package', validatePackage);\n app.param('filename', validateName);\n app.param('tag', validateName);\n app.param('version', validateName);\n app.param('revision', validateName);\n app.param('token', validateName);\n\n // Express route parameter names must be valid JavaScript identifiers, which means\n // they cannot start with a hyphen (-) or contain special characters like dots (.)\n app.param('_rev', match(/^-rev$/));\n app.param('org_couchdb_user', match(/^org\\.couchdb\\.user:/));\n\n // Body parser must be registered before JWT middleware which pauses/resumes the stream\n registerBodyParser(app, config);\n\n // Avoid executing JWT twice when the parent app already registered the JWT middleware\n const apiJwtMiddleware = auth.apiJWTmiddleware();\n app.use((req, res, next) => {\n const remoteUser = (req as any).remote_user ?? (res.locals as any).remote_user;\n if (remoteUser) {\n return next();\n }\n return apiJwtMiddleware(req, res, next);\n });\n\n app.use(enforceGeneratedTokenMetadata(storage, logger));\n app.use(antiLoop(config));\n app.use(makeURLrelative);\n // encode / in a scoped package name to be matched as a single parameter in routes\n app.use(encodeScopePackage);\n // for \"npm whoami\"\n whoami(app);\n profile(app, auth, config);\n search(app, logger);\n user(app, auth, config, logger);\n distTags(app, auth, storage, logger);\n publish(app, auth, storage, config, logger);\n ping(app);\n v1Search(app, auth, storage, logger);\n token(app, auth, storage, config, logger);\n pkg(app, auth, storage, logger);\n if (config.flags?.webLogin) {\n login(app, auth, storage, config, logger);\n }\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,SAAA,YAAyB,QAAgB,MAAY,SAAkB,QAAwB;CAE7F,MAAM,MAAM,QAAQ,OAAO;CAI3B,IAAI,MAAM,WAAW,eAAe;CACpC,IAAI,MAAM,YAAY,YAAY;CAClC,IAAI,MAAM,OAAO,YAAY;CAC7B,IAAI,MAAM,WAAW,YAAY;CACjC,IAAI,MAAM,YAAY,YAAY;CAClC,IAAI,MAAM,SAAS,YAAY;CAI/B,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;CACjC,IAAI,MAAM,oBAAoB,MAAM,sBAAsB,CAAC;CAG3D,mBAAmB,KAAK,MAAM;CAG9B,MAAM,mBAAmB,KAAK,iBAAiB;CAC/C,IAAI,KAAK,KAAK,KAAK,SAAS;EAE1B,IADoB,IAAY,eAAgB,IAAI,OAAe,aAEjE,OAAO,KAAK;EAEd,OAAO,iBAAiB,KAAK,KAAK,IAAI;CACxC,CAAC;CAED,IAAI,IAAI,8BAA8B,SAAS,MAAM,CAAC;CACtD,IAAI,IAAI,SAAS,MAAM,CAAC;CACxB,IAAI,IAAI,eAAe;CAEvB,IAAI,IAAI,kBAAkB;CAE1B,eAAO,GAAG;CACV,gBAAQ,KAAK,MAAM,MAAM;CACzB,eAAO,KAAK,MAAM;CAClB,aAAK,KAAK,MAAM,QAAQ,MAAM;CAC9B,kBAAS,KAAK,MAAM,SAAS,MAAM;CACnC,QAAQ,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC1C,aAAK,GAAG;CACR,iBAAS,KAAK,MAAM,SAAS,MAAM;CACnC,cAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CACxC,gBAAI,KAAK,MAAM,SAAS,MAAM;CAC9B,IAAI,OAAO,OAAO,UAChB,cAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CAE1C,OAAO;AACT"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Router } from 'express';\nimport express from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n antiLoop,\n encodeScopePackage,\n enforceGeneratedTokenMetadata,\n makeURLrelative,\n match,\n registerBodyParser,\n validateName,\n validatePackage,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport search from './search';\nimport user from './user';\nimport login from './v1/login';\nimport profile from './v1/profile';\nimport v1Search from './v1/search';\nimport token from './v1/token';\nimport whoami from './whoami';\n\nexport default function (config: Config, auth: Auth, storage: Storage, logger: Logger): Router {\n /* eslint new-cap:off */\n const app = express.Router();\n\n // validate all of these params as a package name\n // this might be too harsh, so ask if it causes trouble\n app.param('package', validatePackage);\n app.param('filename', validateName);\n app.param('tag', validateName);\n app.param('version', validateName);\n app.param('revision', validateName);\n app.param('token', validateName);\n\n // Express route parameter names must be valid JavaScript identifiers, which means\n // they cannot start with a hyphen (-) or contain special characters like dots (.)\n app.param('_rev', match(/^-rev$/));\n app.param('org_couchdb_user', match(/^org\\.couchdb\\.user:/));\n\n // Body parser must be registered before JWT middleware which pauses/resumes the stream\n registerBodyParser(app, config);\n\n // Avoid executing JWT twice when the parent app already registered the JWT middleware\n const apiJwtMiddleware = auth.apiJWTmiddleware();\n app.use((req, res, next) => {\n const remoteUser = (req as any).remote_user ?? (res.locals as any).remote_user;\n if (remoteUser) {\n return next();\n }\n return apiJwtMiddleware(req, res, next);\n });\n\n app.use(enforceGeneratedTokenMetadata(storage, logger));\n app.use(antiLoop(config));\n app.use(makeURLrelative);\n // encode / in a scoped package name to be matched as a single parameter in routes\n app.use(encodeScopePackage);\n // for \"npm whoami\"\n whoami(app);\n profile(app, auth, config);\n search(app, logger);\n user(app, auth, config, logger);\n distTags(app, auth, storage, logger);\n publish(app, auth, storage, config, logger);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger);\n pkg(app, auth, storage, logger);\n if (config.flags?.webLogin) {\n login(app, auth, storage, config, logger);\n }\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,SAAA,YAAyB,QAAgB,MAAY,SAAkB,QAAwB;CAE7F,MAAM,MAAM,QAAQ,OAAO;CAI3B,IAAI,MAAM,WAAW,eAAe;CACpC,IAAI,MAAM,YAAY,YAAY;CAClC,IAAI,MAAM,OAAO,YAAY;CAC7B,IAAI,MAAM,WAAW,YAAY;CACjC,IAAI,MAAM,YAAY,YAAY;CAClC,IAAI,MAAM,SAAS,YAAY;CAI/B,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;CACjC,IAAI,MAAM,oBAAoB,MAAM,sBAAsB,CAAC;CAG3D,mBAAmB,KAAK,MAAM;CAG9B,MAAM,mBAAmB,KAAK,iBAAiB;CAC/C,IAAI,KAAK,KAAK,KAAK,SAAS;EAE1B,IADoB,IAAY,eAAgB,IAAI,OAAe,aAEjE,OAAO,KAAK;EAEd,OAAO,iBAAiB,KAAK,KAAK,IAAI;CACxC,CAAC;CAED,IAAI,IAAI,8BAA8B,SAAS,MAAM,CAAC;CACtD,IAAI,IAAI,SAAS,MAAM,CAAC;CACxB,IAAI,IAAI,eAAe;CAEvB,IAAI,IAAI,kBAAkB;CAE1B,eAAO,GAAG;CACV,gBAAQ,KAAK,MAAM,MAAM;CACzB,eAAO,KAAK,MAAM;CAClB,aAAK,KAAK,MAAM,QAAQ,MAAM;CAC9B,kBAAS,KAAK,MAAM,SAAS,MAAM;CACnC,QAAQ,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC1C,aAAK,GAAG;CACR,iBAAS,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC3C,cAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CACxC,gBAAI,KAAK,MAAM,SAAS,MAAM;CAC9B,IAAI,OAAO,OAAO,UAChB,cAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CAE1C,OAAO;AACT"}
package/build/publish.js CHANGED
@@ -147,7 +147,6 @@ function publish(router, auth, storage, config, logger) {
147
147
  const version = _verdaccio_core.tarballUtils.getVersionFromTarball(filename);
148
148
  const metadata = {
149
149
  name: packageName,
150
- version,
151
150
  _rev: revision
152
151
  };
153
152
  const publishedPackage = version ? `${packageName}@${version}` : packageName;
@@ -1 +1 @@
1
- {"version":3,"file":"publish.js","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, tarballUtils } from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n PUBLISH_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n media,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, Manifest } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:publish');\n\n/**\n * Publish a package / update package\n *\n * There are multiples scenarios here to be considered:\n *\n * 1. Publish scenario\n *\n * Publish a package consist of at least 1 step (PUT) with a metadata payload.\n * When a package is published, an _attachment property is present that contains the data\n * of the tarball.\n *\n * Example flow of publish.\n *\n * npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1 9627ms\n npm info lifecycle @scope/test1@1.0.1~publish: @scope/test1@1.0.1\n npm info lifecycle @scope/test1@1.0.1~postpublish: @scope/test1@1.0.1\n + @scope/test1@1.0.1\n npm verb exit [ 0, true ]\n *\n *\n * 2. Unpublish scenario\n *\n * Unpublish consist in 3 steps.\n * 1. Try to fetch metadata -> if it fails, return 404\n * 2. Compute metadata locally (client side) and send a mutate payload excluding the version to\n * be unpublished\n * eg: if metadata reflects 1.0.1, 1.0.2 and 1.0.3, the computed metadata won't include 1.0.3.\n * 3. Once the second step has been successfully finished, delete the tarball.\n *\n * All these steps are consecutive and required, there is no transacions here, if step 3 fails,\n * metadata might get corrupted.\n *\n * Note the unpublish call will suffix in the url a /-rev/14-5d500cfce92f90fd revision number,\n * this not\n * used internally.\n *\n *\n * Example flow of unpublish.\n *\n * There are two possible flows:\n *\n * - Remove all packages (entirely)\n * eg: npm unpublish package-name@* --force\n * eg: npm unpublish package-name --force\n *\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * - Remove a specific version\n * eg: npm unpublish package-name@1.0.0 --force\n *\n * Get fresh manifest\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * Update manifest without the version to be unpublished\n * npm http fetch PUT 201 http://localhost:4873/custom-name/-rev/14-5d500cfce92f90fd 956606ms\n * Get fresh manifest (revision should be different)\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1601ms\n * Remove the tarball\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * 3. Change owners of a package\n *\n * Changing owners (maintainers) of a package uses the publish endpoint.\n *\n * The body includes a list of the new owners with the following format\n *\n * {\n \"_id\": pkgName,\n \"_rev\": \"4-b0cdaefc9bdb77c8\",\n \"maintainers\": [\n { \"name\": \"first owner\", \"email\": \"me@verdaccio.org\" },\n { \"name\": \"second owner\", \"email\": \"you@verdaccio.org\" },\n ...\n ]\n }\n *\n */\nexport default function publish(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\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 router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish one version')\n );\n\n router.put(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision')\n );\n\n /**\n * Un-publishing an entire package.\n *\n * This scenario happens when any of these scenarios happens:\n * - the first call detect there is only one version remaining\n * - no version is specified in the unpublish call\n * - all versions are removed npm unpublish package@*\n * - there is no versions on the metadata\n\n * then the client decides to DELETE the resource\n * Example:\n * Get fresh manifest (write=true is a flag to get the latest revision)\n * npm http fetch GET 304 http://localhost:4873/package-name?write=true 1076ms (from cache)\n * Send request to delete the package, this includes the revision number that must match\n * and the package name, it will delete the entire package and all tarballs (or tarball depends the scenario)\n * npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = req.params.package;\n const rev = req.params.revision;\n const username = req?.remote_user?.name;\n\n logger.debug({ packageName }, `unpublishing @{packageName}`);\n try {\n await storage.removePackage(packageName, rev, username);\n debug('package %s unpublished', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of package removal\n const metadata: Partial<Manifest> = { name: packageName, _rev: rev };\n\n void notify(metadata, config, req.remote_user, packageName, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.PKG_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /*\n Remove a tarball, this happens when npm unpublish a package unique version.\n npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.remove_tarball,\n can('unpublish'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = req.params.package;\n const { filename, revision } = req.params;\n const username = req?.remote_user?.name;\n\n logger.debug(\n { packageName, filename, revision },\n `removing a tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n try {\n await storage.removeTarball(packageName, filename, revision, username);\n res.status(HTTP_STATUS.CREATED);\n\n logger.debug(\n { packageName, filename, revision },\n `success remove tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n\n // send notification of version removal\n // getVersionFromTarball returns undefined when the filename is not parseable;\n // fall back to the package name so we never report `name@undefined`\n const version = tarballUtils.getVersionFromTarball(filename);\n const metadata: Partial<Manifest> = { name: packageName, version, _rev: revision };\n const publishedPackage = version ? `${packageName}@${version}` : packageName;\n\n void notify(metadata, config, req.remote_user, publishedPackage, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.TARBALL_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n\nexport function publishPackage(\n storage: Storage,\n config: Config,\n logger: Logger,\n origin: string\n): any {\n return async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n debug(origin);\n const ac = new AbortController();\n const packageName = req.params.package;\n const { revision } = req.params;\n debug('publishing package %s', packageName);\n debug('revision %s', revision);\n if (debug.enabled) {\n debug('body %o', req.body);\n }\n const metadata = req.body;\n\n logger.debug({ packageName }, `publishing package @{packageName}`);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const message = await storage.updateManifest(metadata, {\n name: packageName,\n revision,\n signal: ac.signal,\n requestOptions,\n uplinksLook: false,\n });\n debug('package %s published', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of publication (notification step, non transactional)\n // a publish body is a packument; the published version is the single entry\n // under `versions` (see storage.updateManifest), not a top-level field\n const [version] = Object.keys(metadata.versions ?? {});\n void notify(\n metadata,\n config,\n req.remote_user,\n `${metadata.name}@${version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n return next({\n success: true,\n ok: message,\n });\n } catch (err: any) {\n // TODO: review if we need the abort controller here\n next(err);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publish.js","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, tarballUtils } from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n PUBLISH_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n media,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, Manifest } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:publish');\n\n/**\n * Publish a package / update package\n *\n * There are multiples scenarios here to be considered:\n *\n * 1. Publish scenario\n *\n * Publish a package consist of at least 1 step (PUT) with a metadata payload.\n * When a package is published, an _attachment property is present that contains the data\n * of the tarball.\n *\n * Example flow of publish.\n *\n * npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1 9627ms\n npm info lifecycle @scope/test1@1.0.1~publish: @scope/test1@1.0.1\n npm info lifecycle @scope/test1@1.0.1~postpublish: @scope/test1@1.0.1\n + @scope/test1@1.0.1\n npm verb exit [ 0, true ]\n *\n *\n * 2. Unpublish scenario\n *\n * Unpublish consist in 3 steps.\n * 1. Try to fetch metadata -> if it fails, return 404\n * 2. Compute metadata locally (client side) and send a mutate payload excluding the version to\n * be unpublished\n * eg: if metadata reflects 1.0.1, 1.0.2 and 1.0.3, the computed metadata won't include 1.0.3.\n * 3. Once the second step has been successfully finished, delete the tarball.\n *\n * All these steps are consecutive and required, there is no transacions here, if step 3 fails,\n * metadata might get corrupted.\n *\n * Note the unpublish call will suffix in the url a /-rev/14-5d500cfce92f90fd revision number,\n * this not\n * used internally.\n *\n *\n * Example flow of unpublish.\n *\n * There are two possible flows:\n *\n * - Remove all packages (entirely)\n * eg: npm unpublish package-name@* --force\n * eg: npm unpublish package-name --force\n *\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * - Remove a specific version\n * eg: npm unpublish package-name@1.0.0 --force\n *\n * Get fresh manifest\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * Update manifest without the version to be unpublished\n * npm http fetch PUT 201 http://localhost:4873/custom-name/-rev/14-5d500cfce92f90fd 956606ms\n * Get fresh manifest (revision should be different)\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1601ms\n * Remove the tarball\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * 3. Change owners of a package\n *\n * Changing owners (maintainers) of a package uses the publish endpoint.\n *\n * The body includes a list of the new owners with the following format\n *\n * {\n \"_id\": pkgName,\n \"_rev\": \"4-b0cdaefc9bdb77c8\",\n \"maintainers\": [\n { \"name\": \"first owner\", \"email\": \"me@verdaccio.org\" },\n { \"name\": \"second owner\", \"email\": \"you@verdaccio.org\" },\n ...\n ]\n }\n *\n */\nexport default function publish(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\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 router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish one version')\n );\n\n router.put(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision')\n );\n\n /**\n * Un-publishing an entire package.\n *\n * This scenario happens when any of these scenarios happens:\n * - the first call detect there is only one version remaining\n * - no version is specified in the unpublish call\n * - all versions are removed npm unpublish package@*\n * - there is no versions on the metadata\n\n * then the client decides to DELETE the resource\n * Example:\n * Get fresh manifest (write=true is a flag to get the latest revision)\n * npm http fetch GET 304 http://localhost:4873/package-name?write=true 1076ms (from cache)\n * Send request to delete the package, this includes the revision number that must match\n * and the package name, it will delete the entire package and all tarballs (or tarball depends the scenario)\n * npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = req.params.package;\n const rev = req.params.revision;\n const username = req?.remote_user?.name;\n\n logger.debug({ packageName }, `unpublishing @{packageName}`);\n try {\n await storage.removePackage(packageName, rev, username);\n debug('package %s unpublished', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of package removal\n const metadata: Partial<Manifest> = { name: packageName, _rev: rev };\n\n void notify(metadata, config, req.remote_user, packageName, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.PKG_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /*\n Remove a tarball, this happens when npm unpublish a package unique version.\n npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.remove_tarball,\n can('unpublish'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = req.params.package;\n const { filename, revision } = req.params;\n const username = req?.remote_user?.name;\n\n logger.debug(\n { packageName, filename, revision },\n `removing a tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n try {\n await storage.removeTarball(packageName, filename, revision, username);\n res.status(HTTP_STATUS.CREATED);\n\n logger.debug(\n { packageName, filename, revision },\n `success remove tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n\n // send notification of version removal\n // getVersionFromTarball returns undefined when the filename is not parseable;\n // fall back to the package name so we never report `name@undefined`\n const version = tarballUtils.getVersionFromTarball(filename);\n const metadata: Partial<Manifest> = { name: packageName, _rev: revision };\n const publishedPackage = version ? `${packageName}@${version}` : packageName;\n\n void notify(metadata, config, req.remote_user, publishedPackage, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.TARBALL_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n\nexport function publishPackage(\n storage: Storage,\n config: Config,\n logger: Logger,\n origin: string\n): any {\n return async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n debug(origin);\n const ac = new AbortController();\n const packageName = req.params.package;\n const { revision } = req.params;\n debug('publishing package %s', packageName);\n debug('revision %s', revision);\n if (debug.enabled) {\n debug('body %o', req.body);\n }\n const metadata = req.body;\n\n logger.debug({ packageName }, `publishing package @{packageName}`);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const message = await storage.updateManifest(metadata, {\n name: packageName,\n revision,\n signal: ac.signal,\n requestOptions,\n uplinksLook: false,\n });\n debug('package %s published', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of publication (notification step, non transactional)\n // a publish body is a packument; the published version is the single entry\n // under `versions` (see storage.updateManifest), not a top-level field\n const [version] = Object.keys(metadata.versions ?? {});\n void notify(\n metadata,\n config,\n req.remote_user,\n `${metadata.name}@${version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n return next({\n success: true,\n ok: message,\n });\n } catch (err: any) {\n // TODO: review if we need the abort controller here\n next(err);\n }\n };\n}\n"],"mappings":"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"}
package/build/publish.mjs CHANGED
@@ -145,7 +145,6 @@ function publish(router, auth, storage, config, logger) {
145
145
  const version = tarballUtils.getVersionFromTarball(filename);
146
146
  const metadata = {
147
147
  name: packageName,
148
- version,
149
148
  _rev: revision
150
149
  };
151
150
  const publishedPackage = version ? `${packageName}@${version}` : packageName;
@@ -1 +1 @@
1
- {"version":3,"file":"publish.mjs","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, tarballUtils } from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n PUBLISH_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n media,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, Manifest } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:publish');\n\n/**\n * Publish a package / update package\n *\n * There are multiples scenarios here to be considered:\n *\n * 1. Publish scenario\n *\n * Publish a package consist of at least 1 step (PUT) with a metadata payload.\n * When a package is published, an _attachment property is present that contains the data\n * of the tarball.\n *\n * Example flow of publish.\n *\n * npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1 9627ms\n npm info lifecycle @scope/test1@1.0.1~publish: @scope/test1@1.0.1\n npm info lifecycle @scope/test1@1.0.1~postpublish: @scope/test1@1.0.1\n + @scope/test1@1.0.1\n npm verb exit [ 0, true ]\n *\n *\n * 2. Unpublish scenario\n *\n * Unpublish consist in 3 steps.\n * 1. Try to fetch metadata -> if it fails, return 404\n * 2. Compute metadata locally (client side) and send a mutate payload excluding the version to\n * be unpublished\n * eg: if metadata reflects 1.0.1, 1.0.2 and 1.0.3, the computed metadata won't include 1.0.3.\n * 3. Once the second step has been successfully finished, delete the tarball.\n *\n * All these steps are consecutive and required, there is no transacions here, if step 3 fails,\n * metadata might get corrupted.\n *\n * Note the unpublish call will suffix in the url a /-rev/14-5d500cfce92f90fd revision number,\n * this not\n * used internally.\n *\n *\n * Example flow of unpublish.\n *\n * There are two possible flows:\n *\n * - Remove all packages (entirely)\n * eg: npm unpublish package-name@* --force\n * eg: npm unpublish package-name --force\n *\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * - Remove a specific version\n * eg: npm unpublish package-name@1.0.0 --force\n *\n * Get fresh manifest\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * Update manifest without the version to be unpublished\n * npm http fetch PUT 201 http://localhost:4873/custom-name/-rev/14-5d500cfce92f90fd 956606ms\n * Get fresh manifest (revision should be different)\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1601ms\n * Remove the tarball\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * 3. Change owners of a package\n *\n * Changing owners (maintainers) of a package uses the publish endpoint.\n *\n * The body includes a list of the new owners with the following format\n *\n * {\n \"_id\": pkgName,\n \"_rev\": \"4-b0cdaefc9bdb77c8\",\n \"maintainers\": [\n { \"name\": \"first owner\", \"email\": \"me@verdaccio.org\" },\n { \"name\": \"second owner\", \"email\": \"you@verdaccio.org\" },\n ...\n ]\n }\n *\n */\nexport default function publish(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\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 router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish one version')\n );\n\n router.put(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision')\n );\n\n /**\n * Un-publishing an entire package.\n *\n * This scenario happens when any of these scenarios happens:\n * - the first call detect there is only one version remaining\n * - no version is specified in the unpublish call\n * - all versions are removed npm unpublish package@*\n * - there is no versions on the metadata\n\n * then the client decides to DELETE the resource\n * Example:\n * Get fresh manifest (write=true is a flag to get the latest revision)\n * npm http fetch GET 304 http://localhost:4873/package-name?write=true 1076ms (from cache)\n * Send request to delete the package, this includes the revision number that must match\n * and the package name, it will delete the entire package and all tarballs (or tarball depends the scenario)\n * npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = req.params.package;\n const rev = req.params.revision;\n const username = req?.remote_user?.name;\n\n logger.debug({ packageName }, `unpublishing @{packageName}`);\n try {\n await storage.removePackage(packageName, rev, username);\n debug('package %s unpublished', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of package removal\n const metadata: Partial<Manifest> = { name: packageName, _rev: rev };\n\n void notify(metadata, config, req.remote_user, packageName, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.PKG_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /*\n Remove a tarball, this happens when npm unpublish a package unique version.\n npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.remove_tarball,\n can('unpublish'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = req.params.package;\n const { filename, revision } = req.params;\n const username = req?.remote_user?.name;\n\n logger.debug(\n { packageName, filename, revision },\n `removing a tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n try {\n await storage.removeTarball(packageName, filename, revision, username);\n res.status(HTTP_STATUS.CREATED);\n\n logger.debug(\n { packageName, filename, revision },\n `success remove tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n\n // send notification of version removal\n // getVersionFromTarball returns undefined when the filename is not parseable;\n // fall back to the package name so we never report `name@undefined`\n const version = tarballUtils.getVersionFromTarball(filename);\n const metadata: Partial<Manifest> = { name: packageName, version, _rev: revision };\n const publishedPackage = version ? `${packageName}@${version}` : packageName;\n\n void notify(metadata, config, req.remote_user, publishedPackage, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.TARBALL_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n\nexport function publishPackage(\n storage: Storage,\n config: Config,\n logger: Logger,\n origin: string\n): any {\n return async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n debug(origin);\n const ac = new AbortController();\n const packageName = req.params.package;\n const { revision } = req.params;\n debug('publishing package %s', packageName);\n debug('revision %s', revision);\n if (debug.enabled) {\n debug('body %o', req.body);\n }\n const metadata = req.body;\n\n logger.debug({ packageName }, `publishing package @{packageName}`);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const message = await storage.updateManifest(metadata, {\n name: packageName,\n revision,\n signal: ac.signal,\n requestOptions,\n uplinksLook: false,\n });\n debug('package %s published', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of publication (notification step, non transactional)\n // a publish body is a packument; the published version is the single entry\n // under `versions` (see storage.updateManifest), not a top-level field\n const [version] = Object.keys(metadata.versions ?? {});\n void notify(\n metadata,\n config,\n req.remote_user,\n `${metadata.name}@${version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n return next({\n success: true,\n ok: message,\n });\n } catch (err: any) {\n // TODO: review if we need the abort controller here\n next(err);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publish.mjs","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, tarballUtils } from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n PUBLISH_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n media,\n} from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, Manifest } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:publish');\n\n/**\n * Publish a package / update package\n *\n * There are multiples scenarios here to be considered:\n *\n * 1. Publish scenario\n *\n * Publish a package consist of at least 1 step (PUT) with a metadata payload.\n * When a package is published, an _attachment property is present that contains the data\n * of the tarball.\n *\n * Example flow of publish.\n *\n * npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1 9627ms\n npm info lifecycle @scope/test1@1.0.1~publish: @scope/test1@1.0.1\n npm info lifecycle @scope/test1@1.0.1~postpublish: @scope/test1@1.0.1\n + @scope/test1@1.0.1\n npm verb exit [ 0, true ]\n *\n *\n * 2. Unpublish scenario\n *\n * Unpublish consist in 3 steps.\n * 1. Try to fetch metadata -> if it fails, return 404\n * 2. Compute metadata locally (client side) and send a mutate payload excluding the version to\n * be unpublished\n * eg: if metadata reflects 1.0.1, 1.0.2 and 1.0.3, the computed metadata won't include 1.0.3.\n * 3. Once the second step has been successfully finished, delete the tarball.\n *\n * All these steps are consecutive and required, there is no transacions here, if step 3 fails,\n * metadata might get corrupted.\n *\n * Note the unpublish call will suffix in the url a /-rev/14-5d500cfce92f90fd revision number,\n * this not\n * used internally.\n *\n *\n * Example flow of unpublish.\n *\n * There are two possible flows:\n *\n * - Remove all packages (entirely)\n * eg: npm unpublish package-name@* --force\n * eg: npm unpublish package-name --force\n *\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * - Remove a specific version\n * eg: npm unpublish package-name@1.0.0 --force\n *\n * Get fresh manifest\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms\n * Update manifest without the version to be unpublished\n * npm http fetch PUT 201 http://localhost:4873/custom-name/-rev/14-5d500cfce92f90fd 956606ms\n * Get fresh manifest (revision should be different)\n * npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1601ms\n * Remove the tarball\n * npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms\n *\n * 3. Change owners of a package\n *\n * Changing owners (maintainers) of a package uses the publish endpoint.\n *\n * The body includes a list of the new owners with the following format\n *\n * {\n \"_id\": pkgName,\n \"_rev\": \"4-b0cdaefc9bdb77c8\",\n \"maintainers\": [\n { \"name\": \"first owner\", \"email\": \"me@verdaccio.org\" },\n { \"name\": \"second owner\", \"email\": \"you@verdaccio.org\" },\n ...\n ]\n }\n *\n */\nexport default function publish(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\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 router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish one version')\n );\n\n router.put(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision')\n );\n\n /**\n * Un-publishing an entire package.\n *\n * This scenario happens when any of these scenarios happens:\n * - the first call detect there is only one version remaining\n * - no version is specified in the unpublish call\n * - all versions are removed npm unpublish package@*\n * - there is no versions on the metadata\n\n * then the client decides to DELETE the resource\n * Example:\n * Get fresh manifest (write=true is a flag to get the latest revision)\n * npm http fetch GET 304 http://localhost:4873/package-name?write=true 1076ms (from cache)\n * Send request to delete the package, this includes the revision number that must match\n * and the package name, it will delete the entire package and all tarballs (or tarball depends the scenario)\n * npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.publish_package,\n can('unpublish'),\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = req.params.package;\n const rev = req.params.revision;\n const username = req?.remote_user?.name;\n\n logger.debug({ packageName }, `unpublishing @{packageName}`);\n try {\n await storage.removePackage(packageName, rev, username);\n debug('package %s unpublished', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of package removal\n const metadata: Partial<Manifest> = { name: packageName, _rev: rev };\n\n void notify(metadata, config, req.remote_user, packageName, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.PKG_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /*\n Remove a tarball, this happens when npm unpublish a package unique version.\n npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms\n */\n router.delete(\n PUBLISH_API_ENDPOINTS.remove_tarball,\n can('unpublish'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = req.params.package;\n const { filename, revision } = req.params;\n const username = req?.remote_user?.name;\n\n logger.debug(\n { packageName, filename, revision },\n `removing a tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n try {\n await storage.removeTarball(packageName, filename, revision, username);\n res.status(HTTP_STATUS.CREATED);\n\n logger.debug(\n { packageName, filename, revision },\n `success remove tarball for @{packageName}-@{tarballName}-@{revision}`\n );\n\n // send notification of version removal\n // getVersionFromTarball returns undefined when the filename is not parseable;\n // fall back to the package name so we never report `name@undefined`\n const version = tarballUtils.getVersionFromTarball(filename);\n const metadata: Partial<Manifest> = { name: packageName, _rev: revision };\n const publishedPackage = version ? `${packageName}@${version}` : packageName;\n\n void notify(metadata, config, req.remote_user, publishedPackage, 'unpublish').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n return next({ ok: API_MESSAGE.TARBALL_REMOVED });\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n\nexport function publishPackage(\n storage: Storage,\n config: Config,\n logger: Logger,\n origin: string\n): any {\n return async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n debug(origin);\n const ac = new AbortController();\n const packageName = req.params.package;\n const { revision } = req.params;\n debug('publishing package %s', packageName);\n debug('revision %s', revision);\n if (debug.enabled) {\n debug('body %o', req.body);\n }\n const metadata = req.body;\n\n logger.debug({ packageName }, `publishing package @{packageName}`);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const message = await storage.updateManifest(metadata, {\n name: packageName,\n revision,\n signal: ac.signal,\n requestOptions,\n uplinksLook: false,\n });\n debug('package %s published', packageName);\n res.status(HTTP_STATUS.CREATED);\n\n // send notification of publication (notification step, non transactional)\n // a publish body is a packument; the published version is the single entry\n // under `versions` (see storage.updateManifest), not a top-level field\n const [version] = Object.keys(metadata.versions ?? {});\n void notify(\n metadata,\n config,\n req.remote_user,\n `${metadata.name}@${version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n return next({\n success: true,\n ok: message,\n });\n } catch (err: any) {\n // TODO: review if we need the abort controller here\n next(err);\n }\n };\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { Auth } from '@verdaccio/auth';
2
2
  import { Storage } from '@verdaccio/store';
3
- import { Logger } from '@verdaccio/types';
3
+ import { Config, Logger } from '@verdaccio/types';
4
4
  /**
5
5
  * Endpoint for npm search v1
6
6
  * Empty value
7
7
  * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
8
8
  * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
9
9
  */
10
- export default function (route: any, auth: Auth, storage: Storage, logger: Logger): void;
10
+ export default function (route: any, auth: Auth, storage: Storage, config: Config, logger: Logger): void;
@@ -5,13 +5,22 @@ let debug = require("debug");
5
5
  debug = require_runtime.__toESM(debug);
6
6
  //#region src/v1/search.ts
7
7
  var debug$1 = (0, debug.default)("verdaccio:api:search");
8
+ var DEFAULT_SIZE = 20;
9
+ var MAX_SIZE = 250;
10
+ var MAX_FROM = 1e4;
11
+ var CHECK_ACCESS_BATCH_SIZE = 50;
12
+ function parseQueryInt(value, defaultValue, max) {
13
+ const parsed = Number.parseInt(String(value), 10);
14
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
15
+ return Math.min(parsed, max);
16
+ }
8
17
  /**
9
18
  * Endpoint for npm search v1
10
19
  * Empty value
11
20
  * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
12
21
  * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
13
22
  */
14
- function search_default(route, auth, storage, logger) {
23
+ function search_default(route, auth, storage, config, logger) {
15
24
  function checkAccess(pkg, auth, remoteUser) {
16
25
  return new Promise((resolve, reject) => {
17
26
  auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function(err, allowed) {
@@ -21,10 +30,19 @@ function search_default(route, auth, storage, logger) {
21
30
  });
22
31
  });
23
32
  }
24
- route.get(_verdaccio_middleware.SEARCH_API_ENDPOINTS.search, async (req, res, next) => {
33
+ route.get(_verdaccio_middleware.SEARCH_API_ENDPOINTS.search, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), async (req, res, next) => {
25
34
  const { query, url } = req;
26
- let [size, from] = ["size", "from"].map((k) => query[k]);
27
- let data;
35
+ const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);
36
+ const from = parseQueryInt(query.from, 0, MAX_FROM);
37
+ const safeQuery = {
38
+ ...query,
39
+ size,
40
+ from
41
+ };
42
+ const searchParams = new URLSearchParams(query);
43
+ searchParams.set("size", String(size));
44
+ searchParams.set("from", String(from));
45
+ const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
28
46
  const abort = new AbortController();
29
47
  const onClientClose = () => {
30
48
  debug$1("search web aborted");
@@ -32,18 +50,20 @@ function search_default(route, auth, storage, logger) {
32
50
  };
33
51
  req.socket.on("close", onClientClose);
34
52
  try {
35
- size = parseInt(Number.isFinite(size) ? size : "20", 10);
36
- from = parseInt(Number.isFinite(from) ? from : "0", 10);
37
53
  debug$1("storage search initiated");
38
- data = await storage.search({
39
- query,
40
- url,
54
+ const data = await storage.search({
55
+ query: safeQuery,
56
+ url: safeUrl,
41
57
  abort
42
58
  });
43
- debug$1("storage items tota: %o", data.length);
44
- const final = (await Promise.all(data.map((pkgItem) => {
45
- return checkAccess(pkgItem, auth, req.remote_user);
46
- }))).filter((i) => i !== null).slice(from, from + size);
59
+ debug$1("storage items total: %o", data.length);
60
+ const requested = from + size;
61
+ const allowed = [];
62
+ for (let i = 0; i < data.length && allowed.length < requested && !abort.signal.aborted; i += CHECK_ACCESS_BATCH_SIZE) {
63
+ const batch = await Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user)));
64
+ for (const item of batch) if (item !== null) allowed.push(item);
65
+ }
66
+ const final = allowed.slice(from, requested);
47
67
  logger.debug(`search results ${final?.length}`);
48
68
  const response = {
49
69
  objects: final,
@@ -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 } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger, Manifest } from '@verdaccio/types';\n\nconst debug = buildDebug('verdaccio:api:search');\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 (route, auth: Auth, storage: Storage, logger: Logger): void {\n function checkAccess(pkg: any, auth: any, remoteUser): Promise<Manifest | 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(SEARCH_API_ENDPOINTS.search, async (req, res, next) => {\n const { query, url } = req;\n let [size, from] = ['size', 'from'].map((k) => query[k]);\n let data;\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 size = parseInt(Number.isFinite(size) ? size : '20', 10);\n from = parseInt(Number.isFinite(from) ? from : '0', 10);\n\n debug('storage search initiated');\n data = await storage.search({\n query,\n url,\n abort,\n });\n debug('storage items tota: %o', data.length);\n const checkAccessPromises: searchUtils.SearchItemPkg[] = await Promise.all(\n data.map((pkgItem) => {\n return checkAccess(pkgItem, auth, req.remote_user);\n })\n );\n\n const final: searchUtils.SearchItemPkg[] = checkAccessPromises\n .filter((i) => i !== null)\n .slice(from, from + size);\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"],"mappings":";;;;;;AASA,IAAM,WAAA,GAAA,MAAA,SAAmB,sBAAsB;;;;;;;AAQ/C,SAAA,eAAyB,OAAO,MAAY,SAAkB,QAAsB;CAClF,SAAS,YAAY,KAAU,MAAW,YAAsC;EAC9E,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,KAAK,aAAa,EAAE,aAAa,KAAK,SAAS,KAAK,GAAG,YAAY,SAAU,KAAK,SAAS;IACzF,IAAI,KACF,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,EAAE,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;SAEnB,OAAO,GAAG;SAGZ,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,IAAI,sBAAA,qBAAqB,QAAQ,OAAO,KAAK,KAAK,SAAS;EAC/D,MAAM,EAAE,OAAO,QAAQ;EACvB,IAAI,CAAC,MAAM,QAAQ,CAAC,QAAQ,MAAM,EAAE,KAAK,MAAM,MAAM,EAAE;EACvD,IAAI;EACJ,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,sBAA4B;GAChC,QAAM,oBAAoB;GAC1B,MAAM,MAAM;EACd;EACA,IAAI,OAAO,GAAG,SAAS,aAAa;EAEpC,IAAI;GACF,OAAO,SAAS,OAAO,SAAS,IAAI,IAAI,OAAO,MAAM,EAAE;GACvD,OAAO,SAAS,OAAO,SAAS,IAAI,IAAI,OAAO,KAAK,EAAE;GAEtD,QAAM,0BAA0B;GAChC,OAAO,MAAM,QAAQ,OAAO;IAC1B;IACA;IACA;GACF,CAAC;GACD,QAAM,0BAA0B,KAAK,MAAM;GAO3C,MAAM,SAAqC,MANoB,QAAQ,IACrE,KAAK,KAAK,YAAY;IACpB,OAAO,YAAY,SAAS,MAAM,IAAI,WAAW;GACnD,CAAC,CACH,GAGG,QAAQ,MAAM,MAAM,IAAI,EACxB,MAAM,MAAM,OAAO,IAAI;GAC1B,OAAO,MAAM,kBAAkB,OAAO,QAAQ;GAE9C,MAAM,WAAsC;IAC1C,SAAS;IACT,OAAO,MAAM;IACb,uBAAM,IAAI,KAAK,GAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,gBAAA,YAAY,EAAE,EAAE,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,CAAC;AACH"}
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,GAAA,MAAA,SAAmB,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,KACF,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,EAAE,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;SAEnB,OAAO,GAAG;SAGZ,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,IACJ,sBAAA,qBAAqB,SAAA,GAAA,sBAAA,WACX,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,EAAE,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,EACpC,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,GAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,gBAAA,YAAY,EAAE,EAAE,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,15 +1,24 @@
1
- import { SEARCH_API_ENDPOINTS } from "@verdaccio/middleware";
1
+ import { SEARCH_API_ENDPOINTS, rateLimit } from "@verdaccio/middleware";
2
2
  import { HTTP_STATUS } from "@verdaccio/core";
3
3
  import buildDebug from "debug";
4
4
  //#region src/v1/search.ts
5
5
  var debug = buildDebug("verdaccio:api:search");
6
+ var DEFAULT_SIZE = 20;
7
+ var MAX_SIZE = 250;
8
+ var MAX_FROM = 1e4;
9
+ var CHECK_ACCESS_BATCH_SIZE = 50;
10
+ function parseQueryInt(value, defaultValue, max) {
11
+ const parsed = Number.parseInt(String(value), 10);
12
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
13
+ return Math.min(parsed, max);
14
+ }
6
15
  /**
7
16
  * Endpoint for npm search v1
8
17
  * Empty value
9
18
  * - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
10
19
  * req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
11
20
  */
12
- function search_default(route, auth, storage, logger) {
21
+ function search_default(route, auth, storage, config, logger) {
13
22
  function checkAccess(pkg, auth, remoteUser) {
14
23
  return new Promise((resolve, reject) => {
15
24
  auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function(err, allowed) {
@@ -19,10 +28,19 @@ function search_default(route, auth, storage, logger) {
19
28
  });
20
29
  });
21
30
  }
22
- route.get(SEARCH_API_ENDPOINTS.search, async (req, res, next) => {
31
+ route.get(SEARCH_API_ENDPOINTS.search, rateLimit(config?.userRateLimit), async (req, res, next) => {
23
32
  const { query, url } = req;
24
- let [size, from] = ["size", "from"].map((k) => query[k]);
25
- let data;
33
+ const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);
34
+ const from = parseQueryInt(query.from, 0, MAX_FROM);
35
+ const safeQuery = {
36
+ ...query,
37
+ size,
38
+ from
39
+ };
40
+ const searchParams = new URLSearchParams(query);
41
+ searchParams.set("size", String(size));
42
+ searchParams.set("from", String(from));
43
+ const safeUrl = `${url.split("?")[0]}?${searchParams.toString()}`;
26
44
  const abort = new AbortController();
27
45
  const onClientClose = () => {
28
46
  debug("search web aborted");
@@ -30,18 +48,20 @@ function search_default(route, auth, storage, logger) {
30
48
  };
31
49
  req.socket.on("close", onClientClose);
32
50
  try {
33
- size = parseInt(Number.isFinite(size) ? size : "20", 10);
34
- from = parseInt(Number.isFinite(from) ? from : "0", 10);
35
51
  debug("storage search initiated");
36
- data = await storage.search({
37
- query,
38
- url,
52
+ const data = await storage.search({
53
+ query: safeQuery,
54
+ url: safeUrl,
39
55
  abort
40
56
  });
41
- debug("storage items tota: %o", data.length);
42
- const final = (await Promise.all(data.map((pkgItem) => {
43
- return checkAccess(pkgItem, auth, req.remote_user);
44
- }))).filter((i) => i !== null).slice(from, from + size);
57
+ debug("storage items total: %o", data.length);
58
+ const requested = from + size;
59
+ const allowed = [];
60
+ for (let i = 0; i < data.length && allowed.length < requested && !abort.signal.aborted; i += CHECK_ACCESS_BATCH_SIZE) {
61
+ const batch = await Promise.all(data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map((pkgItem) => checkAccess(pkgItem, auth, req.remote_user)));
62
+ for (const item of batch) if (item !== null) allowed.push(item);
63
+ }
64
+ const final = allowed.slice(from, requested);
45
65
  logger.debug(`search results ${final?.length}`);
46
66
  const response = {
47
67
  objects: final,
@@ -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 } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger, Manifest } from '@verdaccio/types';\n\nconst debug = buildDebug('verdaccio:api:search');\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 (route, auth: Auth, storage: Storage, logger: Logger): void {\n function checkAccess(pkg: any, auth: any, remoteUser): Promise<Manifest | 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(SEARCH_API_ENDPOINTS.search, async (req, res, next) => {\n const { query, url } = req;\n let [size, from] = ['size', 'from'].map((k) => query[k]);\n let data;\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 size = parseInt(Number.isFinite(size) ? size : '20', 10);\n from = parseInt(Number.isFinite(from) ? from : '0', 10);\n\n debug('storage search initiated');\n data = await storage.search({\n query,\n url,\n abort,\n });\n debug('storage items tota: %o', data.length);\n const checkAccessPromises: searchUtils.SearchItemPkg[] = await Promise.all(\n data.map((pkgItem) => {\n return checkAccess(pkgItem, auth, req.remote_user);\n })\n );\n\n const final: searchUtils.SearchItemPkg[] = checkAccessPromises\n .filter((i) => i !== null)\n .slice(from, from + size);\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"],"mappings":";;;;AASA,IAAM,QAAQ,WAAW,sBAAsB;;;;;;;AAQ/C,SAAA,eAAyB,OAAO,MAAY,SAAkB,QAAsB;CAClF,SAAS,YAAY,KAAU,MAAW,YAAsC;EAC9E,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,KAAK,aAAa,EAAE,aAAa,KAAK,SAAS,KAAK,GAAG,YAAY,SAAU,KAAK,SAAS;IACzF,IAAI,KACF,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,EAAE,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;SAEnB,OAAO,GAAG;SAGZ,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,IAAI,qBAAqB,QAAQ,OAAO,KAAK,KAAK,SAAS;EAC/D,MAAM,EAAE,OAAO,QAAQ;EACvB,IAAI,CAAC,MAAM,QAAQ,CAAC,QAAQ,MAAM,EAAE,KAAK,MAAM,MAAM,EAAE;EACvD,IAAI;EACJ,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,sBAA4B;GAChC,MAAM,oBAAoB;GAC1B,MAAM,MAAM;EACd;EACA,IAAI,OAAO,GAAG,SAAS,aAAa;EAEpC,IAAI;GACF,OAAO,SAAS,OAAO,SAAS,IAAI,IAAI,OAAO,MAAM,EAAE;GACvD,OAAO,SAAS,OAAO,SAAS,IAAI,IAAI,OAAO,KAAK,EAAE;GAEtD,MAAM,0BAA0B;GAChC,OAAO,MAAM,QAAQ,OAAO;IAC1B;IACA;IACA;GACF,CAAC;GACD,MAAM,0BAA0B,KAAK,MAAM;GAO3C,MAAM,SAAqC,MANoB,QAAQ,IACrE,KAAK,KAAK,YAAY;IACpB,OAAO,YAAY,SAAS,MAAM,IAAI,WAAW;GACnD,CAAC,CACH,GAGG,QAAQ,MAAM,MAAM,IAAI,EACxB,MAAM,MAAM,OAAO,IAAI;GAC1B,OAAO,MAAM,kBAAkB,OAAO,QAAQ;GAE9C,MAAM,WAAsC;IAC1C,SAAS;IACT,OAAO,MAAM;IACb,uBAAM,IAAI,KAAK,GAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,YAAY,EAAE,EAAE,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,CAAC;AACH"}
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,KACF,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,EAAE,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;SAEnB,OAAO,GAAG;SAGZ,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,EAAE,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,EACpC,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,GAAE,YAAY;GAC/B;GAEA,IAAI,OAAO,YAAY,EAAE,EAAE,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"}
package/build/v1/token.js CHANGED
@@ -3,6 +3,17 @@ let _verdaccio_core = require("@verdaccio/core");
3
3
  let _verdaccio_auth = require("@verdaccio/auth");
4
4
  let lodash_es = require("lodash-es");
5
5
  //#region src/v1/token.ts
6
+ var UNSUPPORTED_TOKEN_OPTIONS = [
7
+ "packages_and_scopes_permission",
8
+ "packages",
9
+ "packages_all",
10
+ "scopes",
11
+ "orgs",
12
+ "orgs_permission",
13
+ "expires",
14
+ "description",
15
+ "bypass_2fa"
16
+ ];
6
17
  function normalizeToken(token) {
7
18
  return {
8
19
  ...token,
@@ -29,9 +40,16 @@ function token_default(route, auth, storage, config, logger) {
29
40
  return next(_verdaccio_core.errorUtils.getUnauthorized());
30
41
  });
31
42
  route.post(_verdaccio_middleware.TOKEN_API_ENDPOINTS.get_tokens, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), function(req, res, next) {
32
- const { password, readonly, cidr_whitelist } = req.body;
43
+ const { password } = req.body;
33
44
  const { name } = req.remote_user;
45
+ const readonly = req.body.readonly ?? false;
46
+ const cidr_whitelist = req.body.cidr_whitelist ?? [];
34
47
  if (!(0, lodash_es.isBoolean)(readonly) || !Array.isArray(cidr_whitelist)) return next(_verdaccio_core.errorUtils.getCode(_verdaccio_core.HTTP_STATUS.BAD_DATA, _verdaccio_core.SUPPORT_ERRORS.PARAMETERS_NOT_VALID));
48
+ const unsupportedOptions = UNSUPPORTED_TOKEN_OPTIONS.filter((option) => (0, lodash_es.isNil)(req.body[option]) === false);
49
+ if (unsupportedOptions.length > 0) logger.warn({
50
+ options: unsupportedOptions.join(", "),
51
+ userAgent: req.get("user-agent") ?? "unknown"
52
+ }, "granular access token options are not supported yet and will be ignored: @{options} (client: @{userAgent})");
35
53
  auth.authenticate(name, password, async (err, user) => {
36
54
  if (err) {
37
55
  const errorCode = err.message ? _verdaccio_core.HTTP_STATUS.UNAUTHORIZED : _verdaccio_core.HTTP_STATUS.INTERNAL_ERROR;
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","names":[],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, cryptoUtils, errorUtils } from '@verdaccio/core';\nimport { TOKEN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, RemoteUser, Token } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nexport type NormalizeToken = Token & {\n cidr_whitelist: string[];\n created: string;\n};\n\n// npm expects \"cidr_whitelist\" for token list\nfunction normalizeToken(token: Token): NormalizeToken {\n return {\n ...token,\n cidr_whitelist: token.cidr || [],\n created: new Date(token.created).toISOString(),\n };\n}\n\n// https://github.com/npm/npm-profile/blob/latest/lib/index.js\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n route.get(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n try {\n const tokens = await storage.readTokens({ user: name });\n const totalTokens = tokens.length;\n logger.debug({ totalTokens }, 'token list retrieved: @{totalTokens}');\n\n res.status(HTTP_STATUS.OK);\n return next({\n objects: tokens.map(normalizeToken),\n urls: {\n next: '', // TODO: pagination?\n },\n });\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token list has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n\n route.post(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { password, readonly, cidr_whitelist } = req.body;\n const { name } = req.remote_user;\n\n if (!isBoolean(readonly) || !Array.isArray(cidr_whitelist)) {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));\n }\n\n auth.authenticate(name, password, async (err, user?: RemoteUser) => {\n if (err) {\n const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;\n return next(errorUtils.getCode(errorCode, err.message));\n }\n\n req.remote_user = user;\n\n if (typeof storage.saveToken !== 'function') {\n return next(\n errorUtils.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT)\n );\n }\n\n try {\n const key = cryptoUtils.generateRandomHexString(16);\n const token = await getApiToken(auth, config, user as RemoteUser, password, {\n tokenKey: key,\n });\n if (!token) {\n throw errorUtils.getInternalError();\n }\n\n // TODO: use a utility here\n const maskedToken = cryptoUtils.mask(token, 5);\n const created = new Date().getTime();\n\n /**\n * cidr_whitelist: is not being used, we pass it through\n * token: we do not store the real token (it is generated once and retrieved\n * to the user), just a mask of it.\n */\n const saveToken: Token = {\n user: name,\n token: maskedToken,\n key,\n cidr: cidr_whitelist,\n readonly,\n created,\n };\n\n await storage.saveToken(saveToken);\n logger.debug({ key, name }, 'token @{key} was created for user @{name}');\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n return next(\n normalizeToken({\n token,\n user: name,\n key: saveToken.key,\n cidr: cidr_whitelist,\n readonly,\n created: saveToken.created,\n })\n );\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getInternalError(error.message));\n }\n });\n }\n );\n\n route.delete(\n TOKEN_API_ENDPOINTS.delete_token,\n rateLimit(config?.userRateLimit),\n async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {\n const {\n params: { tokenKey },\n } = req;\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n try {\n await storage.deleteToken(name, tokenKey);\n logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');\n return next({});\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"token.js","names":[],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, cryptoUtils, errorUtils } from '@verdaccio/core';\nimport { TOKEN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, RemoteUser, Token } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\n// Granular access token options (npm >= 11) that Verdaccio does not implement\n// yet. They are accepted so `npm token create` succeeds, but the restrictions\n// they express are NOT enforced — warn so the operator is not misled.\nconst UNSUPPORTED_TOKEN_OPTIONS = [\n 'packages_and_scopes_permission',\n 'packages',\n 'packages_all',\n 'scopes',\n 'orgs',\n 'orgs_permission',\n 'expires',\n 'description',\n 'bypass_2fa',\n];\n\nexport type NormalizeToken = Token & {\n cidr_whitelist: string[];\n created: string;\n};\n\n// npm expects \"cidr_whitelist\" for token list\nfunction normalizeToken(token: Token): NormalizeToken {\n return {\n ...token,\n cidr_whitelist: token.cidr || [],\n created: new Date(token.created).toISOString(),\n };\n}\n\n// https://github.com/npm/npm-profile/blob/latest/lib/index.js\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n route.get(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n try {\n const tokens = await storage.readTokens({ user: name });\n const totalTokens = tokens.length;\n logger.debug({ totalTokens }, 'token list retrieved: @{totalTokens}');\n\n res.status(HTTP_STATUS.OK);\n return next({\n objects: tokens.map(normalizeToken),\n urls: {\n next: '', // TODO: pagination?\n },\n });\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token list has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n\n route.post(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { password } = req.body;\n const { name } = req.remote_user;\n // `readonly` and `cidr_whitelist` are optional: npm >= 11 rewrote\n // `npm token create` to omit them (and only sends `cidr_whitelist` with\n // `--cidr`), so default them rather than rejecting the request. A wrong\n // type is still rejected.\n const readonly = req.body.readonly ?? false;\n const cidr_whitelist = req.body.cidr_whitelist ?? [];\n\n if (!isBoolean(readonly) || !Array.isArray(cidr_whitelist)) {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));\n }\n\n const unsupportedOptions = UNSUPPORTED_TOKEN_OPTIONS.filter(\n (option) => isNil(req.body[option]) === false\n );\n if (unsupportedOptions.length > 0) {\n logger.warn(\n { options: unsupportedOptions.join(', '), userAgent: req.get('user-agent') ?? 'unknown' },\n 'granular access token options are not supported yet and will be ignored: @{options} (client: @{userAgent})'\n );\n }\n\n auth.authenticate(name, password, async (err, user?: RemoteUser) => {\n if (err) {\n const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;\n return next(errorUtils.getCode(errorCode, err.message));\n }\n\n req.remote_user = user;\n\n if (typeof storage.saveToken !== 'function') {\n return next(\n errorUtils.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT)\n );\n }\n\n try {\n const key = cryptoUtils.generateRandomHexString(16);\n const token = await getApiToken(auth, config, user as RemoteUser, password, {\n tokenKey: key,\n });\n if (!token) {\n throw errorUtils.getInternalError();\n }\n\n // TODO: use a utility here\n const maskedToken = cryptoUtils.mask(token, 5);\n const created = new Date().getTime();\n\n /**\n * cidr_whitelist: is not being used, we pass it through\n * token: we do not store the real token (it is generated once and retrieved\n * to the user), just a mask of it.\n */\n const saveToken: Token = {\n user: name,\n token: maskedToken,\n key,\n cidr: cidr_whitelist,\n readonly,\n created,\n };\n\n await storage.saveToken(saveToken);\n logger.debug({ key, name }, 'token @{key} was created for user @{name}');\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n return next(\n normalizeToken({\n token,\n user: name,\n key: saveToken.key,\n cidr: cidr_whitelist,\n readonly,\n created: saveToken.created,\n })\n );\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getInternalError(error.message));\n }\n });\n }\n );\n\n route.delete(\n TOKEN_API_ENDPOINTS.delete_token,\n rateLimit(config?.userRateLimit),\n async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {\n const {\n params: { tokenKey },\n } = req;\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n try {\n await storage.deleteToken(name, tokenKey);\n logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');\n return next({});\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n}\n"],"mappings":"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"}
@@ -3,6 +3,17 @@ import { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, cryptoUtils, errorUtils } from "@
3
3
  import { getApiToken } from "@verdaccio/auth";
4
4
  import { isBoolean, isNil } from "lodash-es";
5
5
  //#region src/v1/token.ts
6
+ var UNSUPPORTED_TOKEN_OPTIONS = [
7
+ "packages_and_scopes_permission",
8
+ "packages",
9
+ "packages_all",
10
+ "scopes",
11
+ "orgs",
12
+ "orgs_permission",
13
+ "expires",
14
+ "description",
15
+ "bypass_2fa"
16
+ ];
6
17
  function normalizeToken(token) {
7
18
  return {
8
19
  ...token,
@@ -29,9 +40,16 @@ function token_default(route, auth, storage, config, logger) {
29
40
  return next(errorUtils.getUnauthorized());
30
41
  });
31
42
  route.post(TOKEN_API_ENDPOINTS.get_tokens, rateLimit(config?.userRateLimit), function(req, res, next) {
32
- const { password, readonly, cidr_whitelist } = req.body;
43
+ const { password } = req.body;
33
44
  const { name } = req.remote_user;
45
+ const readonly = req.body.readonly ?? false;
46
+ const cidr_whitelist = req.body.cidr_whitelist ?? [];
34
47
  if (!isBoolean(readonly) || !Array.isArray(cidr_whitelist)) return next(errorUtils.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));
48
+ const unsupportedOptions = UNSUPPORTED_TOKEN_OPTIONS.filter((option) => isNil(req.body[option]) === false);
49
+ if (unsupportedOptions.length > 0) logger.warn({
50
+ options: unsupportedOptions.join(", "),
51
+ userAgent: req.get("user-agent") ?? "unknown"
52
+ }, "granular access token options are not supported yet and will be ignored: @{options} (client: @{userAgent})");
35
53
  auth.authenticate(name, password, async (err, user) => {
36
54
  if (err) {
37
55
  const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
@@ -1 +1 @@
1
- {"version":3,"file":"token.mjs","names":[],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, cryptoUtils, errorUtils } from '@verdaccio/core';\nimport { TOKEN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, RemoteUser, Token } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nexport type NormalizeToken = Token & {\n cidr_whitelist: string[];\n created: string;\n};\n\n// npm expects \"cidr_whitelist\" for token list\nfunction normalizeToken(token: Token): NormalizeToken {\n return {\n ...token,\n cidr_whitelist: token.cidr || [],\n created: new Date(token.created).toISOString(),\n };\n}\n\n// https://github.com/npm/npm-profile/blob/latest/lib/index.js\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n route.get(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n try {\n const tokens = await storage.readTokens({ user: name });\n const totalTokens = tokens.length;\n logger.debug({ totalTokens }, 'token list retrieved: @{totalTokens}');\n\n res.status(HTTP_STATUS.OK);\n return next({\n objects: tokens.map(normalizeToken),\n urls: {\n next: '', // TODO: pagination?\n },\n });\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token list has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n\n route.post(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { password, readonly, cidr_whitelist } = req.body;\n const { name } = req.remote_user;\n\n if (!isBoolean(readonly) || !Array.isArray(cidr_whitelist)) {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));\n }\n\n auth.authenticate(name, password, async (err, user?: RemoteUser) => {\n if (err) {\n const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;\n return next(errorUtils.getCode(errorCode, err.message));\n }\n\n req.remote_user = user;\n\n if (typeof storage.saveToken !== 'function') {\n return next(\n errorUtils.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT)\n );\n }\n\n try {\n const key = cryptoUtils.generateRandomHexString(16);\n const token = await getApiToken(auth, config, user as RemoteUser, password, {\n tokenKey: key,\n });\n if (!token) {\n throw errorUtils.getInternalError();\n }\n\n // TODO: use a utility here\n const maskedToken = cryptoUtils.mask(token, 5);\n const created = new Date().getTime();\n\n /**\n * cidr_whitelist: is not being used, we pass it through\n * token: we do not store the real token (it is generated once and retrieved\n * to the user), just a mask of it.\n */\n const saveToken: Token = {\n user: name,\n token: maskedToken,\n key,\n cidr: cidr_whitelist,\n readonly,\n created,\n };\n\n await storage.saveToken(saveToken);\n logger.debug({ key, name }, 'token @{key} was created for user @{name}');\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n return next(\n normalizeToken({\n token,\n user: name,\n key: saveToken.key,\n cidr: cidr_whitelist,\n readonly,\n created: saveToken.created,\n })\n );\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getInternalError(error.message));\n }\n });\n }\n );\n\n route.delete(\n TOKEN_API_ENDPOINTS.delete_token,\n rateLimit(config?.userRateLimit),\n async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {\n const {\n params: { tokenKey },\n } = req;\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n try {\n await storage.deleteToken(name, tokenKey);\n logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');\n return next({});\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"token.mjs","names":[],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, cryptoUtils, errorUtils } from '@verdaccio/core';\nimport { TOKEN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger, RemoteUser, Token } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\n// Granular access token options (npm >= 11) that Verdaccio does not implement\n// yet. They are accepted so `npm token create` succeeds, but the restrictions\n// they express are NOT enforced — warn so the operator is not misled.\nconst UNSUPPORTED_TOKEN_OPTIONS = [\n 'packages_and_scopes_permission',\n 'packages',\n 'packages_all',\n 'scopes',\n 'orgs',\n 'orgs_permission',\n 'expires',\n 'description',\n 'bypass_2fa',\n];\n\nexport type NormalizeToken = Token & {\n cidr_whitelist: string[];\n created: string;\n};\n\n// npm expects \"cidr_whitelist\" for token list\nfunction normalizeToken(token: Token): NormalizeToken {\n return {\n ...token,\n cidr_whitelist: token.cidr || [],\n created: new Date(token.created).toISOString(),\n };\n}\n\n// https://github.com/npm/npm-profile/blob/latest/lib/index.js\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n route.get(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n try {\n const tokens = await storage.readTokens({ user: name });\n const totalTokens = tokens.length;\n logger.debug({ totalTokens }, 'token list retrieved: @{totalTokens}');\n\n res.status(HTTP_STATUS.OK);\n return next({\n objects: tokens.map(normalizeToken),\n urls: {\n next: '', // TODO: pagination?\n },\n });\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token list has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\n }\n );\n\n route.post(\n TOKEN_API_ENDPOINTS.get_tokens,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {\n const { password } = req.body;\n const { name } = req.remote_user;\n // `readonly` and `cidr_whitelist` are optional: npm >= 11 rewrote\n // `npm token create` to omit them (and only sends `cidr_whitelist` with\n // `--cidr`), so default them rather than rejecting the request. A wrong\n // type is still rejected.\n const readonly = req.body.readonly ?? false;\n const cidr_whitelist = req.body.cidr_whitelist ?? [];\n\n if (!isBoolean(readonly) || !Array.isArray(cidr_whitelist)) {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_DATA, SUPPORT_ERRORS.PARAMETERS_NOT_VALID));\n }\n\n const unsupportedOptions = UNSUPPORTED_TOKEN_OPTIONS.filter(\n (option) => isNil(req.body[option]) === false\n );\n if (unsupportedOptions.length > 0) {\n logger.warn(\n { options: unsupportedOptions.join(', '), userAgent: req.get('user-agent') ?? 'unknown' },\n 'granular access token options are not supported yet and will be ignored: @{options} (client: @{userAgent})'\n );\n }\n\n auth.authenticate(name, password, async (err, user?: RemoteUser) => {\n if (err) {\n const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;\n return next(errorUtils.getCode(errorCode, err.message));\n }\n\n req.remote_user = user;\n\n if (typeof storage.saveToken !== 'function') {\n return next(\n errorUtils.getCode(HTTP_STATUS.NOT_IMPLEMENTED, SUPPORT_ERRORS.STORAGE_NOT_IMPLEMENT)\n );\n }\n\n try {\n const key = cryptoUtils.generateRandomHexString(16);\n const token = await getApiToken(auth, config, user as RemoteUser, password, {\n tokenKey: key,\n });\n if (!token) {\n throw errorUtils.getInternalError();\n }\n\n // TODO: use a utility here\n const maskedToken = cryptoUtils.mask(token, 5);\n const created = new Date().getTime();\n\n /**\n * cidr_whitelist: is not being used, we pass it through\n * token: we do not store the real token (it is generated once and retrieved\n * to the user), just a mask of it.\n */\n const saveToken: Token = {\n user: name,\n token: maskedToken,\n key,\n cidr: cidr_whitelist,\n readonly,\n created,\n };\n\n await storage.saveToken(saveToken);\n logger.debug({ key, name }, 'token @{key} was created for user @{name}');\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n return next(\n normalizeToken({\n token,\n user: name,\n key: saveToken.key,\n cidr: cidr_whitelist,\n readonly,\n created: saveToken.created,\n })\n );\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getInternalError(error.message));\n }\n });\n }\n );\n\n route.delete(\n TOKEN_API_ENDPOINTS.delete_token,\n rateLimit(config?.userRateLimit),\n async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {\n const {\n params: { tokenKey },\n } = req;\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n try {\n await storage.deleteToken(name, tokenKey);\n logger.info({ tokenKey, name }, 'token id @{tokenKey} was revoked for user @{name}');\n return next({});\n } catch (error: any) {\n logger.error({ error: error.msg }, 'token creation has failed: @{error}');\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, error.message));\n }\n }\n return next(errorUtils.getUnauthorized());\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.19",
3
+ "version": "9.0.0-next-9.21",
4
4
  "description": "Verdaccio Registry API",
5
5
  "main": "./build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,26 +33,26 @@
33
33
  },
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@verdaccio/auth": "9.0.0-next-9.19",
37
- "@verdaccio/config": "9.0.0-next-9.19",
38
- "@verdaccio/core": "9.0.0-next-9.19",
39
- "@verdaccio/hooks": "9.0.0-next-9.19",
40
- "@verdaccio/logger": "9.0.0-next-9.19",
41
- "@verdaccio/middleware": "9.0.0-next-9.19",
42
- "@verdaccio/store": "9.0.0-next-9.19",
36
+ "@verdaccio/auth": "9.0.0-next-9.21",
37
+ "@verdaccio/config": "9.0.0-next-9.21",
38
+ "@verdaccio/core": "9.0.0-next-9.21",
39
+ "@verdaccio/hooks": "9.0.0-next-9.21",
40
+ "@verdaccio/logger": "9.0.0-next-9.21",
41
+ "@verdaccio/middleware": "9.0.0-next-9.21",
42
+ "@verdaccio/store": "9.0.0-next-9.21",
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.20",
50
- "@verdaccio/types": "14.0.0-next-9.10",
49
+ "@verdaccio/test-helper": "5.0.0-next-9.22",
50
+ "@verdaccio/types": "14.0.0-next-9.11",
51
51
  "mockdate": "3.0.5",
52
52
  "nock": "13.5.6",
53
53
  "rimraf": "6.1.3",
54
54
  "supertest": "7.1.4",
55
- "vite": "8.0.14",
55
+ "vite": "8.0.16",
56
56
  "vitest": "4.1.7"
57
57
  },
58
58
  "funding": {