@verdaccio/api 9.0.0-next-9.35 → 9.0.0-next-9.37
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/dist-tags.js.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +4 -1
- package/build/index.mjs.map +1 -1
- package/build/package-version-status.d.ts +4 -0
- package/build/package-version-status.js +36 -0
- package/build/package-version-status.js.map +1 -0
- package/build/package-version-status.mjs +36 -0
- package/build/package-version-status.mjs.map +1 -0
- package/build/package.js.map +1 -1
- package/build/ping.js.map +1 -1
- package/build/publish.d.ts +1 -1
- package/build/publish.js +3 -2
- package/build/publish.js.map +1 -1
- package/build/publish.mjs +3 -2
- package/build/publish.mjs.map +1 -1
- package/build/require-otp.js.map +1 -1
- package/build/search.js.map +1 -1
- package/build/stage.js.map +1 -1
- package/build/user.js +12 -2
- package/build/user.js.map +1 -1
- package/build/user.mjs +13 -3
- package/build/user.mjs.map +1 -1
- package/build/v1/login.js.map +1 -1
- package/build/v1/profile.js.map +1 -1
- package/build/v1/search.js.map +1 -1
- package/build/v1/token.js.map +1 -1
- package/build/whoami.js.map +1 -1
- package/package.json +14 -14
package/build/dist-tags.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dist-tags.js","names":[],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,OAAA,
|
|
1
|
+
{"version":3,"file":"dist-tags.js","names":["allow","errorUtils","reqUtils","getRequestOptions","constants","DIST_TAGS_API_ENDPOINTS","media","HEADERS"],"sources":["../src/dist-tags.ts"],"sourcesContent":["import type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, constants, errorUtils, reqUtils } from '@verdaccio/core';\nimport { DIST_TAGS_API_ENDPOINTS, allow, getRequestOptions, media } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (\n route: Router,\n auth: Auth,\n storage: Storage,\n logger: Logger,\n /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const addTagPackageVersionMiddleware = async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<$NextFunctionVer> {\n if (typeof req.body !== 'string') {\n return next(errorUtils.getBadRequest('version is missing'));\n }\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = req.body;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_ADDED,\n });\n } catch (err) {\n next(err);\n }\n };\n\n // tagging a package.\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.put(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n media(HEADERS.JSON),\n addTagPackageVersionMiddleware\n );\n\n route.delete(\n DIST_TAGS_API_ENDPOINTS.tagging_package,\n can('publish'),\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const tag = reqUtils.paramToString(req.params.tag);\n const tags = {};\n tags[tag] = null;\n try {\n await storage.mergeTagsNext(packageName, tags, getRequestOptions(req));\n res.status(constants.HTTP_STATUS.CREATED);\n return next({\n ok: constants.API_MESSAGE.TAG_REMOVED,\n });\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n DIST_TAGS_API_ENDPOINTS.get_dist_tags,\n can('access'),\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const requestOptions = getRequestOptions(req);\n try {\n const manifest = await storage.getPackageByOptions({\n name: packageName,\n uplinksLook: true,\n requestOptions,\n });\n next(manifest[constants.DIST_TAGS]);\n } catch (err) {\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;AAUA,SAAA,kBACE,OACA,MACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,MAAM,OAAA,GAAMA,sBAAAA,MAAAA,CAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,iCAAiC,eACrC,KACA,KACA,MAC2B;EAC3B,IAAI,OAAO,IAAI,SAAS,UACtB,OAAO,KAAKC,gBAAAA,WAAW,cAAc,oBAAoB,CAAC;EAE5D,MAAM,cAAcC,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAMA,gBAAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO,IAAI;EAChB,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,OAAA,GAAMC,sBAAAA,kBAAAA,CAAkB,GAAG,CAAC;GACrE,IAAI,OAAOC,gBAAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAIA,gBAAAA,UAAU,YAAY,UAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF;CAGA,MAAM,IACJC,sBAAAA,wBAAwB,SACxB,IAAI,SAAS,GACb,aAAA,GACAC,sBAAAA,MAAAA,CAAMC,gBAAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,IACJF,sBAAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,aAAA,GACAC,sBAAAA,MAAAA,CAAMC,gBAAAA,QAAQ,IAAI,GAClB,8BACF;CAEA,MAAM,OACJF,sBAAAA,wBAAwB,iBACxB,IAAI,SAAS,GACb,YACA,eACE,KACA,KACA,MACe;EACf,MAAM,cAAcH,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAMA,gBAAAA,SAAS,cAAc,IAAI,OAAO,GAAG;EACjD,MAAM,OAAO,CAAC;EACd,KAAK,OAAO;EACZ,IAAI;GACF,MAAM,QAAQ,cAAc,aAAa,OAAA,GAAMC,sBAAAA,kBAAAA,CAAkB,GAAG,CAAC;GACrE,IAAI,OAAOC,gBAAAA,UAAU,YAAY,OAAO;GACxC,OAAO,KAAK,EACV,IAAIA,gBAAAA,UAAU,YAAY,YAC5B,CAAC;EACH,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJC,sBAAAA,wBAAwB,eACxB,IAAI,QAAQ,GACZ,eACE,KACA,KACA,MACe;EACf,MAAM,cAAcH,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EAC7D,MAAM,kBAAA,GAAiBC,sBAAAA,kBAAAA,CAAkB,GAAG;EAC5C,IAAI;GAMF,MAAK,MALkB,QAAQ,oBAAoB;IACjD,MAAM;IACN,aAAa;IACb;GACF,CAAC,EAAA,CACaC,gBAAAA,UAAU,UAAU;EACpC,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;AACF"}
|
package/build/index.js
CHANGED
|
@@ -5,6 +5,7 @@ Object.defineProperties(exports, {
|
|
|
5
5
|
const require_runtime = require("./_virtual/_rolldown/runtime.js");
|
|
6
6
|
const require_dist_tags = require("./dist-tags.js");
|
|
7
7
|
const require_package = require("./package.js");
|
|
8
|
+
const require_package_version_status = require("./package-version-status.js");
|
|
8
9
|
const require_ping = require("./ping.js");
|
|
9
10
|
const require_publish = require("./publish.js");
|
|
10
11
|
const require_require_otp = require("./require-otp.js");
|
|
@@ -33,6 +34,7 @@ function src_default(config, auth, storage, logger) {
|
|
|
33
34
|
app.param("org_couchdb_user", (0, _verdaccio_middleware.match)(/^org\.couchdb\.user:/));
|
|
34
35
|
(0, _verdaccio_middleware.registerBodyParser)(app, config);
|
|
35
36
|
app.use(_verdaccio_middleware.WebUrlsNamespace.endpoints, (_req, _res, next) => next("router"));
|
|
37
|
+
app.get(_verdaccio_middleware.PACKAGE_API_ENDPOINTS.version_status, _verdaccio_middleware.validatePackageVersionStatus);
|
|
36
38
|
const apiJwtMiddleware = auth.apiJWTmiddleware();
|
|
37
39
|
app.use((req, res, next) => {
|
|
38
40
|
if (req.remote_user ?? res.locals.remote_user) return next();
|
|
@@ -73,6 +75,7 @@ function src_default(config, auth, storage, logger) {
|
|
|
73
75
|
require_search$1.default(app, auth, storage, config, logger);
|
|
74
76
|
require_token.default(app, auth, storage, config, logger, otpForAuth);
|
|
75
77
|
if (config.flags?.stage) require_stage.default(app, auth, storage, config, logger, otpForWrites);
|
|
78
|
+
require_package_version_status.default(app, auth, storage);
|
|
76
79
|
require_package.default(app, auth, storage, logger);
|
|
77
80
|
if (config.flags?.webLogin) require_login.default(app, auth, storage, config, logger);
|
|
78
81
|
return app;
|
package/build/index.js.map
CHANGED
|
@@ -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 WebUrlsNamespace,\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 { TfaStore } from '@verdaccio/auth';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport { assertTokenStoreSupport, requireOtp } from './require-otp';\nimport search from './search';\nimport stage from './stage';\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 app.use(WebUrlsNamespace.endpoints, (_req, _res, next) => next('router'));\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 // built once: without the flag nothing must reach the token store\n let tfaStore: TfaStore | undefined;\n if (config.flags?.tfa) {\n // refuse to start rather than answer 503 on every write later\n assertTokenStoreSupport(storage, logger);\n tfaStore = new TfaStore(storage, config.secret, logger);\n }\n const otpForAuth = requireOtp({ tfaStore, scope: 'auth', logger });\n // at login there is no authenticated user yet: the name is in the body\n const otpForLogin = requireOtp({\n tfaStore,\n scope: 'auth',\n logger,\n getUsername: (req) => (typeof req.body?.name === 'string' ? req.body.name : undefined),\n });\n const otpForWrites = requireOtp({ tfaStore, scope: 'write', logger });\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, storage, logger, otpForAuth);\n search(app, logger);\n user(app, auth, config, logger, otpForLogin);\n distTags(app, auth, storage, logger, otpForWrites);\n publish(app, auth, storage, config, logger, otpForWrites);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger, otpForAuth);\n // must stay before pkg(): its '/:package{/:version}' route would otherwise\n // swallow GET /-/stage\n if (config.flags?.stage) {\n stage(app, auth, storage, config, logger, otpForWrites);\n }\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":"
|
|
1
|
+
{"version":3,"file":"index.js","names":["validatePackage","validateName","match","WebUrlsNamespace","PACKAGE_API_ENDPOINTS","validatePackageVersionStatus","TfaStore","requireOtp","enforceGeneratedTokenMetadata","antiLoop","makeURLrelative","encodeScopePackage"],"sources":["../src/index.ts"],"sourcesContent":["import type { Router } from 'express';\nimport express from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n WebUrlsNamespace,\n PACKAGE_API_ENDPOINTS,\n validatePackageVersionStatus,\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 { TfaStore } from '@verdaccio/auth';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport packageVersionStatus from './package-version-status';\nimport ping from './ping';\nimport publish from './publish';\nimport { assertTokenStoreSupport, requireOtp } from './require-otp';\nimport search from './search';\nimport stage from './stage';\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 app.use(WebUrlsNamespace.endpoints, (_req, _res, next) => next('router'));\n\n app.get(PACKAGE_API_ENDPOINTS.version_status, validatePackageVersionStatus);\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 // built once: without the flag nothing must reach the token store\n let tfaStore: TfaStore | undefined;\n if (config.flags?.tfa) {\n // refuse to start rather than answer 503 on every write later\n assertTokenStoreSupport(storage, logger);\n tfaStore = new TfaStore(storage, config.secret, logger);\n }\n const otpForAuth = requireOtp({ tfaStore, scope: 'auth', logger });\n // at login there is no authenticated user yet: the name is in the body\n const otpForLogin = requireOtp({\n tfaStore,\n scope: 'auth',\n logger,\n getUsername: (req) => (typeof req.body?.name === 'string' ? req.body.name : undefined),\n });\n const otpForWrites = requireOtp({ tfaStore, scope: 'write', logger });\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, storage, logger, otpForAuth);\n search(app, logger);\n user(app, auth, config, logger, otpForLogin);\n distTags(app, auth, storage, logger, otpForWrites);\n publish(app, auth, storage, config, logger, otpForWrites);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger, otpForAuth);\n // must stay before pkg(): its '/:package{/:version}' route would otherwise\n // swallow GET /-/stage\n if (config.flags?.stage) {\n stage(app, auth, storage, config, logger, otpForWrites);\n }\n packageVersionStatus(app, auth, storage);\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":";;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAA,YAAyB,QAAgB,MAAY,SAAkB,QAAwB;CAE7F,MAAM,MAAM,QAAA,QAAQ,OAAO;CAI3B,IAAI,MAAM,WAAWA,sBAAAA,eAAe;CACpC,IAAI,MAAM,YAAYC,sBAAAA,YAAY;CAClC,IAAI,MAAM,OAAOA,sBAAAA,YAAY;CAC7B,IAAI,MAAM,WAAWA,sBAAAA,YAAY;CACjC,IAAI,MAAM,YAAYA,sBAAAA,YAAY;CAClC,IAAI,MAAM,SAASA,sBAAAA,YAAY;CAI/B,IAAI,MAAM,SAAA,GAAQC,sBAAAA,MAAAA,CAAM,QAAQ,CAAC;CACjC,IAAI,MAAM,qBAAA,GAAoBA,sBAAAA,MAAAA,CAAM,sBAAsB,CAAC;CAG3D,CAAA,GAAA,sBAAA,mBAAA,CAAmB,KAAK,MAAM;CAE9B,IAAI,IAAIC,sBAAAA,iBAAiB,YAAY,MAAM,MAAM,SAAS,KAAK,QAAQ,CAAC;CAExE,IAAI,IAAIC,sBAAAA,sBAAsB,gBAAgBC,sBAAAA,4BAA4B;CAG1E,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;CAGD,IAAI;CACJ,IAAI,OAAO,OAAO,KAAK;EAErB,oBAAA,wBAAwB,SAAS,MAAM;EACvC,WAAW,IAAIC,gBAAAA,SAAS,SAAS,OAAO,QAAQ,MAAM;CACxD;CACA,MAAM,aAAaC,oBAAAA,WAAW;EAAE;EAAU,OAAO;EAAQ;CAAO,CAAC;CAEjE,MAAM,cAAcA,oBAAAA,WAAW;EAC7B;EACA,OAAO;EACP;EACA,cAAc,QAAS,OAAO,IAAI,MAAM,SAAS,WAAW,IAAI,KAAK,OAAO,KAAA;CAC9E,CAAC;CACD,MAAM,eAAeA,oBAAAA,WAAW;EAAE;EAAU,OAAO;EAAS;CAAO,CAAC;CAEpE,IAAI,KAAA,GAAIC,sBAAAA,8BAAAA,CAA8B,SAAS,MAAM,CAAC;CACtD,IAAI,KAAA,GAAIC,sBAAAA,SAAAA,CAAS,MAAM,CAAC;CACxB,IAAI,IAAIC,sBAAAA,eAAe;CAEvB,IAAI,IAAIC,sBAAAA,kBAAkB;CAE1B,eAAA,QAAO,GAAG;CACV,gBAAA,QAAQ,KAAK,MAAM,QAAQ,SAAS,QAAQ,UAAU;CACtD,eAAA,QAAO,KAAK,MAAM;CAClB,aAAA,QAAK,KAAK,MAAM,QAAQ,QAAQ,WAAW;CAC3C,kBAAA,QAAS,KAAK,MAAM,SAAS,QAAQ,YAAY;CACjD,gBAAA,QAAQ,KAAK,MAAM,SAAS,QAAQ,QAAQ,YAAY;CACxD,aAAA,QAAK,GAAG;CACR,iBAAA,QAAS,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC3C,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,QAAQ,UAAU;CAGpD,IAAI,OAAO,OAAO,OAChB,cAAA,QAAM,KAAK,MAAM,SAAS,QAAQ,QAAQ,YAAY;CAExD,+BAAA,QAAqB,KAAK,MAAM,OAAO;CACvC,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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dist_tags_default from "./dist-tags.mjs";
|
|
2
2
|
import package_default from "./package.mjs";
|
|
3
|
+
import packageVersionStatus from "./package-version-status.mjs";
|
|
3
4
|
import ping_default from "./ping.mjs";
|
|
4
5
|
import publish from "./publish.mjs";
|
|
5
6
|
import { assertTokenStoreSupport, requireOtp } from "./require-otp.mjs";
|
|
@@ -12,7 +13,7 @@ import search_default$1 from "./v1/search.mjs";
|
|
|
12
13
|
import token_default from "./v1/token.mjs";
|
|
13
14
|
import whoami_default from "./whoami.mjs";
|
|
14
15
|
import express from "express";
|
|
15
|
-
import { WebUrlsNamespace, antiLoop, encodeScopePackage, enforceGeneratedTokenMetadata, makeURLrelative, match, registerBodyParser, validateName, validatePackage } from "@verdaccio/middleware";
|
|
16
|
+
import { PACKAGE_API_ENDPOINTS, WebUrlsNamespace, antiLoop, encodeScopePackage, enforceGeneratedTokenMetadata, makeURLrelative, match, registerBodyParser, validateName, validatePackage, validatePackageVersionStatus } from "@verdaccio/middleware";
|
|
16
17
|
import { TfaStore } from "@verdaccio/auth";
|
|
17
18
|
//#region src/index.ts
|
|
18
19
|
function src_default(config, auth, storage, logger) {
|
|
@@ -27,6 +28,7 @@ function src_default(config, auth, storage, logger) {
|
|
|
27
28
|
app.param("org_couchdb_user", match(/^org\.couchdb\.user:/));
|
|
28
29
|
registerBodyParser(app, config);
|
|
29
30
|
app.use(WebUrlsNamespace.endpoints, (_req, _res, next) => next("router"));
|
|
31
|
+
app.get(PACKAGE_API_ENDPOINTS.version_status, validatePackageVersionStatus);
|
|
30
32
|
const apiJwtMiddleware = auth.apiJWTmiddleware();
|
|
31
33
|
app.use((req, res, next) => {
|
|
32
34
|
if (req.remote_user ?? res.locals.remote_user) return next();
|
|
@@ -67,6 +69,7 @@ function src_default(config, auth, storage, logger) {
|
|
|
67
69
|
search_default$1(app, auth, storage, config, logger);
|
|
68
70
|
token_default(app, auth, storage, config, logger, otpForAuth);
|
|
69
71
|
if (config.flags?.stage) stage(app, auth, storage, config, logger, otpForWrites);
|
|
72
|
+
packageVersionStatus(app, auth, storage);
|
|
70
73
|
package_default(app, auth, storage, logger);
|
|
71
74
|
if (config.flags?.webLogin) login_default(app, auth, storage, config, logger);
|
|
72
75
|
return app;
|
package/build/index.mjs.map
CHANGED
|
@@ -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 WebUrlsNamespace,\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 { TfaStore } from '@verdaccio/auth';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport ping from './ping';\nimport publish from './publish';\nimport { assertTokenStoreSupport, requireOtp } from './require-otp';\nimport search from './search';\nimport stage from './stage';\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 app.use(WebUrlsNamespace.endpoints, (_req, _res, next) => next('router'));\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 // built once: without the flag nothing must reach the token store\n let tfaStore: TfaStore | undefined;\n if (config.flags?.tfa) {\n // refuse to start rather than answer 503 on every write later\n assertTokenStoreSupport(storage, logger);\n tfaStore = new TfaStore(storage, config.secret, logger);\n }\n const otpForAuth = requireOtp({ tfaStore, scope: 'auth', logger });\n // at login there is no authenticated user yet: the name is in the body\n const otpForLogin = requireOtp({\n tfaStore,\n scope: 'auth',\n logger,\n getUsername: (req) => (typeof req.body?.name === 'string' ? req.body.name : undefined),\n });\n const otpForWrites = requireOtp({ tfaStore, scope: 'write', logger });\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, storage, logger, otpForAuth);\n search(app, logger);\n user(app, auth, config, logger, otpForLogin);\n distTags(app, auth, storage, logger, otpForWrites);\n publish(app, auth, storage, config, logger, otpForWrites);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger, otpForAuth);\n // must stay before pkg(): its '/:package{/:version}' route would otherwise\n // swallow GET /-/stage\n if (config.flags?.stage) {\n stage(app, auth, storage, config, logger, otpForWrites);\n }\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":"
|
|
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 WebUrlsNamespace,\n PACKAGE_API_ENDPOINTS,\n validatePackageVersionStatus,\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 { TfaStore } from '@verdaccio/auth';\n\nimport distTags from './dist-tags';\nimport pkg from './package';\nimport packageVersionStatus from './package-version-status';\nimport ping from './ping';\nimport publish from './publish';\nimport { assertTokenStoreSupport, requireOtp } from './require-otp';\nimport search from './search';\nimport stage from './stage';\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 app.use(WebUrlsNamespace.endpoints, (_req, _res, next) => next('router'));\n\n app.get(PACKAGE_API_ENDPOINTS.version_status, validatePackageVersionStatus);\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 // built once: without the flag nothing must reach the token store\n let tfaStore: TfaStore | undefined;\n if (config.flags?.tfa) {\n // refuse to start rather than answer 503 on every write later\n assertTokenStoreSupport(storage, logger);\n tfaStore = new TfaStore(storage, config.secret, logger);\n }\n const otpForAuth = requireOtp({ tfaStore, scope: 'auth', logger });\n // at login there is no authenticated user yet: the name is in the body\n const otpForLogin = requireOtp({\n tfaStore,\n scope: 'auth',\n logger,\n getUsername: (req) => (typeof req.body?.name === 'string' ? req.body.name : undefined),\n });\n const otpForWrites = requireOtp({ tfaStore, scope: 'write', logger });\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, storage, logger, otpForAuth);\n search(app, logger);\n user(app, auth, config, logger, otpForLogin);\n distTags(app, auth, storage, logger, otpForWrites);\n publish(app, auth, storage, config, logger, otpForWrites);\n ping(app);\n v1Search(app, auth, storage, config, logger);\n token(app, auth, storage, config, logger, otpForAuth);\n // must stay before pkg(): its '/:package{/:version}' route would otherwise\n // swallow GET /-/stage\n if (config.flags?.stage) {\n stage(app, auth, storage, config, logger, otpForWrites);\n }\n packageVersionStatus(app, auth, storage);\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":";;;;;;;;;;;;;;;;;;AAqCA,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;CAE9B,IAAI,IAAI,iBAAiB,YAAY,MAAM,MAAM,SAAS,KAAK,QAAQ,CAAC;CAExE,IAAI,IAAI,sBAAsB,gBAAgB,4BAA4B;CAG1E,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;CAGD,IAAI;CACJ,IAAI,OAAO,OAAO,KAAK;EAErB,wBAAwB,SAAS,MAAM;EACvC,WAAW,IAAI,SAAS,SAAS,OAAO,QAAQ,MAAM;CACxD;CACA,MAAM,aAAa,WAAW;EAAE;EAAU,OAAO;EAAQ;CAAO,CAAC;CAEjE,MAAM,cAAc,WAAW;EAC7B;EACA,OAAO;EACP;EACA,cAAc,QAAS,OAAO,IAAI,MAAM,SAAS,WAAW,IAAI,KAAK,OAAO,KAAA;CAC9E,CAAC;CACD,MAAM,eAAe,WAAW;EAAE;EAAU,OAAO;EAAS;CAAO,CAAC;CAEpE,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,QAAQ,SAAS,QAAQ,UAAU;CACtD,eAAO,KAAK,MAAM;CAClB,aAAK,KAAK,MAAM,QAAQ,QAAQ,WAAW;CAC3C,kBAAS,KAAK,MAAM,SAAS,QAAQ,YAAY;CACjD,QAAQ,KAAK,MAAM,SAAS,QAAQ,QAAQ,YAAY;CACxD,aAAK,GAAG;CACR,iBAAS,KAAK,MAAM,SAAS,QAAQ,MAAM;CAC3C,cAAM,KAAK,MAAM,SAAS,QAAQ,QAAQ,UAAU;CAGpD,IAAI,OAAO,OAAO,OAChB,MAAM,KAAK,MAAM,SAAS,QAAQ,QAAQ,YAAY;CAExD,qBAAqB,KAAK,MAAM,OAAO;CACvC,gBAAI,KAAK,MAAM,SAAS,MAAM;CAC9B,IAAI,OAAO,OAAO,UAChB,cAAM,KAAK,MAAM,SAAS,QAAQ,MAAM;CAE1C,OAAO;AACT"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
let _verdaccio_middleware = require("@verdaccio/middleware");
|
|
2
|
+
let _verdaccio_core = require("@verdaccio/core");
|
|
3
|
+
//#region src/package-version-status.ts
|
|
4
|
+
function packageVersionStatus(router, auth, storage) {
|
|
5
|
+
router.get(_verdaccio_middleware.PACKAGE_API_ENDPOINTS.version_status, function(req, _res, next) {
|
|
6
|
+
const packageName = _verdaccio_core.reqUtils.paramToString(req.params.statusPackage);
|
|
7
|
+
const version = _verdaccio_core.reqUtils.paramToString(req.params.statusVersion);
|
|
8
|
+
const user = req.remote_user;
|
|
9
|
+
if (!user?.name) return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.MUST_BE_LOGGED));
|
|
10
|
+
auth.allow_publish({
|
|
11
|
+
packageName,
|
|
12
|
+
packageVersion: version
|
|
13
|
+
}, user, async (error, allowed) => {
|
|
14
|
+
if (error && ![
|
|
15
|
+
_verdaccio_core.HTTP_STATUS.UNAUTHORIZED,
|
|
16
|
+
_verdaccio_core.HTTP_STATUS.FORBIDDEN,
|
|
17
|
+
_verdaccio_core.HTTP_STATUS.NOT_FOUND
|
|
18
|
+
].includes(error.status)) return next(error);
|
|
19
|
+
if (error || !allowed) return next(_verdaccio_core.errorUtils.getNotFound("no such package version"));
|
|
20
|
+
try {
|
|
21
|
+
const status = await storage.getPackageVersionStatus(packageName, version);
|
|
22
|
+
return next({
|
|
23
|
+
packageName,
|
|
24
|
+
version,
|
|
25
|
+
status
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return next(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
exports.default = packageVersionStatus;
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=package-version-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-version-status.js","names":["PACKAGE_API_ENDPOINTS","reqUtils","errorUtils","API_ERROR","HTTP_STATUS"],"sources":["../src/package-version-status.ts"],"sourcesContent":["import type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_ERROR, HTTP_STATUS, errorUtils, reqUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function packageVersionStatus(router: Router, auth: Auth, storage: Storage): void {\n router.get(\n PACKAGE_API_ENDPOINTS.version_status,\n function (req: $RequestExtend, _res: $ResponseExtend, next: $NextFunctionVer): void {\n const packageName = reqUtils.paramToString(req.params.statusPackage);\n const version = reqUtils.paramToString(req.params.statusVersion);\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n auth.allow_publish(\n { packageName, packageVersion: version },\n user,\n async (error: any, allowed?: boolean) => {\n if (\n error &&\n ![HTTP_STATUS.UNAUTHORIZED, HTTP_STATUS.FORBIDDEN, HTTP_STATUS.NOT_FOUND].includes(\n error.status\n )\n ) {\n return next(error);\n }\n if (error || !allowed) {\n return next(errorUtils.getNotFound('no such package version'));\n }\n try {\n const status = await storage.getPackageVersionStatus(packageName, version);\n return next({ packageName, version, status });\n } catch (error) {\n return next(error);\n }\n }\n );\n }\n );\n}\n"],"mappings":";;;AASA,SAAwB,qBAAqB,QAAgB,MAAY,SAAwB;CAC/F,OAAO,IACLA,sBAAAA,sBAAsB,gBACtB,SAAU,KAAqB,MAAuB,MAA8B;EAClF,MAAM,cAAcC,gBAAAA,SAAS,cAAc,IAAI,OAAO,aAAa;EACnE,MAAM,UAAUA,gBAAAA,SAAS,cAAc,IAAI,OAAO,aAAa;EAC/D,MAAM,OAAO,IAAI;EACjB,IAAI,CAAC,MAAM,MACT,OAAO,KAAKC,gBAAAA,WAAW,gBAAgBC,gBAAAA,UAAU,cAAc,CAAC;EAElE,KAAK,cACH;GAAE;GAAa,gBAAgB;EAAQ,GACvC,MACA,OAAO,OAAY,YAAsB;GACvC,IACE,SACA,CAAC;IAACC,gBAAAA,YAAY;IAAcA,gBAAAA,YAAY;IAAWA,gBAAAA,YAAY;GAAS,CAAC,CAAC,SACxE,MAAM,MACR,GAEA,OAAO,KAAK,KAAK;GAEnB,IAAI,SAAS,CAAC,SACZ,OAAO,KAAKF,gBAAAA,WAAW,YAAY,yBAAyB,CAAC;GAE/D,IAAI;IACF,MAAM,SAAS,MAAM,QAAQ,wBAAwB,aAAa,OAAO;IACzE,OAAO,KAAK;KAAE;KAAa;KAAS;IAAO,CAAC;GAC9C,SAAS,OAAO;IACd,OAAO,KAAK,KAAK;GACnB;EACF,CACF;CACF,CACF;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PACKAGE_API_ENDPOINTS } from "@verdaccio/middleware";
|
|
2
|
+
import { API_ERROR, HTTP_STATUS, errorUtils, reqUtils } from "@verdaccio/core";
|
|
3
|
+
//#region src/package-version-status.ts
|
|
4
|
+
function packageVersionStatus(router, auth, storage) {
|
|
5
|
+
router.get(PACKAGE_API_ENDPOINTS.version_status, function(req, _res, next) {
|
|
6
|
+
const packageName = reqUtils.paramToString(req.params.statusPackage);
|
|
7
|
+
const version = reqUtils.paramToString(req.params.statusVersion);
|
|
8
|
+
const user = req.remote_user;
|
|
9
|
+
if (!user?.name) return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));
|
|
10
|
+
auth.allow_publish({
|
|
11
|
+
packageName,
|
|
12
|
+
packageVersion: version
|
|
13
|
+
}, user, async (error, allowed) => {
|
|
14
|
+
if (error && ![
|
|
15
|
+
HTTP_STATUS.UNAUTHORIZED,
|
|
16
|
+
HTTP_STATUS.FORBIDDEN,
|
|
17
|
+
HTTP_STATUS.NOT_FOUND
|
|
18
|
+
].includes(error.status)) return next(error);
|
|
19
|
+
if (error || !allowed) return next(errorUtils.getNotFound("no such package version"));
|
|
20
|
+
try {
|
|
21
|
+
const status = await storage.getPackageVersionStatus(packageName, version);
|
|
22
|
+
return next({
|
|
23
|
+
packageName,
|
|
24
|
+
version,
|
|
25
|
+
status
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return next(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { packageVersionStatus as default };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=package-version-status.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-version-status.mjs","names":[],"sources":["../src/package-version-status.ts"],"sourcesContent":["import type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_ERROR, HTTP_STATUS, errorUtils, reqUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function packageVersionStatus(router: Router, auth: Auth, storage: Storage): void {\n router.get(\n PACKAGE_API_ENDPOINTS.version_status,\n function (req: $RequestExtend, _res: $ResponseExtend, next: $NextFunctionVer): void {\n const packageName = reqUtils.paramToString(req.params.statusPackage);\n const version = reqUtils.paramToString(req.params.statusVersion);\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n auth.allow_publish(\n { packageName, packageVersion: version },\n user,\n async (error: any, allowed?: boolean) => {\n if (\n error &&\n ![HTTP_STATUS.UNAUTHORIZED, HTTP_STATUS.FORBIDDEN, HTTP_STATUS.NOT_FOUND].includes(\n error.status\n )\n ) {\n return next(error);\n }\n if (error || !allowed) {\n return next(errorUtils.getNotFound('no such package version'));\n }\n try {\n const status = await storage.getPackageVersionStatus(packageName, version);\n return next({ packageName, version, status });\n } catch (error) {\n return next(error);\n }\n }\n );\n }\n );\n}\n"],"mappings":";;;AASA,SAAwB,qBAAqB,QAAgB,MAAY,SAAwB;CAC/F,OAAO,IACL,sBAAsB,gBACtB,SAAU,KAAqB,MAAuB,MAA8B;EAClF,MAAM,cAAc,SAAS,cAAc,IAAI,OAAO,aAAa;EACnE,MAAM,UAAU,SAAS,cAAc,IAAI,OAAO,aAAa;EAC/D,MAAM,OAAO,IAAI;EACjB,IAAI,CAAC,MAAM,MACT,OAAO,KAAK,WAAW,gBAAgB,UAAU,cAAc,CAAC;EAElE,KAAK,cACH;GAAE;GAAa,gBAAgB;EAAQ,GACvC,MACA,OAAO,OAAY,YAAsB;GACvC,IACE,SACA,CAAC;IAAC,YAAY;IAAc,YAAY;IAAW,YAAY;GAAS,CAAC,CAAC,SACxE,MAAM,MACR,GAEA,OAAO,KAAK,KAAK;GAEnB,IAAI,SAAS,CAAC,SACZ,OAAO,KAAK,WAAW,YAAY,yBAAyB,CAAC;GAE/D,IAAI;IACF,MAAM,SAAS,MAAM,QAAQ,wBAAwB,aAAa,OAAO;IACzE,OAAO,KAAK;KAAE;KAAa;KAAS;IAAO,CAAC;GAC9C,SAAS,OAAO;IACd,OAAO,KAAK,KAAK;GACnB;EACF,CACF;CACF,CACF;AACF"}
|
package/build/package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.js","names":[],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n // the size event races against the first data chunk; setting a\n // header after they are flushed would throw and kill the process\n if (!res.headersSent) {\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n }\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;;;AAWA,
|
|
1
|
+
{"version":3,"file":"package.js","names":["debug","buildDebug","allow","PACKAGE_API_ENDPOINTS","reqUtils","stringUtils","Storage","getRequestOptions","HEADER_TYPE","HEADERS"],"sources":["../src/package.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { HEADERS, HEADER_TYPE, reqUtils, stringUtils } from '@verdaccio/core';\nimport { PACKAGE_API_ENDPOINTS, allow, getRequestOptions } from '@verdaccio/middleware';\nimport { Storage } from '@verdaccio/store';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:package');\n\nexport default function (route: Router, auth: Auth, storage: Storage, logger: Logger): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_by_version,\n can('access'),\n async function (\n req: $RequestExtend,\n _res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const name = reqUtils.paramToString(req.params.package);\n const version = reqUtils.paramToString(req.params.version) || undefined;\n debug('get package by version: %o %o', name, version);\n const abbreviated =\n stringUtils.getByQualityPriorityValue(req.get('Accept')) === Storage.ABBREVIATED_HEADER;\n debug('abbreviated: %o', abbreviated);\n\n const requestOptions = getRequestOptions(req);\n\n try {\n const manifest = await storage.getPackageByOptions({\n name,\n uplinksLook: true,\n abbreviated,\n version,\n requestOptions,\n });\n if (abbreviated) {\n debug('abbreviated response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_INSTALL_CHARSET);\n } else {\n debug('full response');\n _res.setHeader(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);\n }\n\n next(manifest);\n } catch (err) {\n next(err);\n }\n }\n );\n\n route.get(\n PACKAGE_API_ENDPOINTS.get_package_tarball,\n can('access'),\n async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {\n const pkgName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const abort = new AbortController();\n try {\n debug('downloading tarball %o', filename);\n const stream = (await storage.getTarball(pkgName, filename, {\n signal: abort.signal,\n // TODO: review why this param\n // enableRemote: true,\n })) as any;\n\n stream.on('content-length', (size) => {\n debug('tarball size %o', size);\n // the size event races against the first data chunk; setting a\n // header after they are flushed would throw and kill the process\n if (!res.headersSent) {\n res.header(HEADER_TYPE.CONTENT_LENGTH, size);\n }\n });\n\n stream.once('error', (err) => {\n debug('error on download tarball %o', err);\n if (res.headersSent) {\n // The 200 + headers are already on the wire (streaming had started),\n // so an error status can no longer be sent; destroying the response\n // is the only way to signal the failure — otherwise `pipe` just\n // unpipes and the client waits forever on a half-sent body.\n debug('headers already sent, destroying response for %o', req.url);\n res.destroy(err);\n return;\n }\n // The octet-stream header was set optimistically for the download;\n // the error body is JSON (matching registry.npmjs.org), so let the\n // error formatter pick the content type.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n\n req.on('abort', () => {\n debug('request aborted for %o', req.url);\n abort.abort();\n });\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n next(err);\n }\n }\n );\n}\n"],"mappings":";;;;;;;AAWA,IAAMA,WAAAA,GAAQC,MAAAA,QAAAA,CAAW,uBAAuB;AAEhD,SAAA,gBAAyB,OAAe,MAAY,SAAkB,QAAsB;CAC1F,MAAM,OAAA,GAAMC,sBAAAA,MAAAA,CAAM,MAAM;EACtB,YAAY,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;EACtC,WAAW,GAAG,MAAM,OAAO,MAAM,GAAG,CAAC;CACvC,CAAC;CACD,MAAM,IACJC,sBAAAA,sBAAsB,wBACtB,IAAI,QAAQ,GACZ,eACE,KACA,MACA,MACe;EACf,MAAM,OAAOC,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACtD,MAAM,UAAUA,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO,KAAK,KAAA;EAC9D,QAAM,iCAAiC,MAAM,OAAO;EACpD,MAAM,cACJC,gBAAAA,YAAY,0BAA0B,IAAI,IAAI,QAAQ,CAAC,MAAMC,iBAAAA,QAAQ;EACvE,QAAM,mBAAmB,WAAW;EAEpC,MAAM,kBAAA,GAAiBC,sBAAAA,kBAAAA,CAAkB,GAAG;EAE5C,IAAI;GACF,MAAM,WAAW,MAAM,QAAQ,oBAAoB;IACjD;IACA,aAAa;IACb;IACA;IACA;GACF,CAAC;GACD,IAAI,aAAa;IACf,QAAM,sBAAsB;IAC5B,KAAK,UAAUC,gBAAAA,YAAY,cAAcC,gBAAAA,QAAQ,oBAAoB;GACvE,OAAO;IACL,QAAM,eAAe;IACrB,KAAK,UAAUD,gBAAAA,YAAY,cAAcC,gBAAAA,QAAQ,IAAI;GACvD;GAEA,KAAK,QAAQ;EACf,SAAS,KAAK;GACZ,KAAK,GAAG;EACV;CACF,CACF;CAEA,MAAM,IACJN,sBAAAA,sBAAsB,qBACtB,IAAI,QAAQ,GACZ,eAAgB,KAAqB,KAAsB,MAAqB;EAC9E,MAAM,UAAUC,gBAAAA,SAAS,cAAc,IAAI,OAAO,OAAO;EACzD,MAAM,WAAWA,gBAAAA,SAAS,cAAc,IAAI,OAAO,QAAQ;EAC3D,MAAM,QAAQ,IAAI,gBAAgB;EAClC,IAAI;GACF,QAAM,0BAA0B,QAAQ;GACxC,MAAM,SAAU,MAAM,QAAQ,WAAW,SAAS,UAAU,EAC1D,QAAQ,MAAM,OAGhB,CAAC;GAED,OAAO,GAAG,mBAAmB,SAAS;IACpC,QAAM,mBAAmB,IAAI;IAG7B,IAAI,CAAC,IAAI,aACP,IAAI,OAAOI,gBAAAA,YAAY,gBAAgB,IAAI;GAE/C,CAAC;GAED,OAAO,KAAK,UAAU,QAAQ;IAC5B,QAAM,gCAAgC,GAAG;IACzC,IAAI,IAAI,aAAa;KAKnB,QAAM,oDAAoD,IAAI,GAAG;KACjE,IAAI,QAAQ,GAAG;KACf;IACF;IAIA,IAAI,aAAaC,gBAAAA,QAAQ,YAAY;IACrC,IAAI,OAAO,aAAa,GAAG;IAC3B,KAAK,GAAG;GACV,CAAC;GAED,IAAI,GAAG,eAAe;IACpB,QAAM,0BAA0B,IAAI,GAAG;IACvC,MAAM,MAAM;GACd,CAAC;GAED,IAAI,OAAOA,gBAAAA,QAAQ,cAAcA,gBAAAA,QAAQ,YAAY;GACrD,OAAO,KAAK,GAAG;EACjB,SAAS,KAAU;GACjB,IAAI,OAAO,aAAa,GAAG;GAC3B,KAAK,GAAG;EACV;CACF,CACF;AACF"}
|
package/build/ping.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ping.js","names":[],"sources":["../src/ping.ts"],"sourcesContent":["import type { Router } from 'express';\n\nimport { PING_API_ENDPOINTS } from '@verdaccio/middleware';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (route: Router): void {\n route.get(\n PING_API_ENDPOINTS.ping,\n function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n next({});\n }\n );\n}\n"],"mappings":";;AAMA,SAAA,aAAyB,OAAqB;CAC5C,MAAM,
|
|
1
|
+
{"version":3,"file":"ping.js","names":["PING_API_ENDPOINTS"],"sources":["../src/ping.ts"],"sourcesContent":["import type { Router } from 'express';\n\nimport { PING_API_ENDPOINTS } from '@verdaccio/middleware';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nexport default function (route: Router): void {\n route.get(\n PING_API_ENDPOINTS.ping,\n function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n next({});\n }\n );\n}\n"],"mappings":";;AAMA,SAAA,aAAyB,OAAqB;CAC5C,MAAM,IACJA,sBAAAA,mBAAmB,MACnB,SAAU,KAAqB,KAAsB,MAAwB;EAC3E,KAAK,CAAC,CAAC;CACT,CACF;AACF"}
|
package/build/publish.d.ts
CHANGED
|
@@ -82,4 +82,4 @@ import type { Config, Logger } from '@verdaccio/types';
|
|
|
82
82
|
export default function publish(router: Router, auth: Auth, storage: Storage, config: Config, logger: Logger,
|
|
83
83
|
/** No-op unless the caller has two-factor in `auth-and-writes` mode. */
|
|
84
84
|
requireOtp?: RequestHandler): void;
|
|
85
|
-
export declare function publishPackage(storage: Storage, config: Config, logger: Logger, origin: string): any;
|
|
85
|
+
export declare function publishPackage(storage: Storage, config: Config, logger: Logger, origin: string, allowUnpublish?: boolean): any;
|
package/build/publish.js
CHANGED
|
@@ -89,7 +89,7 @@ function publish(router, auth, storage, config, logger, requireOtp = (_req, _res
|
|
|
89
89
|
afterAll: (a, b) => logger.trace(a, b)
|
|
90
90
|
});
|
|
91
91
|
router.put(_verdaccio_middleware.PUBLISH_API_ENDPOINTS.add_package, can("publish"), requireOtp, (0, _verdaccio_middleware.media)(_verdaccio_core.HEADERS.JSON), _verdaccio_middleware.expectJson, publishPackage(storage, config, logger, "publish one version"));
|
|
92
|
-
router.put(_verdaccio_middleware.PUBLISH_API_ENDPOINTS.publish_package, can("unpublish"), requireOtp, (0, _verdaccio_middleware.media)(_verdaccio_core.HEADERS.JSON), _verdaccio_middleware.expectJson, publishPackage(storage, config, logger, "publish with revision"));
|
|
92
|
+
router.put(_verdaccio_middleware.PUBLISH_API_ENDPOINTS.publish_package, can("unpublish"), requireOtp, (0, _verdaccio_middleware.media)(_verdaccio_core.HEADERS.JSON), _verdaccio_middleware.expectJson, publishPackage(storage, config, logger, "publish with revision", true));
|
|
93
93
|
/**
|
|
94
94
|
* Un-publishing an entire package.
|
|
95
95
|
*
|
|
@@ -160,7 +160,7 @@ function publish(router, auth, storage, config, logger, requireOtp = (_req, _res
|
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
-
function publishPackage(storage, config, logger, origin) {
|
|
163
|
+
function publishPackage(storage, config, logger, origin, allowUnpublish = false) {
|
|
164
164
|
return async function(req, res, next) {
|
|
165
165
|
debug$1(origin);
|
|
166
166
|
const ac = new AbortController();
|
|
@@ -174,6 +174,7 @@ function publishPackage(storage, config, logger, origin) {
|
|
|
174
174
|
const requestOptions = (0, _verdaccio_middleware.getRequestOptions)(req);
|
|
175
175
|
try {
|
|
176
176
|
const message = await storage.updateManifest(metadata, {
|
|
177
|
+
allowUnpublish,
|
|
177
178
|
name: packageName,
|
|
178
179
|
revision,
|
|
179
180
|
signal: ac.signal,
|
package/build/publish.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.js","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, reqUtils, 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 /** No-op unless the caller has two-factor in `auth-and-writes` mode. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n requireOtp,\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 requireOtp,\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 requireOtp,\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = reqUtils.paramToString(req.params.package);\n const rev = reqUtils.paramToString(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 requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const revision = reqUtils.paramToString(req.params.revision);\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 = reqUtils.paramToString(req.params.package);\n const revision = reqUtils.paramToString(req.params.revision);\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":["debug","buildDebug","allow","PUBLISH_API_ENDPOINTS","media","HEADERS","expectJson","reqUtils","HTTP_STATUS","notify","API_MESSAGE","tarballUtils","getRequestOptions"],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, reqUtils, 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 /** No-op unless the caller has two-factor in `auth-and-writes` mode. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n requireOtp,\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 requireOtp,\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision', true)\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 requireOtp,\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = reqUtils.paramToString(req.params.package);\n const rev = reqUtils.paramToString(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 requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const revision = reqUtils.paramToString(req.params.revision);\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 allowUnpublish = false\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 = reqUtils.paramToString(req.params.package);\n const revision = reqUtils.paramToString(req.params.revision);\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 allowUnpublish,\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
|
@@ -87,7 +87,7 @@ function publish(router, auth, storage, config, logger, requireOtp = (_req, _res
|
|
|
87
87
|
afterAll: (a, b) => logger.trace(a, b)
|
|
88
88
|
});
|
|
89
89
|
router.put(PUBLISH_API_ENDPOINTS.add_package, can("publish"), requireOtp, media(HEADERS.JSON), expectJson, publishPackage(storage, config, logger, "publish one version"));
|
|
90
|
-
router.put(PUBLISH_API_ENDPOINTS.publish_package, can("unpublish"), requireOtp, media(HEADERS.JSON), expectJson, publishPackage(storage, config, logger, "publish with revision"));
|
|
90
|
+
router.put(PUBLISH_API_ENDPOINTS.publish_package, can("unpublish"), requireOtp, media(HEADERS.JSON), expectJson, publishPackage(storage, config, logger, "publish with revision", true));
|
|
91
91
|
/**
|
|
92
92
|
* Un-publishing an entire package.
|
|
93
93
|
*
|
|
@@ -158,7 +158,7 @@ function publish(router, auth, storage, config, logger, requireOtp = (_req, _res
|
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
-
function publishPackage(storage, config, logger, origin) {
|
|
161
|
+
function publishPackage(storage, config, logger, origin, allowUnpublish = false) {
|
|
162
162
|
return async function(req, res, next) {
|
|
163
163
|
debug(origin);
|
|
164
164
|
const ac = new AbortController();
|
|
@@ -172,6 +172,7 @@ function publishPackage(storage, config, logger, origin) {
|
|
|
172
172
|
const requestOptions = getRequestOptions(req);
|
|
173
173
|
try {
|
|
174
174
|
const message = await storage.updateManifest(metadata, {
|
|
175
|
+
allowUnpublish,
|
|
175
176
|
name: packageName,
|
|
176
177
|
revision,
|
|
177
178
|
signal: ac.signal,
|
package/build/publish.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.mjs","names":[],"sources":["../src/publish.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, reqUtils, 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 /** No-op unless the caller has two-factor in `auth-and-writes` mode. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n requireOtp,\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 requireOtp,\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 requireOtp,\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = reqUtils.paramToString(req.params.package);\n const rev = reqUtils.paramToString(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 requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const revision = reqUtils.paramToString(req.params.revision);\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 = reqUtils.paramToString(req.params.package);\n const revision = reqUtils.paramToString(req.params.revision);\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 { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { API_MESSAGE, HEADERS, HTTP_STATUS, reqUtils, 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 /** No-op unless the caller has two-factor in `auth-and-writes` mode. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n router.put(\n PUBLISH_API_ENDPOINTS.add_package,\n can('publish'),\n requireOtp,\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 requireOtp,\n media(HEADERS.JSON),\n expectJson,\n publishPackage(storage, config, logger, 'publish with revision', true)\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 requireOtp,\n async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {\n const packageName = reqUtils.paramToString(req.params.package);\n const rev = reqUtils.paramToString(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 requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const filename = reqUtils.paramToString(req.params.filename);\n const revision = reqUtils.paramToString(req.params.revision);\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 allowUnpublish = false\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 = reqUtils.paramToString(req.params.package);\n const revision = reqUtils.paramToString(req.params.revision);\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 allowUnpublish,\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/require-otp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"require-otp.js","names":[],"sources":["../src/require-otp.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { NextFunction, Request, RequestHandler, Response } from 'express';\n\nimport type { TfaStore } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:otp');\n\n/**\n * Header carrying the one-time password.\n *\n * npm sets it from `opts.otp` (`npm-registry-fetch/lib/index.js`) and Yarn from\n * `getOtpHeaders` (`plugin-npm/sources/npmHttpUtils.ts`); both spell it the same.\n */\nexport const OTP_HEADER = 'npm-otp';\n\n/**\n * The challenge value clients look for.\n *\n * npm only raises `EOTP` when a 401 carries a `www-authenticate` header whose\n * comma-separated values include `otp`, and Yarn's `isOtpError` does the same.\n * Anything else — including the `Bearer` that `final` adds to every other 401 —\n * surfaces as a plain authentication failure and the client never retries.\n */\nexport const OTP_CHALLENGE = 'otp';\n\n/**\n * Wording kept close to npmjs on purpose: npm has a fallback that matches\n * `/one-time pass/` against the body when the header is missing, so a client\n * that somehow loses the header still recognises the challenge.\n */\nconst OTP_REQUIRED_MESSAGE =\n 'You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.';\n\n/**\n * Which operations a mode covers.\n *\n * - `auth`: logging in and minting tokens, required by both modes\n * - `write`: publishing and other mutations, required only by `auth-and-writes`\n */\nexport type OtpScope = 'auth' | 'write';\n\nexport interface RequireOtpOptions {\n /** Absent when `flags.tfa` is off, which turns the middleware into a no-op. */\n tfaStore?: TfaStore;\n scope: OtpScope;\n logger: Logger;\n /**\n * Resolve whose two-factor configuration applies.\n *\n * Defaults to the authenticated user, but login has no authenticated user\n * yet — there the name comes off the request body.\n */\n getUsername?: (req: Request) => string | undefined;\n}\n\nfunction defaultGetUsername(req: Request): string | undefined {\n const name = (req as $RequestExtend).remote_user?.name;\n return typeof name === 'string' && name.length > 0 ? name : undefined;\n}\n\n/** Answer 401 in the exact shape npm and Yarn recognise as \"send me an OTP\". */\nfunction challenge(res: Response, next: NextFunction): void {\n // must be set before handing over: `final` only defaults to Bearer when the\n // header is absent, and Bearer would make both clients give up\n res.header(HEADERS.WWW_AUTH, OTP_CHALLENGE);\n res.header('npm-notice', 'Provide a one-time password from your authenticator app.');\n next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, OTP_REQUIRED_MESSAGE));\n}\n\n/**\n * Demand a one-time password from users who have two-factor enabled.\n *\n * A no-op when the flag is off, when the user has no two-factor configured, or\n * when their mode does not cover this operation, so requests from everybody\n * else are untouched.\n */\nexport function requireOtp(options: RequireOtpOptions): RequestHandler {\n const { tfaStore, scope, logger } = options;\n const getUsername = options.getUsername ?? defaultGetUsername;\n\n return async function (req: Request, res: Response, next: NextFunction): Promise<void> {\n // flag off: never touch the token store, not even to look\n if (!tfaStore) {\n return next();\n }\n\n const username = getUsername(req);\n if (!username) {\n // not authenticated (or not identifiable): other middleware decides\n return next();\n }\n\n let record;\n try {\n record = await tfaStore.get(username);\n } catch (err) {\n // an unreadable record must not fall through as \"no two-factor\"\n return next(err);\n }\n\n if (!record || record.pending) {\n return next();\n }\n if (scope === 'write' && record.mode === 'auth-only') {\n debug('%o is in auth-only mode, writes do not need an OTP', username);\n return next();\n }\n\n const otp = req.get(OTP_HEADER);\n if (!otp) {\n debug('challenging %o for an OTP', username);\n return challenge(res, next);\n }\n\n if (await tfaStore.verify(username, otp)) {\n debug('OTP accepted for %o', username);\n return next();\n }\n\n logger.warn({ username }, 'rejected an invalid one-time password for @{username}');\n return challenge(res, next);\n };\n}\n\n/**\n * Fail fast when two-factor is enabled on a storage plugin that cannot hold it.\n *\n * `Storage.readTokens` rejects with 503 when the plugin lacks the token\n * interface. Without this check that rejection would surface on every single\n * write, for every user, including those who never enabled two-factor.\n */\nexport function assertTokenStoreSupport(storage: any, logger: Logger): void {\n const plugin = storage?.localStorage?.getStoragePlugin?.();\n const missing = ['saveToken', 'readTokens', 'deleteToken'].filter(\n (method) => typeof plugin?.[method] !== 'function'\n );\n\n if (missing.length > 0) {\n logger.error(\n { missing: missing.join(', ') },\n 'the storage plugin does not implement @{missing}, which two-factor authentication needs'\n );\n throw new Error(\n `flags.tfa is enabled but the storage plugin does not implement ${missing.join(', ')}`\n );\n }\n}\n"],"mappings":";;;;;AASA,
|
|
1
|
+
{"version":3,"file":"require-otp.js","names":["debug","buildDebug","HEADERS","errorUtils","HTTP_STATUS"],"sources":["../src/require-otp.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { NextFunction, Request, RequestHandler, Response } from 'express';\n\nimport type { TfaStore } from '@verdaccio/auth';\nimport { HEADERS, HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport type { Logger } from '@verdaccio/types';\n\nimport type { $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:otp');\n\n/**\n * Header carrying the one-time password.\n *\n * npm sets it from `opts.otp` (`npm-registry-fetch/lib/index.js`) and Yarn from\n * `getOtpHeaders` (`plugin-npm/sources/npmHttpUtils.ts`); both spell it the same.\n */\nexport const OTP_HEADER = 'npm-otp';\n\n/**\n * The challenge value clients look for.\n *\n * npm only raises `EOTP` when a 401 carries a `www-authenticate` header whose\n * comma-separated values include `otp`, and Yarn's `isOtpError` does the same.\n * Anything else — including the `Bearer` that `final` adds to every other 401 —\n * surfaces as a plain authentication failure and the client never retries.\n */\nexport const OTP_CHALLENGE = 'otp';\n\n/**\n * Wording kept close to npmjs on purpose: npm has a fallback that matches\n * `/one-time pass/` against the body when the header is missing, so a client\n * that somehow loses the header still recognises the challenge.\n */\nconst OTP_REQUIRED_MESSAGE =\n 'You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.';\n\n/**\n * Which operations a mode covers.\n *\n * - `auth`: logging in and minting tokens, required by both modes\n * - `write`: publishing and other mutations, required only by `auth-and-writes`\n */\nexport type OtpScope = 'auth' | 'write';\n\nexport interface RequireOtpOptions {\n /** Absent when `flags.tfa` is off, which turns the middleware into a no-op. */\n tfaStore?: TfaStore;\n scope: OtpScope;\n logger: Logger;\n /**\n * Resolve whose two-factor configuration applies.\n *\n * Defaults to the authenticated user, but login has no authenticated user\n * yet — there the name comes off the request body.\n */\n getUsername?: (req: Request) => string | undefined;\n}\n\nfunction defaultGetUsername(req: Request): string | undefined {\n const name = (req as $RequestExtend).remote_user?.name;\n return typeof name === 'string' && name.length > 0 ? name : undefined;\n}\n\n/** Answer 401 in the exact shape npm and Yarn recognise as \"send me an OTP\". */\nfunction challenge(res: Response, next: NextFunction): void {\n // must be set before handing over: `final` only defaults to Bearer when the\n // header is absent, and Bearer would make both clients give up\n res.header(HEADERS.WWW_AUTH, OTP_CHALLENGE);\n res.header('npm-notice', 'Provide a one-time password from your authenticator app.');\n next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, OTP_REQUIRED_MESSAGE));\n}\n\n/**\n * Demand a one-time password from users who have two-factor enabled.\n *\n * A no-op when the flag is off, when the user has no two-factor configured, or\n * when their mode does not cover this operation, so requests from everybody\n * else are untouched.\n */\nexport function requireOtp(options: RequireOtpOptions): RequestHandler {\n const { tfaStore, scope, logger } = options;\n const getUsername = options.getUsername ?? defaultGetUsername;\n\n return async function (req: Request, res: Response, next: NextFunction): Promise<void> {\n // flag off: never touch the token store, not even to look\n if (!tfaStore) {\n return next();\n }\n\n const username = getUsername(req);\n if (!username) {\n // not authenticated (or not identifiable): other middleware decides\n return next();\n }\n\n let record;\n try {\n record = await tfaStore.get(username);\n } catch (err) {\n // an unreadable record must not fall through as \"no two-factor\"\n return next(err);\n }\n\n if (!record || record.pending) {\n return next();\n }\n if (scope === 'write' && record.mode === 'auth-only') {\n debug('%o is in auth-only mode, writes do not need an OTP', username);\n return next();\n }\n\n const otp = req.get(OTP_HEADER);\n if (!otp) {\n debug('challenging %o for an OTP', username);\n return challenge(res, next);\n }\n\n if (await tfaStore.verify(username, otp)) {\n debug('OTP accepted for %o', username);\n return next();\n }\n\n logger.warn({ username }, 'rejected an invalid one-time password for @{username}');\n return challenge(res, next);\n };\n}\n\n/**\n * Fail fast when two-factor is enabled on a storage plugin that cannot hold it.\n *\n * `Storage.readTokens` rejects with 503 when the plugin lacks the token\n * interface. Without this check that rejection would surface on every single\n * write, for every user, including those who never enabled two-factor.\n */\nexport function assertTokenStoreSupport(storage: any, logger: Logger): void {\n const plugin = storage?.localStorage?.getStoragePlugin?.();\n const missing = ['saveToken', 'readTokens', 'deleteToken'].filter(\n (method) => typeof plugin?.[method] !== 'function'\n );\n\n if (missing.length > 0) {\n logger.error(\n { missing: missing.join(', ') },\n 'the storage plugin does not implement @{missing}, which two-factor authentication needs'\n );\n throw new Error(\n `flags.tfa is enabled but the storage plugin does not implement ${missing.join(', ')}`\n );\n }\n}\n"],"mappings":";;;;;AASA,IAAMA,WAAAA,GAAQC,MAAAA,QAAAA,CAAW,mBAAmB;;;;;;;AAQ5C,IAAa,aAAa;;;;;;AAiB1B,IAAM,uBACJ;AAwBF,SAAS,mBAAmB,KAAkC;CAC5D,MAAM,OAAQ,IAAuB,aAAa;CAClD,OAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO,KAAA;AAC9D;;AAGA,SAAS,UAAU,KAAe,MAA0B;CAG1D,IAAI,OAAOC,gBAAAA,QAAQ,UAAA,KAAuB;CAC1C,IAAI,OAAO,cAAc,0DAA0D;CACnF,KAAKC,gBAAAA,WAAW,QAAQC,gBAAAA,YAAY,cAAc,oBAAoB,CAAC;AACzE;;;;;;;;AASA,SAAgB,WAAW,SAA4C;CACrE,MAAM,EAAE,UAAU,OAAO,WAAW;CACpC,MAAM,cAAc,QAAQ,eAAe;CAE3C,OAAO,eAAgB,KAAc,KAAe,MAAmC;EAErF,IAAI,CAAC,UACH,OAAO,KAAK;EAGd,MAAM,WAAW,YAAY,GAAG;EAChC,IAAI,CAAC,UAEH,OAAO,KAAK;EAGd,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,SAAS,IAAI,QAAQ;EACtC,SAAS,KAAK;GAEZ,OAAO,KAAK,GAAG;EACjB;EAEA,IAAI,CAAC,UAAU,OAAO,SACpB,OAAO,KAAK;EAEd,IAAI,UAAU,WAAW,OAAO,SAAS,aAAa;GACpD,QAAM,sDAAsD,QAAQ;GACpE,OAAO,KAAK;EACd;EAEA,MAAM,MAAM,IAAI,IAAI,UAAU;EAC9B,IAAI,CAAC,KAAK;GACR,QAAM,6BAA6B,QAAQ;GAC3C,OAAO,UAAU,KAAK,IAAI;EAC5B;EAEA,IAAI,MAAM,SAAS,OAAO,UAAU,GAAG,GAAG;GACxC,QAAM,uBAAuB,QAAQ;GACrC,OAAO,KAAK;EACd;EAEA,OAAO,KAAK,EAAE,SAAS,GAAG,uDAAuD;EACjF,OAAO,UAAU,KAAK,IAAI;CAC5B;AACF;;;;;;;;AASA,SAAgB,wBAAwB,SAAc,QAAsB;CAC1E,MAAM,SAAS,SAAS,cAAc,mBAAmB;CACzD,MAAM,UAAU;EAAC;EAAa;EAAc;CAAa,CAAC,CAAC,QACxD,WAAW,OAAO,SAAS,YAAY,UAC1C;CAEA,IAAI,QAAQ,SAAS,GAAG;EACtB,OAAO,MACL,EAAE,SAAS,QAAQ,KAAK,IAAI,EAAE,GAC9B,yFACF;EACA,MAAM,IAAI,MACR,kEAAkE,QAAQ,KAAK,IAAI,GACrF;CACF;AACF"}
|
package/build/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["import { HTTP_STATUS } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS } from '@verdaccio/middleware';\nimport type { Logger } from '@verdaccio/types';\n\nexport default function (route, logger: Logger): void {\n // TODO: next major version, remove this\n route.get(SEARCH_API_ENDPOINTS.deprecated_search, function (_req, res) {\n logger.warn('search endpoint has been removed, please use search v1');\n res.status(HTTP_STATUS.NOT_FOUND);\n res.json({ error: 'not found, endpoint was removed' });\n });\n}\n"],"mappings":";;;AAIA,SAAA,eAAyB,OAAO,QAAsB;CAEpD,MAAM,
|
|
1
|
+
{"version":3,"file":"search.js","names":["SEARCH_API_ENDPOINTS","HTTP_STATUS"],"sources":["../src/search.ts"],"sourcesContent":["import { HTTP_STATUS } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS } from '@verdaccio/middleware';\nimport type { Logger } from '@verdaccio/types';\n\nexport default function (route, logger: Logger): void {\n // TODO: next major version, remove this\n route.get(SEARCH_API_ENDPOINTS.deprecated_search, function (_req, res) {\n logger.warn('search endpoint has been removed, please use search v1');\n res.status(HTTP_STATUS.NOT_FOUND);\n res.json({ error: 'not found, endpoint was removed' });\n });\n}\n"],"mappings":";;;AAIA,SAAA,eAAyB,OAAO,QAAsB;CAEpD,MAAM,IAAIA,sBAAAA,qBAAqB,mBAAmB,SAAU,MAAM,KAAK;EACrE,OAAO,KAAK,wDAAwD;EACpE,IAAI,OAAOC,gBAAAA,YAAY,SAAS;EAChC,IAAI,KAAK,EAAE,OAAO,kCAAkC,CAAC;CACvD,CAAC;AACH"}
|
package/build/stage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage.js","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n if (res.headersSent) {\n // The 200 + headers are already on the wire; destroying the\n // response is the only way to signal the failure — and the error\n // middleware must not run again on the terminated connection.\n res.destroy(err);\n return;\n }\n // Error bodies are JSON — drop the optimistic octet-stream header.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stage.js","names":["debug","buildDebug","allow","match","UUID_PATTERN","reqUtils","errorUtils","API_ERROR","STAGE_API_ENDPOINTS","media","HEADERS","expectJson","validationUtils","cryptoUtils","notify","HTTP_STATUS","toStagePackageVersion","HEADER_TYPE","getRequestOptions"],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n if (res.headersSent) {\n // The 200 + headers are already on the wire; destroying the\n // response is the only way to signal the failure — and the error\n // middleware must not run again on the terminated connection.\n res.destroy(err);\n return;\n }\n // Error bodies are JSON — drop the optimistic octet-stream header.\n res.removeHeader(HEADERS.CONTENT_TYPE);\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
|
package/build/user.js
CHANGED
|
@@ -22,7 +22,17 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
|
|
|
22
22
|
token
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
function basicLoginAuth(req, _res, next) {
|
|
26
|
+
if (!(0, _verdaccio_auth.isBasicLoginRequest)(req, config.security)) return next();
|
|
27
|
+
const credentials = (0, _verdaccio_auth.parseBasicLoginCredentials)(req.headers.authorization);
|
|
28
|
+
if (!credentials) return next(_verdaccio_core.errorUtils.getBadRequest(_verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
|
|
29
|
+
auth.authenticate(credentials.user, credentials.password, (err, user) => {
|
|
30
|
+
if (err && !(typeof err.status === "number" && err.status < _verdaccio_core.HTTP_STATUS.INTERNAL_ERROR)) return next(err);
|
|
31
|
+
if (!err && user) req.remote_user = user;
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
route.get(_verdaccio_middleware.USER_API_ENDPOINTS.get_user, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), basicLoginAuth, function(req, res, next) {
|
|
26
36
|
debug$1("verifying user");
|
|
27
37
|
if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
|
|
28
38
|
if (req.remote_user?.error) {
|
|
@@ -65,7 +75,7 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
|
|
|
65
75
|
* @param {Auth} auth
|
|
66
76
|
* @param {Config} config
|
|
67
77
|
*/
|
|
68
|
-
route.put(_verdaccio_middleware.USER_API_ENDPOINTS.add_user, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), function(req, res, next) {
|
|
78
|
+
route.put(_verdaccio_middleware.USER_API_ENDPOINTS.add_user, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), basicLoginAuth, function(req, res, next) {
|
|
69
79
|
const { name, password } = req.body;
|
|
70
80
|
debug$1("login or adduser");
|
|
71
81
|
const remoteName = req?.remote_user?.name;
|
package/build/user.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.js","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"user.js","names":["debug","buildDebug","createRemoteUser","getApiToken","errorUtils","HTTP_STATUS","HEADERS","authUtils","isBasicLoginRequest","parseBasicLoginCredentials","API_ERROR","USER_API_ENDPOINTS","rateLimit","reqUtils","validationUtils","cryptoUtils","API_MESSAGE"],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken, isBasicLoginRequest, parseBasicLoginCredentials } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n function basicLoginAuth(req: $RequestExtend, _res: Response, next: $NextFunctionVer): void {\n if (!isBasicLoginRequest(req, config.security)) {\n return next();\n }\n\n const credentials = parseBasicLoginCredentials(req.headers.authorization as string);\n if (!credentials) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n auth.authenticate(credentials.user, credentials.password, (err, user): void => {\n // a failing backend is reported, not mistaken for an unknown user to register\n if (err && !(typeof err.status === 'number' && err.status < HTTP_STATUS.INTERNAL_ERROR)) {\n return next(err);\n }\n if (!err && user) {\n req.remote_user = user;\n }\n next();\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n basicLoginAuth,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n basicLoginAuth,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
|
package/build/user.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { USER_API_ENDPOINTS, rateLimit } from "@verdaccio/middleware";
|
|
2
|
-
import { getApiToken } from "@verdaccio/auth";
|
|
2
|
+
import { getApiToken, isBasicLoginRequest, parseBasicLoginCredentials } from "@verdaccio/auth";
|
|
3
3
|
import { API_ERROR, API_MESSAGE, HEADERS, HTTP_STATUS, authUtils, cryptoUtils, errorUtils, reqUtils, validationUtils } from "@verdaccio/core";
|
|
4
4
|
import buildDebug from "debug";
|
|
5
5
|
import { createRemoteUser } from "@verdaccio/config";
|
|
@@ -20,7 +20,17 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
|
|
|
20
20
|
token
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function basicLoginAuth(req, _res, next) {
|
|
24
|
+
if (!isBasicLoginRequest(req, config.security)) return next();
|
|
25
|
+
const credentials = parseBasicLoginCredentials(req.headers.authorization);
|
|
26
|
+
if (!credentials) return next(errorUtils.getBadRequest(API_ERROR.BAD_USERNAME_PASSWORD));
|
|
27
|
+
auth.authenticate(credentials.user, credentials.password, (err, user) => {
|
|
28
|
+
if (err && !(typeof err.status === "number" && err.status < HTTP_STATUS.INTERNAL_ERROR)) return next(err);
|
|
29
|
+
if (!err && user) req.remote_user = user;
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
route.get(USER_API_ENDPOINTS.get_user, rateLimit(config?.userRateLimit), basicLoginAuth, function(req, res, next) {
|
|
24
34
|
debug("verifying user");
|
|
25
35
|
if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
|
|
26
36
|
if (req.remote_user?.error) {
|
|
@@ -63,7 +73,7 @@ function user_default(route, auth, config, logger, requireOtp = (_req, _res, nex
|
|
|
63
73
|
* @param {Auth} auth
|
|
64
74
|
* @param {Config} config
|
|
65
75
|
*/
|
|
66
|
-
route.put(USER_API_ENDPOINTS.add_user, rateLimit(config?.userRateLimit), function(req, res, next) {
|
|
76
|
+
route.put(USER_API_ENDPOINTS.add_user, rateLimit(config?.userRateLimit), basicLoginAuth, function(req, res, next) {
|
|
67
77
|
const { name, password } = req.body;
|
|
68
78
|
debug("login or adduser");
|
|
69
79
|
const remoteName = req?.remote_user?.name;
|
package/build/user.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.mjs","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"user.mjs","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Response, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken, isBasicLoginRequest, parseBasicLoginCredentials } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport {\n API_ERROR,\n API_MESSAGE,\n HEADERS,\n HTTP_STATUS,\n authUtils,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { USER_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Config, Logger, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n logger: Logger,\n /** No-op unless the user logging in has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n async function issueLoginToken(\n req: $RequestExtend,\n res: Response,\n next: $NextFunctionVer,\n name: string,\n password: string,\n user: RemoteUser | undefined\n ): Promise<void> {\n const restoredRemoteUser: RemoteUser = createRemoteUser(name, user?.groups || []);\n const token = await getApiToken(auth, config, restoredRemoteUser, password);\n debug('login: new token');\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n\n const message = authUtils.getAuthenticatedMessage(name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\n }\n\n function basicLoginAuth(req: $RequestExtend, _res: Response, next: $NextFunctionVer): void {\n if (!isBasicLoginRequest(req, config.security)) {\n return next();\n }\n\n const credentials = parseBasicLoginCredentials(req.headers.authorization as string);\n if (!credentials) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n auth.authenticate(credentials.user, credentials.password, (err, user): void => {\n // a failing backend is reported, not mistaken for an unknown user to register\n if (err && !(typeof err.status === 'number' && err.status < HTTP_STATUS.INTERNAL_ERROR)) {\n return next(err);\n }\n if (!err && user) {\n req.remote_user = user;\n }\n next();\n });\n }\n\n route.get(\n USER_API_ENDPOINTS.get_user,\n rateLimit(config?.userRateLimit),\n basicLoginAuth,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n debug('verifying user');\n\n if (\n !req.remote_user ||\n typeof req.remote_user.name !== 'string' ||\n req.remote_user.name === ''\n ) {\n // Only fires when a custom auth plugin's apiJWTmiddleware records the failure here;\n // the bundled middleware propagates auth errors instead of storing them on remote_user.\n if (req.remote_user?.error) {\n debug('user authentication failed: %o', req.remote_user.error);\n // a malformed Authorization header is a client error, not a credential failure\n if (req.remote_user.error === API_ERROR.BAD_AUTH_HEADER) {\n return next(errorUtils.getBadRequest(API_ERROR.BAD_AUTH_HEADER));\n }\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n // When Bearer token verification fails the auth middleware intentionally keeps an\n // anonymous user (without recording an error) to stay compatible with npm clients.\n // If credentials were provided but the user is still anonymous they were rejected.\n if (req.headers.authorization) {\n debug('credentials were provided but rejected');\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n debug('user not logged in');\n res.status(HTTP_STATUS.OK);\n return next({ ok: false });\n }\n\n const orgCouchdbUser = reqUtils.paramToString(req.params.org_couchdb_user);\n const userName = orgCouchdbUser?.split(':')[1] ?? '';\n const message = authUtils.getAuthenticatedMessage(req.remote_user.name);\n debug('user authenticated message %o', message);\n res.status(HTTP_STATUS.OK);\n next({\n // 'npm owner' requires user info\n // TODO: we don't have the email\n name: userName,\n email: '',\n ok: message,\n });\n }\n );\n\n /**\n * \n * body example\n * req.body = {\n _id: \"org.couchdb.user:jjjj\",\n name: \"jjjj\",\n password: \"jjjj\",\n type: \"user\",\n roles: [],\n date: \"2022-07-08T15:51:04.002Z\",\n }\n * \n * @export\n * @param {Router} route\n * @param {Auth} auth\n * @param {Config} config\n */\n route.put(\n USER_API_ENDPOINTS.add_user,\n rateLimit(config?.userRateLimit),\n basicLoginAuth,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n const { name, password } = req.body;\n debug('login or adduser');\n const remoteName = req?.remote_user?.name;\n\n const userName = reqUtils.paramToString(req.params.org_couchdb_user);\n if (!validationUtils.validateUserName(userName, name)) {\n return next(errorUtils.getBadRequest(API_ERROR.USERNAME_MISMATCH));\n }\n\n if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {\n debug('login: no remote user detected');\n auth.authenticate(\n name,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { name, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n Promise.resolve(\n requireOtp(req, res, (otpError?: any) => {\n if (otpError) {\n return next(otpError);\n }\n issueLoginToken(req, res, next, name, password, user as RemoteUser).catch(next);\n })\n ).catch(next);\n }\n );\n } else {\n debug('adduser: %o', name);\n if (\n validationUtils.validatePassword(password, config?.server?.passwordValidationRegex) ===\n false\n ) {\n debug('adduser: invalid password');\n\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));\n }\n\n auth.add_user(name, password, async function (err, user): Promise<void> {\n if (err) {\n if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {\n debug('adduser: error on create user');\n // With npm registering is the same as logging in,\n // and npm accepts only an 409 error.\n // So, changing status code here.\n return next(\n errorUtils.getCode(err.status, err.message) || errorUtils.getConflict(err.message)\n );\n }\n return next(err);\n }\n\n const token =\n name && password\n ? await getApiToken(auth, config, user as RemoteUser, password)\n : undefined;\n if (token) {\n debug('adduser: new token %o', cryptoUtils.mask(token as string, 4));\n }\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n req.remote_user = user;\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n debug('adduser: user has been created');\n return next({\n ok: `user '${req.body.name}' created`,\n token,\n });\n });\n }\n }\n );\n\n route.delete(\n USER_API_ENDPOINTS.user_token,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n res.status(HTTP_STATUS.OK);\n next({\n ok: API_MESSAGE.LOGGED_OUT,\n });\n }\n );\n}\n"],"mappings":"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"}
|
package/build/v1/login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","names":[],"sources":["../../src/v1/login.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Response, Router } from 'express';\nimport { randomUUID } from 'node:crypto';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport { API_ERROR, HEADERS, HTTP_STATUS, authUtils, errorUtils, reqUtils } from '@verdaccio/core';\nimport { LOGIN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nconst debug = buildDebug('verdaccio:api:login');\n\nconst WEB_LOGIN_SESSION_ID = 'web-login-sessionId';\n\n// https://github.com/npm/npm-profile/blob/main/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.post(\n LOGIN_API_ENDPOINTS.login,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response): Promise<void> {\n // create new login session (without token)\n const sessionId = randomUUID();\n debug('creating login session %o', sessionId);\n\n await storage.saveToken({\n user: sessionId,\n token: '',\n key: WEB_LOGIN_SESSION_ID,\n readonly: false,\n created: new Date().getTime(),\n });\n\n res.status(HTTP_STATUS.OK);\n\n // Assuming Web UI is running on the same address as API\n const protocol = req.protocol;\n const host = req.hostname;\n const port = req.socket.localPort;\n\n const loginUrl = `${protocol}://${host}:${port}/-/web/login?next=${LOGIN_API_ENDPOINTS.login_cli}/${sessionId}`;\n const doneUrl = `${protocol}://${host}:${port}${LOGIN_API_ENDPOINTS.login_done}/${sessionId}`;\n debug('loginUrl: %o', loginUrl);\n debug('doneUrl: %o', doneUrl);\n\n res.json({\n loginUrl,\n doneUrl,\n });\n }\n );\n\n route.get(\n LOGIN_API_ENDPOINTS.login_done_session,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const sessionId = reqUtils.paramToString(req.params.sessionId);\n debug('polling login session %o', sessionId);\n\n if (!sessionId) {\n debug('sessionId is missing');\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_REQUIRED));\n }\n if (sessionId.length !== 36) {\n debug('sessionId is invalid length: %o', sessionId.length);\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\n }\n\n try {\n const tokens = await storage.readTokens({ user: sessionId });\n debug('tokens found for sessionId %o: %o', sessionId, tokens.length);\n if (tokens && tokens.length === 1 && tokens[0].key === WEB_LOGIN_SESSION_ID) {\n if (tokens[0].token.length === 0) {\n debug('waiting for authentication');\n // Poll again after short delay\n // TODO: make this configurable (default 5 seconds)\n res.status(HTTP_STATUS.ACCEPTED);\n res.set(HEADERS.RETRY_AFTER, '5');\n res.json({});\n } else {\n // session token can only be used once\n debug('deleting session token');\n await storage.deleteToken(sessionId, tokens[0].key);\n\n // Check if token has expired\n // TODO: make this configurable (default 2 minutes)\n const tokenCreatedDate = new Date(tokens[0].created);\n const minutesAgo = new Date(Date.now() - 2 * 60 * 1000);\n if (tokenCreatedDate < minutesAgo) {\n debug('session token expired');\n return next(errorUtils.getUnauthorized(API_ERROR.SESSION_TOKEN_EXPIRED));\n }\n\n debug('session token is valid, login successful');\n\n res.status(HTTP_STATUS.OK);\n res.json({ token: tokens[0].token });\n }\n } else {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\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 );\n\n route.post(\n LOGIN_API_ENDPOINTS.login_cli_session,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const { username, password } = req.body;\n const sessionId = reqUtils.paramToString(req.params.sessionId);\n debug('authenticating login session %o for user %o', sessionId, username);\n\n if (!sessionId) {\n debug('sessionId is missing');\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_REQUIRED));\n }\n if (sessionId.length !== 36) {\n debug('sessionId is invalid length: %o', sessionId.length);\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\n }\n\n auth.authenticate(\n username,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { username, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, err.message));\n }\n\n const remoteUser = createRemoteUser(username, user?.groups || []);\n const token = await getApiToken(auth, config, remoteUser, password);\n\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n // Replace login session with token (to be picked up by the \"done\" endpoint)\n debug('saving token for sessionId %o', sessionId);\n await storage.deleteToken(sessionId, WEB_LOGIN_SESSION_ID);\n await storage.saveToken({\n user: sessionId,\n token,\n key: WEB_LOGIN_SESSION_ID,\n readonly: false,\n created: new Date().getTime(),\n });\n\n const message = authUtils.getAuthenticatedMessage(remoteUser.name ?? '');\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n res.json({ ok: message, token });\n }\n );\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"login.js","names":["debug","buildDebug","LOGIN_API_ENDPOINTS","rateLimit","randomUUID","HTTP_STATUS","reqUtils","errorUtils","API_ERROR","HEADERS","createRemoteUser","getApiToken","authUtils"],"sources":["../../src/v1/login.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Response, Router } from 'express';\nimport { randomUUID } from 'node:crypto';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken } from '@verdaccio/auth';\nimport { createRemoteUser } from '@verdaccio/config';\nimport { API_ERROR, HEADERS, HTTP_STATUS, authUtils, errorUtils, reqUtils } from '@verdaccio/core';\nimport { LOGIN_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nconst debug = buildDebug('verdaccio:api:login');\n\nconst WEB_LOGIN_SESSION_ID = 'web-login-sessionId';\n\n// https://github.com/npm/npm-profile/blob/main/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.post(\n LOGIN_API_ENDPOINTS.login,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response): Promise<void> {\n // create new login session (without token)\n const sessionId = randomUUID();\n debug('creating login session %o', sessionId);\n\n await storage.saveToken({\n user: sessionId,\n token: '',\n key: WEB_LOGIN_SESSION_ID,\n readonly: false,\n created: new Date().getTime(),\n });\n\n res.status(HTTP_STATUS.OK);\n\n // Assuming Web UI is running on the same address as API\n const protocol = req.protocol;\n const host = req.hostname;\n const port = req.socket.localPort;\n\n const loginUrl = `${protocol}://${host}:${port}/-/web/login?next=${LOGIN_API_ENDPOINTS.login_cli}/${sessionId}`;\n const doneUrl = `${protocol}://${host}:${port}${LOGIN_API_ENDPOINTS.login_done}/${sessionId}`;\n debug('loginUrl: %o', loginUrl);\n debug('doneUrl: %o', doneUrl);\n\n res.json({\n loginUrl,\n doneUrl,\n });\n }\n );\n\n route.get(\n LOGIN_API_ENDPOINTS.login_done_session,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const sessionId = reqUtils.paramToString(req.params.sessionId);\n debug('polling login session %o', sessionId);\n\n if (!sessionId) {\n debug('sessionId is missing');\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_REQUIRED));\n }\n if (sessionId.length !== 36) {\n debug('sessionId is invalid length: %o', sessionId.length);\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\n }\n\n try {\n const tokens = await storage.readTokens({ user: sessionId });\n debug('tokens found for sessionId %o: %o', sessionId, tokens.length);\n if (tokens && tokens.length === 1 && tokens[0].key === WEB_LOGIN_SESSION_ID) {\n if (tokens[0].token.length === 0) {\n debug('waiting for authentication');\n // Poll again after short delay\n // TODO: make this configurable (default 5 seconds)\n res.status(HTTP_STATUS.ACCEPTED);\n res.set(HEADERS.RETRY_AFTER, '5');\n res.json({});\n } else {\n // session token can only be used once\n debug('deleting session token');\n await storage.deleteToken(sessionId, tokens[0].key);\n\n // Check if token has expired\n // TODO: make this configurable (default 2 minutes)\n const tokenCreatedDate = new Date(tokens[0].created);\n const minutesAgo = new Date(Date.now() - 2 * 60 * 1000);\n if (tokenCreatedDate < minutesAgo) {\n debug('session token expired');\n return next(errorUtils.getUnauthorized(API_ERROR.SESSION_TOKEN_EXPIRED));\n }\n\n debug('session token is valid, login successful');\n\n res.status(HTTP_STATUS.OK);\n res.json({ token: tokens[0].token });\n }\n } else {\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\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 );\n\n route.post(\n LOGIN_API_ENDPOINTS.login_cli_session,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const { username, password } = req.body;\n const sessionId = reqUtils.paramToString(req.params.sessionId);\n debug('authenticating login session %o for user %o', sessionId, username);\n\n if (!sessionId) {\n debug('sessionId is missing');\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_REQUIRED));\n }\n if (sessionId.length !== 36) {\n debug('sessionId is invalid length: %o', sessionId.length);\n return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.SESSION_ID_INVALID));\n }\n\n auth.authenticate(\n username,\n password,\n async function callbackAuthenticate(err, user): Promise<void> {\n if (err) {\n logger.trace(\n { username, err },\n 'authenticating for user @{username} failed. Error: @{err.message}'\n );\n return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, err.message));\n }\n\n const remoteUser = createRemoteUser(username, user?.groups || []);\n const token = await getApiToken(auth, config, remoteUser, password);\n\n if (!token) {\n return next(errorUtils.getUnauthorized());\n }\n\n // Replace login session with token (to be picked up by the \"done\" endpoint)\n debug('saving token for sessionId %o', sessionId);\n await storage.deleteToken(sessionId, WEB_LOGIN_SESSION_ID);\n await storage.saveToken({\n user: sessionId,\n token,\n key: WEB_LOGIN_SESSION_ID,\n readonly: false,\n created: new Date().getTime(),\n });\n\n const message = authUtils.getAuthenticatedMessage(remoteUser.name ?? '');\n\n res.status(HTTP_STATUS.CREATED);\n res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);\n res.json({ ok: message, token });\n }\n );\n }\n );\n}\n"],"mappings":"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"}
|
package/build/v1/profile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.js","names":[],"sources":["../../src/v1/profile.ts"],"sourcesContent":["import type { RequestHandler, Response, Router } from 'express';\nimport { isEmpty, isNil } from 'lodash-es';\n\nimport type { Auth, TfaMode, TfaStatus } from '@verdaccio/auth';\nimport { TfaStore } from '@verdaccio/auth';\nimport {\n API_ERROR,\n APP_ERROR,\n HTTP_STATUS,\n SUPPORT_ERRORS,\n errorUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { PROFILE_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nconst TFA_MODES: TfaMode[] = ['auth-only', 'auth-and-writes'];\n\nexport interface Profile {\n /**\n * `false` when two-factor is off, otherwise the mode and whether enrolment is\n * still half-finished. `npm profile` reads `tfa.mode` and `tfa.pending`.\n */\n tfa: TfaStatus;\n name: string;\n email: string;\n email_verified: boolean;\n created: string;\n updated: string;\n cidr_whitelist: string[] | null;\n fullname: string;\n}\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n storage?: Storage,\n logger?: Logger,\n /** No-op unless the caller already has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n // only built when the flag is on: without it nothing must touch the token store\n const tfaStore =\n config.flags?.tfa && storage && logger\n ? new TfaStore(storage, config.secret, logger)\n : undefined;\n\n function buildProfile(name: string, tfa: TfaStatus = false): Profile {\n return {\n tfa,\n name,\n email: '',\n email_verified: false,\n created: '',\n updated: '',\n cidr_whitelist: null,\n fullname: '',\n };\n }\n\n route.get(\n PROFILE_API_ENDPOINTS.get_profile,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const { name } = req.remote_user;\n if (isNil(name)) {\n res.status(HTTP_STATUS.UNAUTHORIZED);\n return next({\n message: API_ERROR.MUST_BE_LOGGED,\n });\n }\n\n try {\n const tfa = tfaStore ? await tfaStore.status(name) : false;\n return next(buildProfile(name, tfa));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Promisified `auth.authenticate`, used to re-check the password. */\n function verifyPassword(username: string, password: unknown): Promise<boolean> {\n if (typeof password !== 'string' || isEmpty(password)) {\n return Promise.resolve(false);\n }\n return new Promise((resolve) => {\n auth.authenticate(username, password, (err) => resolve(!err));\n });\n }\n\n /**\n * The `tfa` body of `POST /-/npm/v1/user`, in the three shapes `npm profile`\n * sends:\n *\n * { mode, password } start enrolment -> { tfa: 'otpauth://...' }\n * [ '123456' ] finish enrolment -> { tfa: [recovery codes] }\n * { mode: 'disable', password } turn it off -> the profile\n *\n * Step two must answer with a plain string: the CLI checks\n * `/^otpauth:[/][/]/` and aborts otherwise. Step three must answer with an\n * array, which the CLI prints as the recovery codes.\n */\n async function handleTfa(tfa: any, name: string, next: $NextFunctionVer): Promise<void> {\n const store = tfaStore as TfaStore;\n\n // finish enrolment: `{ tfa: ['123456'] }`\n if (Array.isArray(tfa)) {\n const [code] = tfa;\n const recoveryCodes = await store.completeEnrolment(name, String(code ?? ''));\n if (!recoveryCodes) {\n return next(errorUtils.getUnauthorized('invalid one-time password'));\n }\n return next({ tfa: recoveryCodes });\n }\n\n if (typeof tfa !== 'object' || tfa === null) {\n return next(errorUtils.getBadRequest('unsupported two-factor payload'));\n }\n\n const { mode, password } = tfa;\n if (!(await verifyPassword(name, password))) {\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n if (mode === 'disable') {\n await store.disable(name);\n logger?.info({ name }, 'two-factor authentication disabled for @{name}');\n return next(buildProfile(name, false));\n }\n\n if (TFA_MODES.includes(mode) === false) {\n return next(\n errorUtils.getBadRequest(`two-factor mode must be one of ${TFA_MODES.join(', ')}`)\n );\n }\n\n const { otpauthUrl } = await store.beginEnrolment(name, mode, config.web?.title ?? 'Verdaccio');\n logger?.info({ name, mode }, 'two-factor enrolment started for @{name} in mode @{mode}');\n return next({ tfa: otpauthUrl });\n }\n\n route.post(\n PROFILE_API_ENDPOINTS.get_profile,\n rateLimit(config?.userRateLimit),\n // Turning two-factor off, switching its mode or changing the password are\n // exactly the operations a stolen session must not be able to perform on\n // its own. The middleware stays quiet while enrolment is pending, so the\n // three steps of `npm profile enable-2fa` are unaffected; `otplease` in the\n // CLI answers the challenge on the ones that follow.\n requireOtp,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n if (isNil(req.remote_user.name)) {\n res.status(HTTP_STATUS.UNAUTHORIZED);\n return next({\n message: API_ERROR.MUST_BE_LOGGED,\n });\n }\n\n const { password, tfa } = req.body;\n const { name } = req.remote_user;\n\n if (isNil(password) === false) {\n if (\n validationUtils.validatePassword(\n password.new,\n config?.server?.passwordValidationRegex\n ) === false\n ) {\n /* eslint new-cap:off */\n return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT));\n }\n\n if (isEmpty(password.old)) {\n return next(errorUtils.getBadRequest('old password is required'));\n }\n\n auth.changePassword(\n name,\n password.old,\n password.new,\n (err, isUpdated): $NextFunctionVer => {\n if (err !== null) {\n return next(errorUtils.getForbidden(err.message));\n }\n\n if (isUpdated) {\n return next(buildProfile(req.remote_user.name));\n }\n return next(errorUtils.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));\n }\n );\n } else if (isNil(tfa) === false) {\n if (!tfaStore) {\n return next(\n errorUtils.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED)\n );\n }\n handleTfa(tfa, name, next).catch(next);\n } else {\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));\n }\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"profile.js","names":["TfaStore","PROFILE_API_ENDPOINTS","rateLimit","isNil","HTTP_STATUS","API_ERROR","isEmpty","errorUtils","validationUtils","SUPPORT_ERRORS","APP_ERROR"],"sources":["../../src/v1/profile.ts"],"sourcesContent":["import type { RequestHandler, Response, Router } from 'express';\nimport { isEmpty, isNil } from 'lodash-es';\n\nimport type { Auth, TfaMode, TfaStatus } from '@verdaccio/auth';\nimport { TfaStore } from '@verdaccio/auth';\nimport {\n API_ERROR,\n APP_ERROR,\n HTTP_STATUS,\n SUPPORT_ERRORS,\n errorUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { PROFILE_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../../types/custom';\n\nconst TFA_MODES: TfaMode[] = ['auth-only', 'auth-and-writes'];\n\nexport interface Profile {\n /**\n * `false` when two-factor is off, otherwise the mode and whether enrolment is\n * still half-finished. `npm profile` reads `tfa.mode` and `tfa.pending`.\n */\n tfa: TfaStatus;\n name: string;\n email: string;\n email_verified: boolean;\n created: string;\n updated: string;\n cidr_whitelist: string[] | null;\n fullname: string;\n}\n\nexport default function (\n route: Router,\n auth: Auth,\n config: Config,\n storage?: Storage,\n logger?: Logger,\n /** No-op unless the caller already has two-factor enabled. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n // only built when the flag is on: without it nothing must touch the token store\n const tfaStore =\n config.flags?.tfa && storage && logger\n ? new TfaStore(storage, config.secret, logger)\n : undefined;\n\n function buildProfile(name: string, tfa: TfaStatus = false): Profile {\n return {\n tfa,\n name,\n email: '',\n email_verified: false,\n created: '',\n updated: '',\n cidr_whitelist: null,\n fullname: '',\n };\n }\n\n route.get(\n PROFILE_API_ENDPOINTS.get_profile,\n rateLimit(config?.userRateLimit),\n async function (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> {\n const { name } = req.remote_user;\n if (isNil(name)) {\n res.status(HTTP_STATUS.UNAUTHORIZED);\n return next({\n message: API_ERROR.MUST_BE_LOGGED,\n });\n }\n\n try {\n const tfa = tfaStore ? await tfaStore.status(name) : false;\n return next(buildProfile(name, tfa));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Promisified `auth.authenticate`, used to re-check the password. */\n function verifyPassword(username: string, password: unknown): Promise<boolean> {\n if (typeof password !== 'string' || isEmpty(password)) {\n return Promise.resolve(false);\n }\n return new Promise((resolve) => {\n auth.authenticate(username, password, (err) => resolve(!err));\n });\n }\n\n /**\n * The `tfa` body of `POST /-/npm/v1/user`, in the three shapes `npm profile`\n * sends:\n *\n * { mode, password } start enrolment -> { tfa: 'otpauth://...' }\n * [ '123456' ] finish enrolment -> { tfa: [recovery codes] }\n * { mode: 'disable', password } turn it off -> the profile\n *\n * Step two must answer with a plain string: the CLI checks\n * `/^otpauth:[/][/]/` and aborts otherwise. Step three must answer with an\n * array, which the CLI prints as the recovery codes.\n */\n async function handleTfa(tfa: any, name: string, next: $NextFunctionVer): Promise<void> {\n const store = tfaStore as TfaStore;\n\n // finish enrolment: `{ tfa: ['123456'] }`\n if (Array.isArray(tfa)) {\n const [code] = tfa;\n const recoveryCodes = await store.completeEnrolment(name, String(code ?? ''));\n if (!recoveryCodes) {\n return next(errorUtils.getUnauthorized('invalid one-time password'));\n }\n return next({ tfa: recoveryCodes });\n }\n\n if (typeof tfa !== 'object' || tfa === null) {\n return next(errorUtils.getBadRequest('unsupported two-factor payload'));\n }\n\n const { mode, password } = tfa;\n if (!(await verifyPassword(name, password))) {\n return next(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));\n }\n\n if (mode === 'disable') {\n await store.disable(name);\n logger?.info({ name }, 'two-factor authentication disabled for @{name}');\n return next(buildProfile(name, false));\n }\n\n if (TFA_MODES.includes(mode) === false) {\n return next(\n errorUtils.getBadRequest(`two-factor mode must be one of ${TFA_MODES.join(', ')}`)\n );\n }\n\n const { otpauthUrl } = await store.beginEnrolment(name, mode, config.web?.title ?? 'Verdaccio');\n logger?.info({ name, mode }, 'two-factor enrolment started for @{name} in mode @{mode}');\n return next({ tfa: otpauthUrl });\n }\n\n route.post(\n PROFILE_API_ENDPOINTS.get_profile,\n rateLimit(config?.userRateLimit),\n // Turning two-factor off, switching its mode or changing the password are\n // exactly the operations a stolen session must not be able to perform on\n // its own. The middleware stays quiet while enrolment is pending, so the\n // three steps of `npm profile enable-2fa` are unaffected; `otplease` in the\n // CLI answers the challenge on the ones that follow.\n requireOtp,\n function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {\n if (isNil(req.remote_user.name)) {\n res.status(HTTP_STATUS.UNAUTHORIZED);\n return next({\n message: API_ERROR.MUST_BE_LOGGED,\n });\n }\n\n const { password, tfa } = req.body;\n const { name } = req.remote_user;\n\n if (isNil(password) === false) {\n if (\n validationUtils.validatePassword(\n password.new,\n config?.server?.passwordValidationRegex\n ) === false\n ) {\n /* eslint new-cap:off */\n return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT));\n }\n\n if (isEmpty(password.old)) {\n return next(errorUtils.getBadRequest('old password is required'));\n }\n\n auth.changePassword(\n name,\n password.old,\n password.new,\n (err, isUpdated): $NextFunctionVer => {\n if (err !== null) {\n return next(errorUtils.getForbidden(err.message));\n }\n\n if (isUpdated) {\n return next(buildProfile(req.remote_user.name));\n }\n return next(errorUtils.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));\n }\n );\n } else if (isNil(tfa) === false) {\n if (!tfaStore) {\n return next(\n errorUtils.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED)\n );\n }\n handleTfa(tfa, name, next).catch(next);\n } else {\n return next(errorUtils.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));\n }\n }\n );\n}\n"],"mappings":";;;;;AAmBA,IAAM,YAAuB,CAAC,aAAa,iBAAiB;AAiB5D,SAAA,gBACE,OACA,MACA,QACA,SACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CAEN,MAAM,WACJ,OAAO,OAAO,OAAO,WAAW,SAC5B,IAAIA,gBAAAA,SAAS,SAAS,OAAO,QAAQ,MAAM,IAC3C,KAAA;CAEN,SAAS,aAAa,MAAc,MAAiB,OAAgB;EACnE,OAAO;GACL;GACA;GACA,OAAO;GACP,gBAAgB;GAChB,SAAS;GACT,SAAS;GACT,gBAAgB;GAChB,UAAU;EACZ;CACF;CAEA,MAAM,IACJC,sBAAAA,sBAAsB,cAAA,GACtBC,sBAAAA,UAAAA,CAAU,QAAQ,aAAa,GAC/B,eAAgB,KAAqB,KAAe,MAAuC;EACzF,MAAM,EAAE,SAAS,IAAI;EACrB,KAAA,GAAIC,UAAAA,MAAAA,CAAM,IAAI,GAAG;GACf,IAAI,OAAOC,gBAAAA,YAAY,YAAY;GACnC,OAAO,KAAK,EACV,SAASC,gBAAAA,UAAU,eACrB,CAAC;EACH;EAEA,IAAI;GAEF,OAAO,KAAK,aAAa,MADb,WAAW,MAAM,SAAS,OAAO,IAAI,IAAI,KACnB,CAAC;EACrC,SAAS,KAAK;GACZ,OAAO,KAAK,GAAG;EACjB;CACF,CACF;;CAGA,SAAS,eAAe,UAAkB,UAAqC;EAC7E,IAAI,OAAO,aAAa,aAAA,GAAYC,UAAAA,QAAAA,CAAQ,QAAQ,GAClD,OAAO,QAAQ,QAAQ,KAAK;EAE9B,OAAO,IAAI,SAAS,YAAY;GAC9B,KAAK,aAAa,UAAU,WAAW,QAAQ,QAAQ,CAAC,GAAG,CAAC;EAC9D,CAAC;CACH;;;;;;;;;;;;;CAcA,eAAe,UAAU,KAAU,MAAc,MAAuC;EACtF,MAAM,QAAQ;EAGd,IAAI,MAAM,QAAQ,GAAG,GAAG;GACtB,MAAM,CAAC,QAAQ;GACf,MAAM,gBAAgB,MAAM,MAAM,kBAAkB,MAAM,OAAO,QAAQ,EAAE,CAAC;GAC5E,IAAI,CAAC,eACH,OAAO,KAAKC,gBAAAA,WAAW,gBAAgB,2BAA2B,CAAC;GAErE,OAAO,KAAK,EAAE,KAAK,cAAc,CAAC;EACpC;EAEA,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,OAAO,KAAKA,gBAAAA,WAAW,cAAc,gCAAgC,CAAC;EAGxE,MAAM,EAAE,MAAM,aAAa;EAC3B,IAAI,CAAE,MAAM,eAAe,MAAM,QAAQ,GACvC,OAAO,KAAKA,gBAAAA,WAAW,gBAAgBF,gBAAAA,UAAU,qBAAqB,CAAC;EAGzE,IAAI,SAAS,WAAW;GACtB,MAAM,MAAM,QAAQ,IAAI;GACxB,QAAQ,KAAK,EAAE,KAAK,GAAG,gDAAgD;GACvE,OAAO,KAAK,aAAa,MAAM,KAAK,CAAC;EACvC;EAEA,IAAI,UAAU,SAAS,IAAI,MAAM,OAC/B,OAAO,KACLE,gBAAAA,WAAW,cAAc,kCAAkC,UAAU,KAAK,IAAI,GAAG,CACnF;EAGF,MAAM,EAAE,eAAe,MAAM,MAAM,eAAe,MAAM,MAAM,OAAO,KAAK,SAAS,WAAW;EAC9F,QAAQ,KAAK;GAAE;GAAM;EAAK,GAAG,0DAA0D;EACvF,OAAO,KAAK,EAAE,KAAK,WAAW,CAAC;CACjC;CAEA,MAAM,KACJN,sBAAAA,sBAAsB,cAAA,GACtBC,sBAAAA,UAAAA,CAAU,QAAQ,aAAa,GAM/B,YACA,SAAU,KAAqB,KAAe,MAA8B;EAC1E,KAAA,GAAIC,UAAAA,MAAAA,CAAM,IAAI,YAAY,IAAI,GAAG;GAC/B,IAAI,OAAOC,gBAAAA,YAAY,YAAY;GACnC,OAAO,KAAK,EACV,SAASC,gBAAAA,UAAU,eACrB,CAAC;EACH;EAEA,MAAM,EAAE,UAAU,QAAQ,IAAI;EAC9B,MAAM,EAAE,SAAS,IAAI;EAErB,KAAA,GAAIF,UAAAA,MAAAA,CAAM,QAAQ,MAAM,OAAO;GAC7B,IACEK,gBAAAA,gBAAgB,iBACd,SAAS,KACT,QAAQ,QAAQ,uBAClB,MAAM,OAGN,OAAO,KAAKD,gBAAAA,WAAW,QAAQH,gBAAAA,YAAY,cAAcC,gBAAAA,UAAU,cAAc,CAAC;GAGpF,KAAA,GAAIC,UAAAA,QAAAA,CAAQ,SAAS,GAAG,GACtB,OAAO,KAAKC,gBAAAA,WAAW,cAAc,0BAA0B,CAAC;GAGlE,KAAK,eACH,MACA,SAAS,KACT,SAAS,MACR,KAAK,cAAgC;IACpC,IAAI,QAAQ,MACV,OAAO,KAAKA,gBAAAA,WAAW,aAAa,IAAI,OAAO,CAAC;IAGlD,IAAI,WACF,OAAO,KAAK,aAAa,IAAI,YAAY,IAAI,CAAC;IAEhD,OAAO,KAAKA,gBAAAA,WAAW,iBAAiBF,gBAAAA,UAAU,qBAAqB,CAAC;GAC1E,CACF;EACF,OAAO,KAAA,GAAIF,UAAAA,MAAAA,CAAM,GAAG,MAAM,OAAO;GAC/B,IAAI,CAAC,UACH,OAAO,KACLI,gBAAAA,WAAW,QAAQH,gBAAAA,YAAY,qBAAqBK,gBAAAA,eAAe,YAAY,CACjF;GAEF,UAAU,KAAK,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI;EACvC,OACE,OAAO,KAAKF,gBAAAA,WAAW,QAAQH,gBAAAA,YAAY,gBAAgBM,gBAAAA,UAAU,aAAa,CAAC;CAEvF,CACF;AACF"}
|
package/build/v1/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","names":[],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n if (typeof value !== 'string') {\n return defaultValue;\n }\n const parsed = Number.parseInt(value, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n if (typeof query.text !== 'string' || query.text.trim().length === 0) {\n const error = errorUtils.getBadRequest(\"'text' query parameter is required\");\n res.status(error.status).json({\n error: error.message,\n code: 'ERR_TEXT_MISSING',\n });\n return;\n }\n\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n // forward only plain string parameters to the uplinks, with the clamped pagination\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (typeof value === 'string') {\n searchParams.set(key, value);\n }\n }\n searchParams.set('size', String(size));\n searchParams.set('from', String(from));\n const safeUrl = `${url.split('?')[0]}?${searchParams.toString()}`;\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url: safeUrl, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":";;;AAOA,IAAM,eAAe;AAErB,IAAM,WAAW;AAGjB,IAAM,WAAW;AAGjB,IAAM,0BAA0B;AAChC,IAAM,oBAAoB;AAE1B,SAAS,cAAc,OAAgB,cAAsB,KAAqB;CAChF,IAAI,OAAO,UAAU,UACnB,OAAO;CAET,MAAM,SAAS,OAAO,SAAS,OAAO,EAAE;CACxC,IAAI,OAAO,MAAM,MAAM,KAAK,SAAS,GACnC,OAAO;CAET,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC7B;;AAGA,SAAA,eACE,OACA,MACA,SACA,QACA,QACM;CACN,SAAS,YACP,KACA,MACA,YAC+C;EAC/C,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,KAAK,aAAa,EAAE,aAAa,KAAK,SAAS,KAAK,GAAG,YAAY,SAAU,KAAK,SAAS;IACzF,IAAI,KAAK;KACP,IAAI,IAAI,UAAU,OAAO,IAAI,MAAM,CAAC,CAAC,MAAM,SAAS,GAGlD,OAAO,QAAQ,IAAI;UAEnB,OAAO,GAAG;IAEd,OACE,OAAO,QAAQ,UAAU,MAAM,IAAI;GAEvC,CAAC;EACH,CAAC;CACH;CAEA,MAAM,
|
|
1
|
+
{"version":3,"file":"search.js","names":["SEARCH_API_ENDPOINTS","rateLimit","errorUtils","HTTP_STATUS"],"sources":["../../src/v1/search.ts"],"sourcesContent":["import type { Auth } from '@verdaccio/auth';\nimport type { searchUtils } from '@verdaccio/core';\nimport { HTTP_STATUS, errorUtils } from '@verdaccio/core';\nimport { SEARCH_API_ENDPOINTS, rateLimit } from '@verdaccio/middleware';\nimport type { Storage } from '@verdaccio/store';\nimport type { Config, Logger } from '@verdaccio/types';\n\nconst DEFAULT_SIZE = 20;\n// the public npm registry caps page size at 250 as well\nconst MAX_SIZE = 250;\n// upper bound for the pagination offset so a single request cannot force\n// an access check on an arbitrarily large slice of the catalog\nconst MAX_FROM = 10_000;\n// access checks run in batches so the scan can stop early once the\n// requested page is filled\nconst CHECK_ACCESS_BATCH_SIZE = 50;\nconst SEARCH_TIMEOUT_MS = 30_000;\n\nfunction parseQueryInt(value: unknown, defaultValue: number, max: number): number {\n if (typeof value !== 'string') {\n return defaultValue;\n }\n const parsed = Number.parseInt(value, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return defaultValue;\n }\n return Math.min(parsed, max);\n}\n\n/** Search v1 includes the response time as an ISO 8601 UTC timestamp. */\nexport default function (\n route,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger\n): void {\n function checkAccess(\n pkg: any,\n auth: any,\n remoteUser\n ): Promise<searchUtils.SearchPackageItem | null> {\n return new Promise((resolve, reject) => {\n auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {\n if (err) {\n if (err.status && String(err.status).match(/^4\\d\\d$/)) {\n // auth plugin returns 4xx user error,\n // that's equivalent of !allowed basically\n return resolve(null);\n } else {\n reject(err);\n }\n } else {\n return resolve(allowed ? pkg : null);\n }\n });\n });\n }\n\n route.get(\n SEARCH_API_ENDPOINTS.search,\n rateLimit(config?.userRateLimit),\n async (req, res, next) => {\n const { query, url } = req;\n if (typeof query.text !== 'string' || query.text.trim().length === 0) {\n const error = errorUtils.getBadRequest(\"'text' query parameter is required\");\n res.status(error.status).json({\n error: error.message,\n code: 'ERR_TEXT_MISSING',\n });\n return;\n }\n\n // `size` and `from` are attacker-controlled: clamp them so a single\n // request cannot demand unbounded work\n const size = parseQueryInt(query.size, DEFAULT_SIZE, MAX_SIZE);\n const from = parseQueryInt(query.from, 0, MAX_FROM);\n const safeQuery = { ...query, size, from };\n // forward only plain string parameters to the uplinks, with the clamped pagination\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (typeof value === 'string') {\n searchParams.set(key, value);\n }\n }\n searchParams.set('size', String(size));\n searchParams.set('from', String(from));\n const safeUrl = `${url.split('?')[0]}?${searchParams.toString()}`;\n const abort = new AbortController();\n const onClientClose = (): void => {\n if (!res.writableEnded) abort.abort(new Error('search client disconnected'));\n };\n res.on('close', onClientClose);\n const timeout = setTimeout(() => {\n abort.abort(errorUtils.getServiceUnavailable('search pagination timed out'));\n }, SEARCH_TIMEOUT_MS);\n timeout.unref();\n let onAbort: () => void;\n const cancelled = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(abort.signal.reason);\n abort.signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n const requested = from + size;\n const access = new Map<string, Promise<boolean>>();\n const collect = async (): Promise<searchUtils.SearchPackageItem[]> => {\n if (size === 0) return [];\n let allowed: searchUtils.SearchPackageItem[] = [];\n for await (const data of storage.searchPages({ query: safeQuery, url: safeUrl, abort })) {\n abort.signal.throwIfAborted();\n allowed = [];\n for (\n let i = 0;\n i < data.length && allowed.length < requested;\n i += CHECK_ACCESS_BATCH_SIZE\n ) {\n abort.signal.throwIfAborted();\n const batch = await Promise.race([\n Promise.all(\n data.slice(i, i + CHECK_ACCESS_BATCH_SIZE).map(async (item) => {\n const name = item.package.name;\n let permission = access.get(name);\n if (!permission) {\n permission = checkAccess(item, auth, req.remote_user).then(\n (result) => result !== null\n );\n access.set(name, permission);\n }\n return (await permission) ? item : null;\n })\n ),\n cancelled,\n ]);\n for (const item of batch) if (item !== null) allowed.push(item);\n }\n if (allowed.length >= requested) break;\n }\n abort.signal.throwIfAborted();\n return allowed.slice(from, requested);\n };\n const final = await Promise.race([collect(), cancelled]);\n logger.debug(`search results ${final?.length}`);\n\n const response = {\n objects: final,\n total: final.length,\n time: new Date().toISOString(),\n };\n\n res.status(HTTP_STATUS.OK).json(response);\n } catch (error) {\n if (res.destroyed) return;\n logger.error({ error }, 'search endpoint has failed @{error.message}');\n next(error);\n return;\n } finally {\n clearTimeout(timeout);\n abort.signal.removeEventListener('abort', onAbort!);\n res.off('close', onClientClose);\n abort.abort();\n }\n }\n );\n}\n"],"mappings":"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"}
|
package/build/v1/token.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.js","names":[],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { RequestHandler, Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken, isReservedTokenKey } from '@verdaccio/auth';\nimport {\n HEADERS,\n HTTP_STATUS,\n SUPPORT_ERRORS,\n cryptoUtils,\n errorUtils,\n reqUtils,\n} 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 /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\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 // the token store doubles as a per-user key-value store; rows under a\n // reserved key are internal state, not access tokens, and listing them\n // would expose their payload through `npm token ls`\n const tokens = (await storage.readTokens({ user: name })).filter(\n ({ key }) => isReservedTokenKey(key) === false\n );\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 requireOtp,\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 tokenKey = reqUtils.paramToString(req.params.tokenKey);\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n if (isReservedTokenKey(tokenKey)) {\n // this row is not a token: deleting it here would switch two-factor\n // off without the password and one-time password `npm profile\n // disable-2fa` requires\n logger.warn(\n { tokenKey, name },\n 'refused to revoke reserved key @{tokenKey} for user @{name}'\n );\n return next(errorUtils.getNotFound('token not found'));\n }\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":["TOKEN_API_ENDPOINTS","rateLimit","isNil","isReservedTokenKey","HTTP_STATUS","errorUtils","isBoolean","SUPPORT_ERRORS","cryptoUtils","getApiToken","HEADERS","reqUtils"],"sources":["../../src/v1/token.ts"],"sourcesContent":["import type { RequestHandler, Response, Router } from 'express';\nimport { isBoolean, isNil } from 'lodash-es';\n\nimport type { Auth } from '@verdaccio/auth';\nimport { getApiToken, isReservedTokenKey } from '@verdaccio/auth';\nimport {\n HEADERS,\n HTTP_STATUS,\n SUPPORT_ERRORS,\n cryptoUtils,\n errorUtils,\n reqUtils,\n} 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 /** No-op unless the caller has two-factor enabled for this operation. */\n requireOtp: RequestHandler = (_req, _res, next) => next()\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 // the token store doubles as a per-user key-value store; rows under a\n // reserved key are internal state, not access tokens, and listing them\n // would expose their payload through `npm token ls`\n const tokens = (await storage.readTokens({ user: name })).filter(\n ({ key }) => isReservedTokenKey(key) === false\n );\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 requireOtp,\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 tokenKey = reqUtils.paramToString(req.params.tokenKey);\n const { name } = req.remote_user;\n\n if (isNil(name) === false) {\n logger.debug({ name }, '@{name} has requested remove a token');\n if (isReservedTokenKey(tokenKey)) {\n // this row is not a token: deleting it here would switch two-factor\n // off without the password and one-time password `npm profile\n // disable-2fa` requires\n logger.warn(\n { tokenKey, name },\n 'refused to revoke reserved key @{tokenKey} for user @{name}'\n );\n return next(errorUtils.getNotFound('token not found'));\n }\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/build/whoami.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whoami.js","names":[],"sources":["../src/whoami.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Response, Router } from 'express';\n\nimport { errorUtils } from '@verdaccio/core';\nimport { USER_API_ENDPOINTS } from '@verdaccio/middleware';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (route: Router): void {\n route.get(\n USER_API_ENDPOINTS.whoami,\n (req: $RequestExtend, _res: Response, next: $NextFunctionVer): any => {\n // remote_user is set by the auth middleware\n const username = req?.remote_user?.name;\n if (!username) {\n debug('whoami: user not found');\n return next(errorUtils.getUnauthorized('Unauthorized'));\n }\n\n debug('whoami: response %o', username);\n return next({ username: username });\n }\n );\n}\n"],"mappings":";;;;;;AAQA,
|
|
1
|
+
{"version":3,"file":"whoami.js","names":["debug","buildDebug","USER_API_ENDPOINTS","errorUtils"],"sources":["../src/whoami.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { Response, Router } from 'express';\n\nimport { errorUtils } from '@verdaccio/core';\nimport { USER_API_ENDPOINTS } from '@verdaccio/middleware';\n\nimport type { $NextFunctionVer, $RequestExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:user');\n\nexport default function (route: Router): void {\n route.get(\n USER_API_ENDPOINTS.whoami,\n (req: $RequestExtend, _res: Response, next: $NextFunctionVer): any => {\n // remote_user is set by the auth middleware\n const username = req?.remote_user?.name;\n if (!username) {\n debug('whoami: user not found');\n return next(errorUtils.getUnauthorized('Unauthorized'));\n }\n\n debug('whoami: response %o', username);\n return next({ username: username });\n }\n );\n}\n"],"mappings":";;;;;;AAQA,IAAMA,WAAAA,GAAQC,MAAAA,QAAAA,CAAW,oBAAoB;AAE7C,SAAA,eAAyB,OAAqB;CAC5C,MAAM,IACJC,sBAAAA,mBAAmB,SAClB,KAAqB,MAAgB,SAAgC;EAEpE,MAAM,WAAW,KAAK,aAAa;EACnC,IAAI,CAAC,UAAU;GACb,QAAM,wBAAwB;GAC9B,OAAO,KAAKC,gBAAAA,WAAW,gBAAgB,cAAc,CAAC;EACxD;EAEA,QAAM,uBAAuB,QAAQ;EACrC,OAAO,KAAK,EAAY,SAAS,CAAC;CACpC,CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verdaccio/api",
|
|
3
|
-
"version": "9.0.0-next-9.
|
|
3
|
+
"version": "9.0.0-next-9.37",
|
|
4
4
|
"description": "Verdaccio Registry API",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -33,28 +33,28 @@
|
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@verdaccio/auth": "9.0.0-next-9.
|
|
37
|
-
"@verdaccio/config": "9.0.0-next-9.
|
|
38
|
-
"@verdaccio/core": "9.0.0-next-9.
|
|
39
|
-
"@verdaccio/hooks": "9.0.0-next-9.
|
|
40
|
-
"@verdaccio/logger": "9.0.0-next-9.
|
|
41
|
-
"@verdaccio/middleware": "9.0.0-next-9.
|
|
42
|
-
"@verdaccio/store": "9.0.0-next-9.
|
|
36
|
+
"@verdaccio/auth": "9.0.0-next-9.37",
|
|
37
|
+
"@verdaccio/config": "9.0.0-next-9.37",
|
|
38
|
+
"@verdaccio/core": "9.0.0-next-9.37",
|
|
39
|
+
"@verdaccio/hooks": "9.0.0-next-9.37",
|
|
40
|
+
"@verdaccio/logger": "9.0.0-next-9.37",
|
|
41
|
+
"@verdaccio/middleware": "9.0.0-next-9.37",
|
|
42
|
+
"@verdaccio/store": "9.0.0-next-9.37",
|
|
43
43
|
"debug": "4.4.3",
|
|
44
|
-
"express": "5.
|
|
44
|
+
"express": "5.3.0",
|
|
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.
|
|
50
|
-
"@verdaccio/types": "14.0.0-next-9.
|
|
49
|
+
"@verdaccio/test-helper": "5.0.0-next-9.38",
|
|
50
|
+
"@verdaccio/types": "14.0.0-next-9.17",
|
|
51
51
|
"mockdate": "3.0.5",
|
|
52
52
|
"nock": "13.5.6",
|
|
53
53
|
"otpauth": "9.5.1",
|
|
54
54
|
"rimraf": "6.1.3",
|
|
55
55
|
"supertest": "7.1.4",
|
|
56
|
-
"vite": "8.
|
|
57
|
-
"vitest": "
|
|
56
|
+
"vite": "8.3.4",
|
|
57
|
+
"vitest": "5.0.5"
|
|
58
58
|
},
|
|
59
59
|
"funding": {
|
|
60
60
|
"type": "opencollective",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"clean": "rimraf ./build",
|
|
79
|
-
"test": "vitest run",
|
|
79
|
+
"test": "vitest run --fsModuleCachePath node_modules/.vitest-cache",
|
|
80
80
|
"watch": "vite build --watch",
|
|
81
81
|
"build": "vite build"
|
|
82
82
|
}
|