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

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.
@@ -0,0 +1,261 @@
1
+ import { STAGE_API_ENDPOINTS, allow, expectJson, getRequestOptions, match, media } from "@verdaccio/middleware";
2
+ import { API_ERROR, HEADERS, HEADER_TYPE, HTTP_STATUS, UUID_PATTERN, cryptoUtils, errorUtils, reqUtils, validationUtils } from "@verdaccio/core";
3
+ import buildDebug from "debug";
4
+ import { toStagePackageVersion } from "@verdaccio/store";
5
+ import { notify } from "@verdaccio/hooks";
6
+ //#region src/stage.ts
7
+ var debug = buildDebug("verdaccio:api:stage");
8
+ var DEFAULT_PER_PAGE = 10;
9
+ var MAX_PER_PAGE = 100;
10
+ /**
11
+ * Resolve whether a user may publish a package, as a boolean.
12
+ *
13
+ * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it
14
+ * reads `req.params.package`, which is not in those URLs — the package name only
15
+ * becomes known after the staged record is loaded.
16
+ */
17
+ function canPublish(auth, packageName, user) {
18
+ return new Promise((resolve) => {
19
+ auth.allow_publish({ packageName }, user, (error, allowed) => {
20
+ resolve(!error && Boolean(allowed));
21
+ });
22
+ });
23
+ }
24
+ /**
25
+ * Parse `page`/`perPage`.
26
+ *
27
+ * `npm stage list` loops until `items.length >= total`, so a `total` that does
28
+ * not match the number of items actually reachable through pagination makes the
29
+ * CLI spin forever. Keep both derived from the same filtered array.
30
+ */
31
+ function parsePagination(query) {
32
+ const rawPage = Number.parseInt(String(query.page ?? ""), 10);
33
+ const rawPerPage = Number.parseInt(String(query.perPage ?? ""), 10);
34
+ return {
35
+ page: Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0,
36
+ perPage: Number.isFinite(rawPerPage) && rawPerPage > 0 ? Math.min(rawPerPage, MAX_PER_PAGE) : DEFAULT_PER_PAGE
37
+ };
38
+ }
39
+ /**
40
+ * Staged publish workflow (`npm stage`), behind `flags.stage`.
41
+ *
42
+ * A version is uploaded but stays invisible to installs until a maintainer
43
+ * approves it. Staging itself never requires a one-time password — deferring
44
+ * proof of presence to approval time is the whole point of the flow.
45
+ */
46
+ function stage(router, auth, storage, config, logger, requireOtp = (_req, _res, next) => next()) {
47
+ const can = allow(auth, {
48
+ beforeAll: (a, b) => logger.trace(a, b),
49
+ afterAll: (a, b) => logger.trace(a, b)
50
+ });
51
+ const stageStorage = storage.getStageStorage();
52
+ router.param("stageId", match(UUID_PATTERN));
53
+ /**
54
+ * Load a staged record and check the caller may act on it.
55
+ *
56
+ * Answers 404 rather than 403 when the caller lacks permission, so the
57
+ * endpoint does not confirm that an id exists to someone not allowed to see it.
58
+ */
59
+ async function loadRecord(req) {
60
+ const stageId = reqUtils.paramToString(req.params.stageId);
61
+ const user = req.remote_user;
62
+ if (!user?.name) throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);
63
+ const record = await stageStorage.get(stageId);
64
+ if (!record) throw errorUtils.getNotFound("no such staged package version");
65
+ if (record.actor === user.name || await canPublish(auth, record.packageName, user)) return record;
66
+ debug("user %o may not see staged item %o", user.name, stageId);
67
+ throw errorUtils.getNotFound("no such staged package version");
68
+ }
69
+ /**
70
+ * Stage a version for review.
71
+ *
72
+ * The body is the very same packument a regular publish sends: `libnpmpublish`
73
+ * builds the payload once and only swaps method and route when staging.
74
+ */
75
+ router.post(STAGE_API_ENDPOINTS.stage_package, can("stage"), media(HEADERS.JSON), expectJson, async function(req, res, next) {
76
+ const packageName = reqUtils.paramToString(req.params.package);
77
+ const username = req.remote_user?.name;
78
+ if (!username) return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));
79
+ const manifest = req.body;
80
+ if (validationUtils.validatePublishSingleVersion(manifest) === false) {
81
+ debug("invalid staging payload for %o", packageName);
82
+ return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));
83
+ }
84
+ const [version] = Object.keys(manifest.versions);
85
+ const [attachmentName] = Object.keys(manifest._attachments);
86
+ const tarball = Buffer.from(manifest._attachments[attachmentName].data, "base64");
87
+ try {
88
+ if ((await storage.getPackageLocalMetadata(packageName).catch(() => null))?.versions?.[version]) {
89
+ debug("%o@%o already published locally", packageName, version);
90
+ return next(errorUtils.getConflict());
91
+ }
92
+ const alreadyStaged = await stageStorage.findByVersion(packageName, version);
93
+ if (alreadyStaged) {
94
+ debug("%o@%o already staged as %o", packageName, version, alreadyStaged.id);
95
+ return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));
96
+ }
97
+ const abort = new AbortController();
98
+ req.on("aborted", () => abort.abort());
99
+ const record = await stageStorage.add({
100
+ packageName,
101
+ version,
102
+ tag: Object.keys(manifest["dist-tags"] ?? {})[0] ?? "latest",
103
+ actor: username,
104
+ access: manifest.access === "private" ? "private" : "public",
105
+ shasum: cryptoUtils.createTarballHash().update(tarball).digest("hex"),
106
+ tarballFilename: attachmentName.split("/").pop(),
107
+ packument: {
108
+ ...manifest,
109
+ _attachments: void 0
110
+ }
111
+ }, tarball, { signal: abort.signal });
112
+ notify(manifest, config, req.remote_user, `${packageName}@${version}`, "stage").catch((error) => {
113
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
114
+ });
115
+ res.status(HTTP_STATUS.CREATED);
116
+ return next({
117
+ message: "Package version staged successfully.",
118
+ stageId: record.id
119
+ });
120
+ } catch (err) {
121
+ return next(err);
122
+ }
123
+ });
124
+ /** List staged versions visible to the caller, newest first. */
125
+ router.get(STAGE_API_ENDPOINTS.list, async function(req, res, next) {
126
+ const user = req.remote_user;
127
+ if (!user?.name) return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));
128
+ const packageName = typeof req.query.package === "string" ? req.query.package : void 0;
129
+ const { page, perPage } = parsePagination(req.query);
130
+ try {
131
+ const candidates = await stageStorage.list({ packageName });
132
+ const permissions = /* @__PURE__ */ new Map();
133
+ const items = [];
134
+ for (const candidate of candidates) {
135
+ if (candidate.actor === user.name) {
136
+ items.push(candidate);
137
+ continue;
138
+ }
139
+ if (!permissions.has(candidate.packageName)) permissions.set(candidate.packageName, await canPublish(auth, candidate.packageName, user));
140
+ if (permissions.get(candidate.packageName)) items.push(candidate);
141
+ }
142
+ const start = page * perPage;
143
+ res.status(HTTP_STATUS.OK);
144
+ return next({
145
+ items: items.slice(start, start + perPage).map(toStagePackageVersion),
146
+ page,
147
+ perPage,
148
+ total: items.length
149
+ });
150
+ } catch (err) {
151
+ return next(err);
152
+ }
153
+ });
154
+ /** Inspect a single staged version. */
155
+ router.get(STAGE_API_ENDPOINTS.item, async function(req, res, next) {
156
+ try {
157
+ const record = await loadRecord(req);
158
+ res.status(HTTP_STATUS.OK);
159
+ return next(toStagePackageVersion(record));
160
+ } catch (err) {
161
+ return next(err);
162
+ }
163
+ });
164
+ /** Download the staged tarball so a maintainer can inspect it before approving. */
165
+ router.get(STAGE_API_ENDPOINTS.tarball, async function(req, res, next) {
166
+ const abort = new AbortController();
167
+ try {
168
+ const record = await loadRecord(req);
169
+ const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });
170
+ stream.on(HEADER_TYPE.CONTENT_LENGTH, (size) => {
171
+ res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));
172
+ });
173
+ stream.once("error", (err) => {
174
+ res.locals.report_error(err);
175
+ next(err);
176
+ });
177
+ req.on("aborted", () => abort.abort());
178
+ res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);
179
+ stream.pipe(res);
180
+ } catch (err) {
181
+ res.locals.report_error(err);
182
+ return next(err);
183
+ }
184
+ });
185
+ /**
186
+ * Approve a staged version and publish it for real.
187
+ *
188
+ * The tarball is read back and re-attached so the regular publish path runs
189
+ * untouched: upstream conflict checks, dist-tag merging, filters and hooks all
190
+ * behave exactly as they do for `npm publish`.
191
+ */
192
+ router.post(STAGE_API_ENDPOINTS.approve, requireOtp, async function(req, res, next) {
193
+ const abort = new AbortController();
194
+ req.on("aborted", () => abort.abort());
195
+ try {
196
+ const record = await loadRecord(req);
197
+ if (!await canPublish(auth, record.packageName, req.remote_user)) return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));
198
+ const packument = await stageStorage.getPackument(record.id);
199
+ if (!packument) return next(errorUtils.getNotFound("no such staged package version"));
200
+ const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });
201
+ const chunks = [];
202
+ for await (const chunk of stream) chunks.push(chunk);
203
+ const tarball = Buffer.concat(chunks);
204
+ const manifest = {
205
+ ...packument,
206
+ _attachments: { [record.tarballFilename]: {
207
+ content_type: HEADERS.OCTET_STREAM,
208
+ data: tarball.toString("base64"),
209
+ length: tarball.length
210
+ } }
211
+ };
212
+ await storage.updateManifest(manifest, {
213
+ name: record.packageName,
214
+ revision: void 0,
215
+ signal: abort.signal,
216
+ requestOptions: getRequestOptions(req),
217
+ uplinksLook: false
218
+ });
219
+ await stageStorage.remove(record.id);
220
+ notify(manifest, config, req.remote_user, `${record.packageName}@${record.version}`, "publish").catch((error) => {
221
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
222
+ });
223
+ logger.info({
224
+ packageName: record.packageName,
225
+ version: record.version,
226
+ stageId: record.id
227
+ }, "approved staged @{packageName}@@{version} (@{stageId})");
228
+ res.status(HTTP_STATUS.CREATED);
229
+ return next({ message: "Package version approved and published successfully." });
230
+ } catch (err) {
231
+ return next(err);
232
+ }
233
+ });
234
+ /** Reject a staged version, dropping the record and its tarball. */
235
+ router.delete(STAGE_API_ENDPOINTS.item, requireOtp, async function(req, res, next) {
236
+ try {
237
+ const record = await loadRecord(req);
238
+ if (!(record.actor === req.remote_user?.name) && !await canPublish(auth, record.packageName, req.remote_user)) return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));
239
+ await stageStorage.remove(record.id);
240
+ const rejected = {
241
+ name: record.packageName,
242
+ versions: { [record.version]: { version: record.version } }
243
+ };
244
+ notify(rejected, config, req.remote_user, `${record.packageName}@${record.version}`, "unstage").catch((error) => {
245
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
246
+ });
247
+ logger.info({
248
+ packageName: record.packageName,
249
+ version: record.version,
250
+ stageId: record.id
251
+ }, "rejected staged @{packageName}@@{version} (@{stageId})");
252
+ res.status(HTTP_STATUS.NO_CONTENT).end();
253
+ } catch (err) {
254
+ return next(err);
255
+ }
256
+ });
257
+ }
258
+ //#endregion
259
+ export { stage as default };
260
+
261
+ //# sourceMappingURL=stage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage.mjs","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}
package/build/user.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Router } from 'express';
1
+ import type { RequestHandler, Router } from 'express';
2
2
  import type { Auth } from '@verdaccio/auth';
3
3
  import type { Config, Logger } from '@verdaccio/types';
4
- export default function (route: Router, auth: Auth, config: Config, logger: Logger): void;
4
+ export default function (route: Router, auth: Auth, config: Config, logger: Logger,
5
+ /** No-op unless the user logging in has two-factor enabled. */
6
+ requireOtp?: RequestHandler): void;
package/build/user.js CHANGED
@@ -1,13 +1,27 @@
1
1
  const require_runtime = require("./_virtual/_rolldown/runtime.js");
2
2
  let _verdaccio_middleware = require("@verdaccio/middleware");
3
+ let _verdaccio_auth = require("@verdaccio/auth");
3
4
  let _verdaccio_core = require("@verdaccio/core");
4
5
  let debug = require("debug");
5
6
  debug = require_runtime.__toESM(debug);
6
- let _verdaccio_auth = require("@verdaccio/auth");
7
7
  let _verdaccio_config = require("@verdaccio/config");
8
8
  //#region src/user.ts
9
9
  var debug$1 = (0, debug.default)("verdaccio:api:user");
10
- function user_default(route, auth, config, logger) {
10
+ function user_default(route, auth, config, logger, requireOtp = (_req, _res, next) => next()) {
11
+ async function issueLoginToken(req, res, next, name, password, user) {
12
+ const restoredRemoteUser = (0, _verdaccio_config.createRemoteUser)(name, user?.groups || []);
13
+ const token = await (0, _verdaccio_auth.getApiToken)(auth, config, restoredRemoteUser, password);
14
+ debug$1("login: new token");
15
+ if (!token) return next(_verdaccio_core.errorUtils.getUnauthorized());
16
+ res.status(_verdaccio_core.HTTP_STATUS.CREATED);
17
+ res.set(_verdaccio_core.HEADERS.CACHE_CONTROL, _verdaccio_core.HEADERS.NO_CACHE);
18
+ const message = _verdaccio_core.authUtils.getAuthenticatedMessage(name);
19
+ debug$1("login: created user message %o", message);
20
+ return next({
21
+ ok: message,
22
+ token
23
+ });
24
+ }
11
25
  route.get(_verdaccio_middleware.USER_API_ENDPOINTS.get_user, (0, _verdaccio_middleware.rateLimit)(config?.userRateLimit), function(req, res, next) {
12
26
  debug$1("verifying user");
13
27
  if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
@@ -58,18 +72,10 @@ function user_default(route, auth, config, logger) {
58
72
  }, "authenticating for user @{username} failed. Error: @{err.message}");
59
73
  return next(_verdaccio_core.errorUtils.getCode(_verdaccio_core.HTTP_STATUS.UNAUTHORIZED, _verdaccio_core.API_ERROR.BAD_USERNAME_PASSWORD));
60
74
  }
61
- const restoredRemoteUser = (0, _verdaccio_config.createRemoteUser)(name, user?.groups || []);
62
- const token = await (0, _verdaccio_auth.getApiToken)(auth, config, restoredRemoteUser, password);
63
- debug$1("login: new token");
64
- if (!token) return next(_verdaccio_core.errorUtils.getUnauthorized());
65
- res.status(_verdaccio_core.HTTP_STATUS.CREATED);
66
- res.set(_verdaccio_core.HEADERS.CACHE_CONTROL, _verdaccio_core.HEADERS.NO_CACHE);
67
- const message = _verdaccio_core.authUtils.getAuthenticatedMessage(req.remote_user.name);
68
- debug$1("login: created user message %o", message);
69
- return next({
70
- ok: message,
71
- token
72
- });
75
+ Promise.resolve(requireOtp(req, res, (otpError) => {
76
+ if (otpError) return next(otpError);
77
+ issueLoginToken(req, res, next, name, password, user).catch(next);
78
+ })).catch(next);
73
79
  });
74
80
  } else {
75
81
  debug$1("adduser: %o", 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 { 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 (route: Router, auth: Auth, config: Config, logger: Logger): void {\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 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(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\n }\n\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(req.remote_user.name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\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":[],"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 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(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\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,11 +1,25 @@
1
1
  import { USER_API_ENDPOINTS, rateLimit } from "@verdaccio/middleware";
2
+ import { getApiToken } from "@verdaccio/auth";
2
3
  import { API_ERROR, API_MESSAGE, HEADERS, HTTP_STATUS, authUtils, cryptoUtils, errorUtils, reqUtils, validationUtils } from "@verdaccio/core";
3
4
  import buildDebug from "debug";
4
- import { getApiToken } from "@verdaccio/auth";
5
5
  import { createRemoteUser } from "@verdaccio/config";
6
6
  //#region src/user.ts
7
7
  var debug = buildDebug("verdaccio:api:user");
8
- function user_default(route, auth, config, logger) {
8
+ function user_default(route, auth, config, logger, requireOtp = (_req, _res, next) => next()) {
9
+ async function issueLoginToken(req, res, next, name, password, user) {
10
+ const restoredRemoteUser = createRemoteUser(name, user?.groups || []);
11
+ const token = await getApiToken(auth, config, restoredRemoteUser, password);
12
+ debug("login: new token");
13
+ if (!token) return next(errorUtils.getUnauthorized());
14
+ res.status(HTTP_STATUS.CREATED);
15
+ res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);
16
+ const message = authUtils.getAuthenticatedMessage(name);
17
+ debug("login: created user message %o", message);
18
+ return next({
19
+ ok: message,
20
+ token
21
+ });
22
+ }
9
23
  route.get(USER_API_ENDPOINTS.get_user, rateLimit(config?.userRateLimit), function(req, res, next) {
10
24
  debug("verifying user");
11
25
  if (!req.remote_user || typeof req.remote_user.name !== "string" || req.remote_user.name === "") {
@@ -56,18 +70,10 @@ function user_default(route, auth, config, logger) {
56
70
  }, "authenticating for user @{username} failed. Error: @{err.message}");
57
71
  return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD));
58
72
  }
59
- const restoredRemoteUser = createRemoteUser(name, user?.groups || []);
60
- const token = await getApiToken(auth, config, restoredRemoteUser, password);
61
- debug("login: new token");
62
- if (!token) return next(errorUtils.getUnauthorized());
63
- res.status(HTTP_STATUS.CREATED);
64
- res.set(HEADERS.CACHE_CONTROL, HEADERS.NO_CACHE);
65
- const message = authUtils.getAuthenticatedMessage(req.remote_user.name);
66
- debug("login: created user message %o", message);
67
- return next({
68
- ok: message,
69
- token
70
- });
73
+ Promise.resolve(requireOtp(req, res, (otpError) => {
74
+ if (otpError) return next(otpError);
75
+ issueLoginToken(req, res, next, name, password, user).catch(next);
76
+ })).catch(next);
71
77
  });
72
78
  } else {
73
79
  debug("adduser: %o", name);
@@ -1 +1 @@
1
- {"version":3,"file":"user.mjs","names":[],"sources":["../src/user.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { 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 (route: Router, auth: Auth, config: Config, logger: Logger): void {\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 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(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\n }\n\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(req.remote_user.name);\n debug('login: created user message %o', message);\n\n return next({\n ok: message,\n token,\n });\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":";;;;;;AAsBA,IAAM,QAAQ,WAAW,oBAAoB;AAE7C,SAAA,aAAyB,OAAe,MAAY,QAAgB,QAAsB;CACxF,MAAM,IACJ,mBAAmB,UACnB,UAAU,QAAQ,aAAa,GAC/B,SAAU,KAAqB,KAAe,MAA8B;EAC1E,MAAM,gBAAgB;EAEtB,IACE,CAAC,IAAI,eACL,OAAO,IAAI,YAAY,SAAS,YAChC,IAAI,YAAY,SAAS,IACzB;GACA,MAAM,oBAAoB;GAC1B,IAAI,OAAO,YAAY,EAAE;GACzB,OAAO,KAAK,EAAE,IAAI,MAAM,CAAC;EAC3B;EAGA,MAAM,WADiB,SAAS,cAAc,IAAI,OAAO,gBACxC,CAAA,EAAgB,MAAM,GAAG,CAAC,CAAC,MAAM;EAClD,MAAM,UAAU,UAAU,wBAAwB,IAAI,YAAY,IAAI;EACtE,MAAM,iCAAiC,OAAO;EAC9C,IAAI,OAAO,YAAY,EAAE;EACzB,KAAK;GAGH,MAAM;GACN,OAAO;GACP,IAAI;EACN,CAAC;CACH,CACF;;;;;;;;;;;;;;;;;;CAmBA,MAAM,IACJ,mBAAmB,UACnB,UAAU,QAAQ,aAAa,GAC/B,SAAU,KAAqB,KAAe,MAA8B;EAC1E,MAAM,EAAE,MAAM,aAAa,IAAI;EAC/B,MAAM,kBAAkB;EACxB,MAAM,aAAa,KAAK,aAAa;EAErC,MAAM,WAAW,SAAS,cAAc,IAAI,OAAO,gBAAgB;EACnE,IAAI,CAAC,gBAAgB,iBAAiB,UAAU,IAAI,GAClD,OAAO,KAAK,WAAW,cAAc,UAAU,iBAAiB,CAAC;EAGnE,IAAI,OAAO,eAAe,eAAe,OAAO,SAAS,YAAY,eAAe,MAAM;GACxF,MAAM,gCAAgC;GACtC,KAAK,aACH,MACA,UACA,eAAe,qBAAqB,KAAK,MAAqB;IAC5D,IAAI,KAAK;KACP,OAAO,MACL;MAAE;MAAM;KAAI,GACZ,mEACF;KACA,OAAO,KACL,WAAW,QAAQ,YAAY,cAAc,UAAU,qBAAqB,CAC9E;IACF;IAEA,MAAM,qBAAiC,iBAAiB,MAAM,MAAM,UAAU,CAAC,CAAC;IAChF,MAAM,QAAQ,MAAM,YAAY,MAAM,QAAQ,oBAAoB,QAAQ;IAC1E,MAAM,kBAAkB;IACxB,IAAI,CAAC,OACH,OAAO,KAAK,WAAW,gBAAgB,CAAC;IAG1C,IAAI,OAAO,YAAY,OAAO;IAC9B,IAAI,IAAI,QAAQ,eAAe,QAAQ,QAAQ;IAE/C,MAAM,UAAU,UAAU,wBAAwB,IAAI,YAAY,IAAI;IACtE,MAAM,kCAAkC,OAAO;IAE/C,OAAO,KAAK;KACV,IAAI;KACJ;IACF,CAAC;GACH,CACF;EACF,OAAO;GACL,MAAM,eAAe,IAAI;GACzB,IACE,gBAAgB,iBAAiB,UAAU,QAAQ,QAAQ,uBAAuB,MAClF,OACA;IACA,MAAM,2BAA2B;IAEjC,OAAO,KAAK,WAAW,QAAQ,YAAY,aAAa,UAAU,cAAc,CAAC;GACnF;GAEA,KAAK,SAAS,MAAM,UAAU,eAAgB,KAAK,MAAqB;IACtE,IAAI,KAAK;KACP,IAAI,IAAI,UAAU,YAAY,eAAe,IAAI,SAAS,YAAY,gBAAgB;MACpF,MAAM,+BAA+B;MAIrC,OAAO,KACL,WAAW,QAAQ,IAAI,QAAQ,IAAI,OAAO,KAAK,WAAW,YAAY,IAAI,OAAO,CACnF;KACF;KACA,OAAO,KAAK,GAAG;IACjB;IAEA,MAAM,QACJ,QAAQ,WACJ,MAAM,YAAY,MAAM,QAAQ,MAAoB,QAAQ,IAC5D,KAAA;IACN,IAAI,OACF,MAAM,yBAAyB,YAAY,KAAK,OAAiB,CAAC,CAAC;IAErE,IAAI,CAAC,OACH,OAAO,KAAK,WAAW,gBAAgB,CAAC;IAG1C,IAAI,cAAc;IAClB,IAAI,OAAO,YAAY,OAAO;IAC9B,IAAI,IAAI,QAAQ,eAAe,QAAQ,QAAQ;IAC/C,MAAM,gCAAgC;IACtC,OAAO,KAAK;KACV,IAAI,SAAS,IAAI,KAAK,KAAK;KAC3B;IACF,CAAC;GACH,CAAC;EACH;CACF,CACF;CAEA,MAAM,OACJ,mBAAmB,YACnB,SAAU,KAAqB,KAAe,MAA8B;EAC1E,IAAI,OAAO,YAAY,EAAE;EACzB,KAAK,EACH,IAAI,YAAY,WAClB,CAAC;CACH,CACF;AACF"}
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 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(\n errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.BAD_USERNAME_PASSWORD)\n );\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":";;;;;;AAsBA,IAAM,QAAQ,WAAW,oBAAoB;AAE7C,SAAA,aACE,OACA,MACA,QACA,QAEA,cAA8B,MAAM,MAAM,SAAS,KAAK,GAClD;CACN,eAAe,gBACb,KACA,KACA,MACA,MACA,UACA,MACe;EACf,MAAM,qBAAiC,iBAAiB,MAAM,MAAM,UAAU,CAAC,CAAC;EAChF,MAAM,QAAQ,MAAM,YAAY,MAAM,QAAQ,oBAAoB,QAAQ;EAC1E,MAAM,kBAAkB;EACxB,IAAI,CAAC,OACH,OAAO,KAAK,WAAW,gBAAgB,CAAC;EAG1C,IAAI,OAAO,YAAY,OAAO;EAC9B,IAAI,IAAI,QAAQ,eAAe,QAAQ,QAAQ;EAE/C,MAAM,UAAU,UAAU,wBAAwB,IAAI;EACtD,MAAM,kCAAkC,OAAO;EAE/C,OAAO,KAAK;GACV,IAAI;GACJ;EACF,CAAC;CACH;CAEA,MAAM,IACJ,mBAAmB,UACnB,UAAU,QAAQ,aAAa,GAC/B,SAAU,KAAqB,KAAe,MAA8B;EAC1E,MAAM,gBAAgB;EAEtB,IACE,CAAC,IAAI,eACL,OAAO,IAAI,YAAY,SAAS,YAChC,IAAI,YAAY,SAAS,IACzB;GACA,MAAM,oBAAoB;GAC1B,IAAI,OAAO,YAAY,EAAE;GACzB,OAAO,KAAK,EAAE,IAAI,MAAM,CAAC;EAC3B;EAGA,MAAM,WADiB,SAAS,cAAc,IAAI,OAAO,gBACxC,CAAA,EAAgB,MAAM,GAAG,CAAC,CAAC,MAAM;EAClD,MAAM,UAAU,UAAU,wBAAwB,IAAI,YAAY,IAAI;EACtE,MAAM,iCAAiC,OAAO;EAC9C,IAAI,OAAO,YAAY,EAAE;EACzB,KAAK;GAGH,MAAM;GACN,OAAO;GACP,IAAI;EACN,CAAC;CACH,CACF;;;;;;;;;;;;;;;;;;CAmBA,MAAM,IACJ,mBAAmB,UACnB,UAAU,QAAQ,aAAa,GAC/B,SAAU,KAAqB,KAAe,MAA8B;EAC1E,MAAM,EAAE,MAAM,aAAa,IAAI;EAC/B,MAAM,kBAAkB;EACxB,MAAM,aAAa,KAAK,aAAa;EAErC,MAAM,WAAW,SAAS,cAAc,IAAI,OAAO,gBAAgB;EACnE,IAAI,CAAC,gBAAgB,iBAAiB,UAAU,IAAI,GAClD,OAAO,KAAK,WAAW,cAAc,UAAU,iBAAiB,CAAC;EAGnE,IAAI,OAAO,eAAe,eAAe,OAAO,SAAS,YAAY,eAAe,MAAM;GACxF,MAAM,gCAAgC;GACtC,KAAK,aACH,MACA,UACA,eAAe,qBAAqB,KAAK,MAAqB;IAC5D,IAAI,KAAK;KACP,OAAO,MACL;MAAE;MAAM;KAAI,GACZ,mEACF;KACA,OAAO,KACL,WAAW,QAAQ,YAAY,cAAc,UAAU,qBAAqB,CAC9E;IACF;IAEA,QAAQ,QACN,WAAW,KAAK,MAAM,aAAmB;KACvC,IAAI,UACF,OAAO,KAAK,QAAQ;KAEtB,gBAAgB,KAAK,KAAK,MAAM,MAAM,UAAU,IAAkB,CAAC,CAAC,MAAM,IAAI;IAChF,CAAC,CACH,CAAC,CAAC,MAAM,IAAI;GACd,CACF;EACF,OAAO;GACL,MAAM,eAAe,IAAI;GACzB,IACE,gBAAgB,iBAAiB,UAAU,QAAQ,QAAQ,uBAAuB,MAClF,OACA;IACA,MAAM,2BAA2B;IAEjC,OAAO,KAAK,WAAW,QAAQ,YAAY,aAAa,UAAU,cAAc,CAAC;GACnF;GAEA,KAAK,SAAS,MAAM,UAAU,eAAgB,KAAK,MAAqB;IACtE,IAAI,KAAK;KACP,IAAI,IAAI,UAAU,YAAY,eAAe,IAAI,SAAS,YAAY,gBAAgB;MACpF,MAAM,+BAA+B;MAIrC,OAAO,KACL,WAAW,QAAQ,IAAI,QAAQ,IAAI,OAAO,KAAK,WAAW,YAAY,IAAI,OAAO,CACnF;KACF;KACA,OAAO,KAAK,GAAG;IACjB;IAEA,MAAM,QACJ,QAAQ,WACJ,MAAM,YAAY,MAAM,QAAQ,MAAoB,QAAQ,IAC5D,KAAA;IACN,IAAI,OACF,MAAM,yBAAyB,YAAY,KAAK,OAAiB,CAAC,CAAC;IAErE,IAAI,CAAC,OACH,OAAO,KAAK,WAAW,gBAAgB,CAAC;IAG1C,IAAI,cAAc;IAClB,IAAI,OAAO,YAAY,OAAO;IAC9B,IAAI,IAAI,QAAQ,eAAe,QAAQ,QAAQ;IAC/C,MAAM,gCAAgC;IACtC,OAAO,KAAK;KACV,IAAI,SAAS,IAAI,KAAK,KAAK;KAC3B;IACF,CAAC;GACH,CAAC;EACH;CACF,CACF;CAEA,MAAM,OACJ,mBAAmB,YACnB,SAAU,KAAqB,KAAe,MAA8B;EAC1E,IAAI,OAAO,YAAY,EAAE;EACzB,KAAK,EACH,IAAI,YAAY,WAClB,CAAC;CACH,CACF;AACF"}
package/build/v1/login.js CHANGED
@@ -1,9 +1,9 @@
1
1
  const require_runtime = require("../_virtual/_rolldown/runtime.js");
2
2
  let _verdaccio_middleware = require("@verdaccio/middleware");
3
+ let _verdaccio_auth = require("@verdaccio/auth");
3
4
  let _verdaccio_core = require("@verdaccio/core");
4
5
  let debug = require("debug");
5
6
  debug = require_runtime.__toESM(debug);
6
- let _verdaccio_auth = require("@verdaccio/auth");
7
7
  let _verdaccio_config = require("@verdaccio/config");
8
8
  let node_crypto = require("node:crypto");
9
9
  //#region src/v1/login.ts
@@ -1,7 +1,7 @@
1
1
  import { LOGIN_API_ENDPOINTS, rateLimit } from "@verdaccio/middleware";
2
+ import { getApiToken } from "@verdaccio/auth";
2
3
  import { API_ERROR, HEADERS, HTTP_STATUS, authUtils, errorUtils, reqUtils } from "@verdaccio/core";
3
4
  import buildDebug from "debug";
4
- import { getApiToken } from "@verdaccio/auth";
5
5
  import { createRemoteUser } from "@verdaccio/config";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/v1/login.ts
@@ -1,8 +1,13 @@
1
- import type { Router } from 'express';
2
- import type { Auth } from '@verdaccio/auth';
3
- import type { Config } from '@verdaccio/types';
1
+ import type { RequestHandler, Router } from 'express';
2
+ import type { Auth, TfaStatus } from '@verdaccio/auth';
3
+ import type { Storage } from '@verdaccio/store';
4
+ import type { Config, Logger } from '@verdaccio/types';
4
5
  export interface Profile {
5
- tfa: boolean;
6
+ /**
7
+ * `false` when two-factor is off, otherwise the mode and whether enrolment is
8
+ * still half-finished. `npm profile` reads `tfa.mode` and `tfa.pending`.
9
+ */
10
+ tfa: TfaStatus;
6
11
  name: string;
7
12
  email: string;
8
13
  email_verified: boolean;
@@ -11,4 +16,6 @@ export interface Profile {
11
16
  cidr_whitelist: string[] | null;
12
17
  fullname: string;
13
18
  }
14
- export default function (route: Router, auth: Auth, config: Config): void;
19
+ export default function (route: Router, auth: Auth, config: Config, storage?: Storage, logger?: Logger,
20
+ /** No-op unless the caller already has two-factor enabled. */
21
+ requireOtp?: RequestHandler): void;