@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,92 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.js");
2
+ let _verdaccio_core = require("@verdaccio/core");
3
+ let debug = require("debug");
4
+ debug = require_runtime.__toESM(debug);
5
+ //#region src/require-otp.ts
6
+ var debug$1 = (0, debug.default)("verdaccio:api:otp");
7
+ /**
8
+ * Header carrying the one-time password.
9
+ *
10
+ * npm sets it from `opts.otp` (`npm-registry-fetch/lib/index.js`) and Yarn from
11
+ * `getOtpHeaders` (`plugin-npm/sources/npmHttpUtils.ts`); both spell it the same.
12
+ */
13
+ var OTP_HEADER = "npm-otp";
14
+ /**
15
+ * Wording kept close to npmjs on purpose: npm has a fallback that matches
16
+ * `/one-time pass/` against the body when the header is missing, so a client
17
+ * that somehow loses the header still recognises the challenge.
18
+ */
19
+ var OTP_REQUIRED_MESSAGE = "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.";
20
+ function defaultGetUsername(req) {
21
+ const name = req.remote_user?.name;
22
+ return typeof name === "string" && name.length > 0 ? name : void 0;
23
+ }
24
+ /** Answer 401 in the exact shape npm and Yarn recognise as "send me an OTP". */
25
+ function challenge(res, next) {
26
+ res.header(_verdaccio_core.HEADERS.WWW_AUTH, "otp");
27
+ res.header("npm-notice", "Provide a one-time password from your authenticator app.");
28
+ next(_verdaccio_core.errorUtils.getCode(_verdaccio_core.HTTP_STATUS.UNAUTHORIZED, OTP_REQUIRED_MESSAGE));
29
+ }
30
+ /**
31
+ * Demand a one-time password from users who have two-factor enabled.
32
+ *
33
+ * A no-op when the flag is off, when the user has no two-factor configured, or
34
+ * when their mode does not cover this operation, so requests from everybody
35
+ * else are untouched.
36
+ */
37
+ function requireOtp(options) {
38
+ const { tfaStore, scope, logger } = options;
39
+ const getUsername = options.getUsername ?? defaultGetUsername;
40
+ return async function(req, res, next) {
41
+ if (!tfaStore) return next();
42
+ const username = getUsername(req);
43
+ if (!username) return next();
44
+ let record;
45
+ try {
46
+ record = await tfaStore.get(username);
47
+ } catch (err) {
48
+ return next(err);
49
+ }
50
+ if (!record || record.pending) return next();
51
+ if (scope === "write" && record.mode === "auth-only") {
52
+ debug$1("%o is in auth-only mode, writes do not need an OTP", username);
53
+ return next();
54
+ }
55
+ const otp = req.get(OTP_HEADER);
56
+ if (!otp) {
57
+ debug$1("challenging %o for an OTP", username);
58
+ return challenge(res, next);
59
+ }
60
+ if (await tfaStore.verify(username, otp)) {
61
+ debug$1("OTP accepted for %o", username);
62
+ return next();
63
+ }
64
+ logger.warn({ username }, "rejected an invalid one-time password for @{username}");
65
+ return challenge(res, next);
66
+ };
67
+ }
68
+ /**
69
+ * Fail fast when two-factor is enabled on a storage plugin that cannot hold it.
70
+ *
71
+ * `Storage.readTokens` rejects with 503 when the plugin lacks the token
72
+ * interface. Without this check that rejection would surface on every single
73
+ * write, for every user, including those who never enabled two-factor.
74
+ */
75
+ function assertTokenStoreSupport(storage, logger) {
76
+ const plugin = storage?.localStorage?.getStoragePlugin?.();
77
+ const missing = [
78
+ "saveToken",
79
+ "readTokens",
80
+ "deleteToken"
81
+ ].filter((method) => typeof plugin?.[method] !== "function");
82
+ if (missing.length > 0) {
83
+ logger.error({ missing: missing.join(", ") }, "the storage plugin does not implement @{missing}, which two-factor authentication needs");
84
+ throw new Error(`flags.tfa is enabled but the storage plugin does not implement ${missing.join(", ")}`);
85
+ }
86
+ }
87
+ //#endregion
88
+ exports.OTP_HEADER = OTP_HEADER;
89
+ exports.assertTokenStoreSupport = assertTokenStoreSupport;
90
+ exports.requireOtp = requireOtp;
91
+
92
+ //# sourceMappingURL=require-otp.js.map
@@ -0,0 +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,IAAM,WAAA,GAAQ,MAAA,QAAA,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,OAAO,gBAAA,QAAQ,UAAA,KAAuB;CAC1C,IAAI,OAAO,cAAc,0DAA0D;CACnF,KAAK,gBAAA,WAAW,QAAQ,gBAAA,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"}
@@ -0,0 +1,88 @@
1
+ import { HEADERS, HTTP_STATUS, errorUtils } from "@verdaccio/core";
2
+ import buildDebug from "debug";
3
+ //#region src/require-otp.ts
4
+ var debug = buildDebug("verdaccio:api:otp");
5
+ /**
6
+ * Header carrying the one-time password.
7
+ *
8
+ * npm sets it from `opts.otp` (`npm-registry-fetch/lib/index.js`) and Yarn from
9
+ * `getOtpHeaders` (`plugin-npm/sources/npmHttpUtils.ts`); both spell it the same.
10
+ */
11
+ var OTP_HEADER = "npm-otp";
12
+ /**
13
+ * Wording kept close to npmjs on purpose: npm has a fallback that matches
14
+ * `/one-time pass/` against the body when the header is missing, so a client
15
+ * that somehow loses the header still recognises the challenge.
16
+ */
17
+ var OTP_REQUIRED_MESSAGE = "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.";
18
+ function defaultGetUsername(req) {
19
+ const name = req.remote_user?.name;
20
+ return typeof name === "string" && name.length > 0 ? name : void 0;
21
+ }
22
+ /** Answer 401 in the exact shape npm and Yarn recognise as "send me an OTP". */
23
+ function challenge(res, next) {
24
+ res.header(HEADERS.WWW_AUTH, "otp");
25
+ res.header("npm-notice", "Provide a one-time password from your authenticator app.");
26
+ next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, OTP_REQUIRED_MESSAGE));
27
+ }
28
+ /**
29
+ * Demand a one-time password from users who have two-factor enabled.
30
+ *
31
+ * A no-op when the flag is off, when the user has no two-factor configured, or
32
+ * when their mode does not cover this operation, so requests from everybody
33
+ * else are untouched.
34
+ */
35
+ function requireOtp(options) {
36
+ const { tfaStore, scope, logger } = options;
37
+ const getUsername = options.getUsername ?? defaultGetUsername;
38
+ return async function(req, res, next) {
39
+ if (!tfaStore) return next();
40
+ const username = getUsername(req);
41
+ if (!username) return next();
42
+ let record;
43
+ try {
44
+ record = await tfaStore.get(username);
45
+ } catch (err) {
46
+ return next(err);
47
+ }
48
+ if (!record || record.pending) return next();
49
+ if (scope === "write" && record.mode === "auth-only") {
50
+ debug("%o is in auth-only mode, writes do not need an OTP", username);
51
+ return next();
52
+ }
53
+ const otp = req.get(OTP_HEADER);
54
+ if (!otp) {
55
+ debug("challenging %o for an OTP", username);
56
+ return challenge(res, next);
57
+ }
58
+ if (await tfaStore.verify(username, otp)) {
59
+ debug("OTP accepted for %o", username);
60
+ return next();
61
+ }
62
+ logger.warn({ username }, "rejected an invalid one-time password for @{username}");
63
+ return challenge(res, next);
64
+ };
65
+ }
66
+ /**
67
+ * Fail fast when two-factor is enabled on a storage plugin that cannot hold it.
68
+ *
69
+ * `Storage.readTokens` rejects with 503 when the plugin lacks the token
70
+ * interface. Without this check that rejection would surface on every single
71
+ * write, for every user, including those who never enabled two-factor.
72
+ */
73
+ function assertTokenStoreSupport(storage, logger) {
74
+ const plugin = storage?.localStorage?.getStoragePlugin?.();
75
+ const missing = [
76
+ "saveToken",
77
+ "readTokens",
78
+ "deleteToken"
79
+ ].filter((method) => typeof plugin?.[method] !== "function");
80
+ if (missing.length > 0) {
81
+ logger.error({ missing: missing.join(", ") }, "the storage plugin does not implement @{missing}, which two-factor authentication needs");
82
+ throw new Error(`flags.tfa is enabled but the storage plugin does not implement ${missing.join(", ")}`);
83
+ }
84
+ }
85
+ //#endregion
86
+ export { OTP_HEADER, assertTokenStoreSupport, requireOtp };
87
+
88
+ //# sourceMappingURL=require-otp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-otp.mjs","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,IAAM,QAAQ,WAAW,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,OAAO,QAAQ,UAAA,KAAuB;CAC1C,IAAI,OAAO,cAAc,0DAA0D;CACnF,KAAK,WAAW,QAAQ,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,MAAM,sDAAsD,QAAQ;GACpE,OAAO,KAAK;EACd;EAEA,MAAM,MAAM,IAAI,IAAI,UAAU;EAC9B,IAAI,CAAC,KAAK;GACR,MAAM,6BAA6B,QAAQ;GAC3C,OAAO,UAAU,KAAK,IAAI;EAC5B;EAEA,IAAI,MAAM,SAAS,OAAO,UAAU,GAAG,GAAG;GACxC,MAAM,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"}
@@ -0,0 +1,17 @@
1
+ import type { RequestHandler, Router } from 'express';
2
+ import type { Auth } from '@verdaccio/auth';
3
+ import type { Storage } from '@verdaccio/store';
4
+ import type { Config, Logger } from '@verdaccio/types';
5
+ /**
6
+ * Staged publish workflow (`npm stage`), behind `flags.stage`.
7
+ *
8
+ * A version is uploaded but stays invisible to installs until a maintainer
9
+ * approves it. Staging itself never requires a one-time password — deferring
10
+ * proof of presence to approval time is the whole point of the flow.
11
+ */
12
+ export default function stage(router: Router, auth: Auth, storage: Storage, config: Config, logger: Logger,
13
+ /**
14
+ * Guards approval, never staging: publishing without a one-time password and
15
+ * proving presence at approval time is the point of the whole flow.
16
+ */
17
+ requireOtp?: RequestHandler): void;
package/build/stage.js ADDED
@@ -0,0 +1,263 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.js");
2
+ let _verdaccio_middleware = require("@verdaccio/middleware");
3
+ let _verdaccio_core = require("@verdaccio/core");
4
+ let debug = require("debug");
5
+ debug = require_runtime.__toESM(debug);
6
+ let _verdaccio_store = require("@verdaccio/store");
7
+ let _verdaccio_hooks = require("@verdaccio/hooks");
8
+ //#region src/stage.ts
9
+ var debug$1 = (0, debug.default)("verdaccio:api:stage");
10
+ var DEFAULT_PER_PAGE = 10;
11
+ var MAX_PER_PAGE = 100;
12
+ /**
13
+ * Resolve whether a user may publish a package, as a boolean.
14
+ *
15
+ * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it
16
+ * reads `req.params.package`, which is not in those URLs — the package name only
17
+ * becomes known after the staged record is loaded.
18
+ */
19
+ function canPublish(auth, packageName, user) {
20
+ return new Promise((resolve) => {
21
+ auth.allow_publish({ packageName }, user, (error, allowed) => {
22
+ resolve(!error && Boolean(allowed));
23
+ });
24
+ });
25
+ }
26
+ /**
27
+ * Parse `page`/`perPage`.
28
+ *
29
+ * `npm stage list` loops until `items.length >= total`, so a `total` that does
30
+ * not match the number of items actually reachable through pagination makes the
31
+ * CLI spin forever. Keep both derived from the same filtered array.
32
+ */
33
+ function parsePagination(query) {
34
+ const rawPage = Number.parseInt(String(query.page ?? ""), 10);
35
+ const rawPerPage = Number.parseInt(String(query.perPage ?? ""), 10);
36
+ return {
37
+ page: Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0,
38
+ perPage: Number.isFinite(rawPerPage) && rawPerPage > 0 ? Math.min(rawPerPage, MAX_PER_PAGE) : DEFAULT_PER_PAGE
39
+ };
40
+ }
41
+ /**
42
+ * Staged publish workflow (`npm stage`), behind `flags.stage`.
43
+ *
44
+ * A version is uploaded but stays invisible to installs until a maintainer
45
+ * approves it. Staging itself never requires a one-time password — deferring
46
+ * proof of presence to approval time is the whole point of the flow.
47
+ */
48
+ function stage(router, auth, storage, config, logger, requireOtp = (_req, _res, next) => next()) {
49
+ const can = (0, _verdaccio_middleware.allow)(auth, {
50
+ beforeAll: (a, b) => logger.trace(a, b),
51
+ afterAll: (a, b) => logger.trace(a, b)
52
+ });
53
+ const stageStorage = storage.getStageStorage();
54
+ router.param("stageId", (0, _verdaccio_middleware.match)(_verdaccio_core.UUID_PATTERN));
55
+ /**
56
+ * Load a staged record and check the caller may act on it.
57
+ *
58
+ * Answers 404 rather than 403 when the caller lacks permission, so the
59
+ * endpoint does not confirm that an id exists to someone not allowed to see it.
60
+ */
61
+ async function loadRecord(req) {
62
+ const stageId = _verdaccio_core.reqUtils.paramToString(req.params.stageId);
63
+ const user = req.remote_user;
64
+ if (!user?.name) throw _verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.MUST_BE_LOGGED);
65
+ const record = await stageStorage.get(stageId);
66
+ if (!record) throw _verdaccio_core.errorUtils.getNotFound("no such staged package version");
67
+ if (record.actor === user.name || await canPublish(auth, record.packageName, user)) return record;
68
+ debug$1("user %o may not see staged item %o", user.name, stageId);
69
+ throw _verdaccio_core.errorUtils.getNotFound("no such staged package version");
70
+ }
71
+ /**
72
+ * Stage a version for review.
73
+ *
74
+ * The body is the very same packument a regular publish sends: `libnpmpublish`
75
+ * builds the payload once and only swaps method and route when staging.
76
+ */
77
+ router.post(_verdaccio_middleware.STAGE_API_ENDPOINTS.stage_package, can("stage"), (0, _verdaccio_middleware.media)(_verdaccio_core.HEADERS.JSON), _verdaccio_middleware.expectJson, async function(req, res, next) {
78
+ const packageName = _verdaccio_core.reqUtils.paramToString(req.params.package);
79
+ const username = req.remote_user?.name;
80
+ if (!username) return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.MUST_BE_LOGGED));
81
+ const manifest = req.body;
82
+ if (_verdaccio_core.validationUtils.validatePublishSingleVersion(manifest) === false) {
83
+ debug$1("invalid staging payload for %o", packageName);
84
+ return next(_verdaccio_core.errorUtils.getBadRequest(_verdaccio_core.API_ERROR.UNSUPORTED_REGISTRY_CALL));
85
+ }
86
+ const [version] = Object.keys(manifest.versions);
87
+ const [attachmentName] = Object.keys(manifest._attachments);
88
+ const tarball = Buffer.from(manifest._attachments[attachmentName].data, "base64");
89
+ try {
90
+ if ((await storage.getPackageLocalMetadata(packageName).catch(() => null))?.versions?.[version]) {
91
+ debug$1("%o@%o already published locally", packageName, version);
92
+ return next(_verdaccio_core.errorUtils.getConflict());
93
+ }
94
+ const alreadyStaged = await stageStorage.findByVersion(packageName, version);
95
+ if (alreadyStaged) {
96
+ debug$1("%o@%o already staged as %o", packageName, version, alreadyStaged.id);
97
+ return next(_verdaccio_core.errorUtils.getConflict(`${packageName}@${version} is already staged`));
98
+ }
99
+ const abort = new AbortController();
100
+ req.on("aborted", () => abort.abort());
101
+ const record = await stageStorage.add({
102
+ packageName,
103
+ version,
104
+ tag: Object.keys(manifest["dist-tags"] ?? {})[0] ?? "latest",
105
+ actor: username,
106
+ access: manifest.access === "private" ? "private" : "public",
107
+ shasum: _verdaccio_core.cryptoUtils.createTarballHash().update(tarball).digest("hex"),
108
+ tarballFilename: attachmentName.split("/").pop(),
109
+ packument: {
110
+ ...manifest,
111
+ _attachments: void 0
112
+ }
113
+ }, tarball, { signal: abort.signal });
114
+ (0, _verdaccio_hooks.notify)(manifest, config, req.remote_user, `${packageName}@${version}`, "stage").catch((error) => {
115
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
116
+ });
117
+ res.status(_verdaccio_core.HTTP_STATUS.CREATED);
118
+ return next({
119
+ message: "Package version staged successfully.",
120
+ stageId: record.id
121
+ });
122
+ } catch (err) {
123
+ return next(err);
124
+ }
125
+ });
126
+ /** List staged versions visible to the caller, newest first. */
127
+ router.get(_verdaccio_middleware.STAGE_API_ENDPOINTS.list, async function(req, res, next) {
128
+ const user = req.remote_user;
129
+ if (!user?.name) return next(_verdaccio_core.errorUtils.getUnauthorized(_verdaccio_core.API_ERROR.MUST_BE_LOGGED));
130
+ const packageName = typeof req.query.package === "string" ? req.query.package : void 0;
131
+ const { page, perPage } = parsePagination(req.query);
132
+ try {
133
+ const candidates = await stageStorage.list({ packageName });
134
+ const permissions = /* @__PURE__ */ new Map();
135
+ const items = [];
136
+ for (const candidate of candidates) {
137
+ if (candidate.actor === user.name) {
138
+ items.push(candidate);
139
+ continue;
140
+ }
141
+ if (!permissions.has(candidate.packageName)) permissions.set(candidate.packageName, await canPublish(auth, candidate.packageName, user));
142
+ if (permissions.get(candidate.packageName)) items.push(candidate);
143
+ }
144
+ const start = page * perPage;
145
+ res.status(_verdaccio_core.HTTP_STATUS.OK);
146
+ return next({
147
+ items: items.slice(start, start + perPage).map(_verdaccio_store.toStagePackageVersion),
148
+ page,
149
+ perPage,
150
+ total: items.length
151
+ });
152
+ } catch (err) {
153
+ return next(err);
154
+ }
155
+ });
156
+ /** Inspect a single staged version. */
157
+ router.get(_verdaccio_middleware.STAGE_API_ENDPOINTS.item, async function(req, res, next) {
158
+ try {
159
+ const record = await loadRecord(req);
160
+ res.status(_verdaccio_core.HTTP_STATUS.OK);
161
+ return next((0, _verdaccio_store.toStagePackageVersion)(record));
162
+ } catch (err) {
163
+ return next(err);
164
+ }
165
+ });
166
+ /** Download the staged tarball so a maintainer can inspect it before approving. */
167
+ router.get(_verdaccio_middleware.STAGE_API_ENDPOINTS.tarball, async function(req, res, next) {
168
+ const abort = new AbortController();
169
+ try {
170
+ const record = await loadRecord(req);
171
+ const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });
172
+ stream.on(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, (size) => {
173
+ res.header(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, String(size));
174
+ });
175
+ stream.once("error", (err) => {
176
+ res.locals.report_error(err);
177
+ next(err);
178
+ });
179
+ req.on("aborted", () => abort.abort());
180
+ res.header(_verdaccio_core.HEADERS.CONTENT_TYPE, _verdaccio_core.HEADERS.OCTET_STREAM);
181
+ stream.pipe(res);
182
+ } catch (err) {
183
+ res.locals.report_error(err);
184
+ return next(err);
185
+ }
186
+ });
187
+ /**
188
+ * Approve a staged version and publish it for real.
189
+ *
190
+ * The tarball is read back and re-attached so the regular publish path runs
191
+ * untouched: upstream conflict checks, dist-tag merging, filters and hooks all
192
+ * behave exactly as they do for `npm publish`.
193
+ */
194
+ router.post(_verdaccio_middleware.STAGE_API_ENDPOINTS.approve, requireOtp, async function(req, res, next) {
195
+ const abort = new AbortController();
196
+ req.on("aborted", () => abort.abort());
197
+ try {
198
+ const record = await loadRecord(req);
199
+ if (!await canPublish(auth, record.packageName, req.remote_user)) return next(_verdaccio_core.errorUtils.getForbidden(_verdaccio_core.API_ERROR.UNAUTHORIZED_ACCESS));
200
+ const packument = await stageStorage.getPackument(record.id);
201
+ if (!packument) return next(_verdaccio_core.errorUtils.getNotFound("no such staged package version"));
202
+ const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });
203
+ const chunks = [];
204
+ for await (const chunk of stream) chunks.push(chunk);
205
+ const tarball = Buffer.concat(chunks);
206
+ const manifest = {
207
+ ...packument,
208
+ _attachments: { [record.tarballFilename]: {
209
+ content_type: _verdaccio_core.HEADERS.OCTET_STREAM,
210
+ data: tarball.toString("base64"),
211
+ length: tarball.length
212
+ } }
213
+ };
214
+ await storage.updateManifest(manifest, {
215
+ name: record.packageName,
216
+ revision: void 0,
217
+ signal: abort.signal,
218
+ requestOptions: (0, _verdaccio_middleware.getRequestOptions)(req),
219
+ uplinksLook: false
220
+ });
221
+ await stageStorage.remove(record.id);
222
+ (0, _verdaccio_hooks.notify)(manifest, config, req.remote_user, `${record.packageName}@${record.version}`, "publish").catch((error) => {
223
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
224
+ });
225
+ logger.info({
226
+ packageName: record.packageName,
227
+ version: record.version,
228
+ stageId: record.id
229
+ }, "approved staged @{packageName}@@{version} (@{stageId})");
230
+ res.status(_verdaccio_core.HTTP_STATUS.CREATED);
231
+ return next({ message: "Package version approved and published successfully." });
232
+ } catch (err) {
233
+ return next(err);
234
+ }
235
+ });
236
+ /** Reject a staged version, dropping the record and its tarball. */
237
+ router.delete(_verdaccio_middleware.STAGE_API_ENDPOINTS.item, requireOtp, async function(req, res, next) {
238
+ try {
239
+ const record = await loadRecord(req);
240
+ if (!(record.actor === req.remote_user?.name) && !await canPublish(auth, record.packageName, req.remote_user)) return next(_verdaccio_core.errorUtils.getForbidden(_verdaccio_core.API_ERROR.UNAUTHORIZED_ACCESS));
241
+ await stageStorage.remove(record.id);
242
+ const rejected = {
243
+ name: record.packageName,
244
+ versions: { [record.version]: { version: record.version } }
245
+ };
246
+ (0, _verdaccio_hooks.notify)(rejected, config, req.remote_user, `${record.packageName}@${record.version}`, "unstage").catch((error) => {
247
+ logger.error({ error: error?.message }, "notify batch service has failed: @{error}");
248
+ });
249
+ logger.info({
250
+ packageName: record.packageName,
251
+ version: record.version,
252
+ stageId: record.id
253
+ }, "rejected staged @{packageName}@@{version} (@{stageId})");
254
+ res.status(_verdaccio_core.HTTP_STATUS.NO_CONTENT).end();
255
+ } catch (err) {
256
+ return next(err);
257
+ }
258
+ });
259
+ }
260
+ //#endregion
261
+ exports.default = stage;
262
+
263
+ //# sourceMappingURL=stage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport type { RequestHandler, Router } from 'express';\n\nimport type { Auth } from '@verdaccio/auth';\nimport {\n API_ERROR,\n HEADERS,\n HEADER_TYPE,\n HTTP_STATUS,\n UUID_PATTERN,\n cryptoUtils,\n errorUtils,\n reqUtils,\n validationUtils,\n} from '@verdaccio/core';\nimport { notify } from '@verdaccio/hooks';\nimport {\n STAGE_API_ENDPOINTS,\n allow,\n expectJson,\n getRequestOptions,\n match,\n media,\n} from '@verdaccio/middleware';\nimport type { StageRecord, Storage } from '@verdaccio/store';\nimport { toStagePackageVersion } from '@verdaccio/store';\nimport type { Config, Logger, Manifest, RemoteUser } from '@verdaccio/types';\n\nimport type { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';\n\nconst debug = buildDebug('verdaccio:api:stage');\n\nconst DEFAULT_PER_PAGE = 10;\nconst MAX_PER_PAGE = 100;\n\n/**\n * Resolve whether a user may publish a package, as a boolean.\n *\n * The `allow` middleware cannot be used on the `/-/stage/:stageId` routes: it\n * reads `req.params.package`, which is not in those URLs — the package name only\n * becomes known after the staged record is loaded.\n */\nfunction canPublish(auth: Auth, packageName: string, user: RemoteUser): Promise<boolean> {\n return new Promise((resolve) => {\n auth.allow_publish({ packageName }, user, (error, allowed): void => {\n resolve(!error && Boolean(allowed));\n });\n });\n}\n\n/**\n * Parse `page`/`perPage`.\n *\n * `npm stage list` loops until `items.length >= total`, so a `total` that does\n * not match the number of items actually reachable through pagination makes the\n * CLI spin forever. Keep both derived from the same filtered array.\n */\nfunction parsePagination(query: any): { page: number; perPage: number } {\n const rawPage = Number.parseInt(String(query.page ?? ''), 10);\n const rawPerPage = Number.parseInt(String(query.perPage ?? ''), 10);\n const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 0;\n const perPage =\n Number.isFinite(rawPerPage) && rawPerPage > 0\n ? Math.min(rawPerPage, MAX_PER_PAGE)\n : DEFAULT_PER_PAGE;\n\n return { page, perPage };\n}\n\n/**\n * Staged publish workflow (`npm stage`), behind `flags.stage`.\n *\n * A version is uploaded but stays invisible to installs until a maintainer\n * approves it. Staging itself never requires a one-time password — deferring\n * proof of presence to approval time is the whole point of the flow.\n */\nexport default function stage(\n router: Router,\n auth: Auth,\n storage: Storage,\n config: Config,\n logger: Logger,\n /**\n * Guards approval, never staging: publishing without a one-time password and\n * proving presence at approval time is the point of the whole flow.\n */\n requireOtp: RequestHandler = (_req, _res, next) => next()\n): void {\n const can = allow(auth, {\n beforeAll: (a, b) => logger.trace(a, b),\n afterAll: (a, b) => logger.trace(a, b),\n });\n const stageStorage = storage.getStageStorage();\n\n router.param('stageId', match(UUID_PATTERN));\n\n /**\n * Load a staged record and check the caller may act on it.\n *\n * Answers 404 rather than 403 when the caller lacks permission, so the\n * endpoint does not confirm that an id exists to someone not allowed to see it.\n */\n async function loadRecord(req: $RequestExtend): Promise<StageRecord> {\n const stageId = reqUtils.paramToString(req.params.stageId);\n const user = req.remote_user;\n if (!user?.name) {\n throw errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED);\n }\n\n const record = await stageStorage.get(stageId);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n if (record.actor === user.name || (await canPublish(auth, record.packageName, user))) {\n return record;\n }\n debug('user %o may not see staged item %o', user.name, stageId);\n throw errorUtils.getNotFound('no such staged package version');\n }\n\n /**\n * Stage a version for review.\n *\n * The body is the very same packument a regular publish sends: `libnpmpublish`\n * builds the payload once and only swaps method and route when staging.\n */\n router.post(\n STAGE_API_ENDPOINTS.stage_package,\n // `stage`, not `publish`: granting it to a group without publish rights is\n // what makes review a real gate rather than a convention\n can('stage'),\n media(HEADERS.JSON),\n expectJson,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const packageName = reqUtils.paramToString(req.params.package);\n const username = req.remote_user?.name;\n\n if (!username) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const manifest: Manifest = req.body;\n if (validationUtils.validatePublishSingleVersion(manifest) === false) {\n debug('invalid staging payload for %o', packageName);\n return next(errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));\n }\n\n const [version] = Object.keys(manifest.versions);\n const [attachmentName] = Object.keys(manifest._attachments);\n const tarball = Buffer.from(manifest._attachments[attachmentName].data as string, 'base64');\n\n try {\n const localManifest = await storage.getPackageLocalMetadata(packageName).catch(() => null);\n if (localManifest?.versions?.[version]) {\n debug('%o@%o already published locally', packageName, version);\n return next(errorUtils.getConflict());\n }\n\n const alreadyStaged = await stageStorage.findByVersion(packageName, version);\n if (alreadyStaged) {\n debug('%o@%o already staged as %o', packageName, version, alreadyStaged.id);\n return next(errorUtils.getConflict(`${packageName}@${version} is already staged`));\n }\n\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n\n const record = await stageStorage.add(\n {\n packageName,\n version,\n tag: Object.keys(manifest['dist-tags'] ?? {})[0] ?? 'latest',\n actor: username,\n access: manifest.access === 'private' ? 'private' : 'public',\n shasum: cryptoUtils.createTarballHash().update(tarball).digest('hex'),\n tarballFilename: attachmentName.split('/').pop() as string,\n // the tarball lives next to the record, not inlined as base64\n packument: { ...manifest, _attachments: undefined } as any,\n },\n tarball,\n { signal: abort.signal }\n );\n\n // a staged version needs a maintainer to act on it, so it is worth\n // announcing even though nothing became installable\n void notify(manifest, config, req.remote_user, `${packageName}@${version}`, 'stage').catch(\n (error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n }\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({\n message: 'Package version staged successfully.',\n stageId: record.id,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** List staged versions visible to the caller, newest first. */\n router.get(\n STAGE_API_ENDPOINTS.list,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const user = req.remote_user;\n if (!user?.name) {\n return next(errorUtils.getUnauthorized(API_ERROR.MUST_BE_LOGGED));\n }\n\n const packageName = typeof req.query.package === 'string' ? req.query.package : undefined;\n const { page, perPage } = parsePagination(req.query);\n\n try {\n const candidates = await stageStorage.list({ packageName });\n // one permission lookup per distinct package, not per item\n const permissions = new Map<string, boolean>();\n const items: StageRecord[] = [];\n for (const candidate of candidates) {\n if (candidate.actor === user.name) {\n items.push(candidate);\n continue;\n }\n if (!permissions.has(candidate.packageName)) {\n permissions.set(\n candidate.packageName,\n await canPublish(auth, candidate.packageName, user)\n );\n }\n if (permissions.get(candidate.packageName)) {\n items.push(candidate);\n }\n }\n\n const start = page * perPage;\n res.status(HTTP_STATUS.OK);\n return next({\n items: items.slice(start, start + perPage).map(toStagePackageVersion),\n page,\n perPage,\n // the count the CLI paginates against: after filtering, before slicing\n total: items.length,\n });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Inspect a single staged version. */\n router.get(\n STAGE_API_ENDPOINTS.item,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n res.status(HTTP_STATUS.OK);\n return next(toStagePackageVersion(record));\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Download the staged tarball so a maintainer can inspect it before approving. */\n router.get(\n STAGE_API_ENDPOINTS.tarball,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n try {\n const record = await loadRecord(req);\n const stream: any = await stageStorage.readTarball(record.id, { signal: abort.signal });\n\n stream.on(HEADER_TYPE.CONTENT_LENGTH, (size: number) => {\n res.header(HEADER_TYPE.CONTENT_LENGTH, String(size));\n });\n stream.once('error', (err: any) => {\n res.locals.report_error(err);\n next(err);\n });\n req.on('aborted', () => abort.abort());\n\n res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);\n stream.pipe(res);\n } catch (err: any) {\n res.locals.report_error(err);\n return next(err);\n }\n }\n );\n\n /**\n * Approve a staged version and publish it for real.\n *\n * The tarball is read back and re-attached so the regular publish path runs\n * untouched: upstream conflict checks, dist-tag merging, filters and hooks all\n * behave exactly as they do for `npm publish`.\n */\n router.post(\n STAGE_API_ENDPOINTS.approve,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n const abort = new AbortController();\n req.on('aborted', () => abort.abort());\n try {\n const record = await loadRecord(req);\n if (!(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n const packument = await stageStorage.getPackument(record.id);\n if (!packument) {\n return next(errorUtils.getNotFound('no such staged package version'));\n }\n\n const stream = await stageStorage.readTarball(record.id, { signal: abort.signal });\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk as Buffer);\n }\n const tarball = Buffer.concat(chunks);\n\n const manifest = {\n ...packument,\n _attachments: {\n [record.tarballFilename]: {\n content_type: HEADERS.OCTET_STREAM,\n data: tarball.toString('base64'),\n length: tarball.length,\n },\n },\n } as Manifest;\n\n await storage.updateManifest(manifest, {\n name: record.packageName,\n revision: undefined,\n signal: abort.signal,\n requestOptions: getRequestOptions(req),\n uplinksLook: false,\n });\n\n // the staged copy is only dropped once the version is really published\n await stageStorage.remove(record.id);\n\n void notify(\n manifest,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'publish'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'approved staged @{packageName}@@{version} (@{stageId})'\n );\n\n res.status(HTTP_STATUS.CREATED);\n return next({ message: 'Package version approved and published successfully.' });\n } catch (err) {\n return next(err);\n }\n }\n );\n\n /** Reject a staged version, dropping the record and its tarball. */\n router.delete(\n STAGE_API_ENDPOINTS.item,\n requireOtp,\n async function (\n req: $RequestExtend,\n res: $ResponseExtend,\n next: $NextFunctionVer\n ): Promise<void> {\n try {\n const record = await loadRecord(req);\n // whoever staged it may always withdraw it; anyone else needs to be\n // able to publish the package\n const isOwnSubmission = record.actor === req.remote_user?.name;\n if (!isOwnSubmission && !(await canPublish(auth, record.packageName, req.remote_user))) {\n return next(errorUtils.getForbidden(API_ERROR.UNAUTHORIZED_ACCESS));\n }\n\n await stageStorage.remove(record.id);\n\n const rejected: Partial<Manifest> = {\n name: record.packageName,\n versions: { [record.version]: { version: record.version } } as any,\n };\n void notify(\n rejected,\n config,\n req.remote_user,\n `${record.packageName}@${record.version}`,\n 'unstage'\n ).catch((error: any) => {\n logger.error({ error: error?.message }, 'notify batch service has failed: @{error}');\n });\n\n logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'rejected staged @{packageName}@@{version} (@{stageId})'\n );\n\n // `npm stage reject` sends `ignoreBody: true`; answering through `next()`\n // would make the final middleware serialize a body onto a 204.\n res.status(HTTP_STATUS.NO_CONTENT).end();\n } catch (err) {\n return next(err);\n }\n }\n );\n}\n"],"mappings":"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"}