@verdaccio/proxy 8.0.0-next-8.12 → 8.0.0-next-8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/proxy.ts DELETED
@@ -1,650 +0,0 @@
1
- import JSONStream from 'JSONStream';
2
- import buildDebug from 'debug';
3
- import got, {
4
- Agents,
5
- Delays,
6
- Options,
7
- RequestError,
8
- RetryOptions,
9
- Headers as gotHeaders,
10
- } from 'got-cjs';
11
- import _ from 'lodash';
12
- import Stream, { PassThrough, Readable } from 'stream';
13
- import { URL } from 'url';
14
-
15
- import {
16
- API_ERROR,
17
- HEADERS,
18
- HTTP_STATUS,
19
- TOKEN_BASIC,
20
- TOKEN_BEARER,
21
- constants,
22
- errorUtils,
23
- searchUtils,
24
- } from '@verdaccio/core';
25
- import { AgentOptionsConf, Config, Logger, Manifest, UpLinkConf } from '@verdaccio/types';
26
- import { buildToken } from '@verdaccio/utils';
27
-
28
- import CustomAgents from './agent';
29
- import { parseInterval } from './proxy-utils';
30
-
31
- const debug = buildDebug('verdaccio:proxy');
32
-
33
- const encode = function (thing): string {
34
- return encodeURIComponent(thing).replace(/^%40/, '@');
35
- };
36
-
37
- const jsonContentType = HEADERS.JSON;
38
- const contentTypeAccept = `${jsonContentType};`;
39
-
40
- /**
41
- * Just a helper (`config[key] || default` doesn't work because of zeroes)
42
- */
43
- const setConfig = (config: UpLinkConfLocal, key: string, def): string => {
44
- return _.isNil(config[key]) === false ? config[key] : def;
45
- };
46
-
47
- export type UpLinkConfLocal = UpLinkConf & {
48
- no_proxy?: string;
49
- };
50
-
51
- export interface ProxyList {
52
- [key: string]: IProxy;
53
- }
54
-
55
- export type ProxySearchParams = {
56
- url: string;
57
- abort: AbortController;
58
- query?: searchUtils.SearchQuery;
59
- headers?: Headers;
60
- retry?: Partial<RetryOptions>;
61
- };
62
- export interface IProxy {
63
- config: UpLinkConfLocal;
64
- failed_requests: number;
65
- userAgent: string;
66
- ca?: string | void;
67
- logger: Logger;
68
- server_id: string;
69
- url: URL;
70
- maxage: number;
71
- timeout: Delays;
72
- max_fails: number;
73
- fail_timeout: number;
74
- upname: string;
75
- search(options: ProxySearchParams): Promise<Stream.Readable>;
76
- getRemoteMetadata(
77
- name: string,
78
- options: Partial<ISyncUplinksOptions>
79
- ): Promise<[Manifest, string]>;
80
- fetchTarball(
81
- url: string,
82
- options: Partial<Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>>
83
- ): PassThrough;
84
- }
85
-
86
- // this type is need it by storage
87
- export { Options as FetchOptions };
88
-
89
- export interface ISyncUplinksOptions extends Options {
90
- uplinksLook?: boolean;
91
- etag?: string;
92
- remoteAddress?: string;
93
- }
94
-
95
- /**
96
- * Implements Storage interface
97
- * (same for storage.js, local-storage.js, up-storage.js)
98
- */
99
- class ProxyStorage implements IProxy {
100
- public config: UpLinkConfLocal;
101
- public failed_requests: number;
102
- public userAgent: string;
103
- public ca: string | void;
104
- public logger: Logger;
105
- public server_id: string;
106
- public url: URL;
107
- public maxage: number;
108
- public timeout: Delays;
109
- public max_fails: number;
110
- public fail_timeout: number;
111
- public agent_options: AgentOptionsConf;
112
- // FIXME: upname is assigned to each instance
113
- // @ts-ignore
114
- public upname: string;
115
- public proxy: string | undefined;
116
- private agent: Agents;
117
- // @ts-ignore
118
- public last_request_time: number | null;
119
- public strict_ssl: boolean;
120
- private retry: Partial<RetryOptions>;
121
-
122
- public constructor(config: UpLinkConfLocal, mainConfig: Config, logger: Logger, agent?: Agents) {
123
- this.config = config;
124
- this.failed_requests = 0;
125
- this.userAgent = mainConfig.user_agent ?? 'hidden';
126
- this.ca = config.ca;
127
- this.logger = logger;
128
- this.server_id = mainConfig.server_id;
129
- this.agent_options = setConfig(this.config, 'agent_options', {
130
- keepAlive: true,
131
- maxSockets: 40,
132
- maxFreeSockets: 10,
133
- }) as AgentOptionsConf;
134
- this.url = new URL(this.config.url);
135
- const isHTTPS = this.url.protocol === 'https:';
136
- this._setupProxy(this.url.hostname, config, mainConfig, isHTTPS);
137
- this.agent = agent ?? this.getAgent();
138
- this.config.url = this.config.url.replace(/\/$/, '');
139
-
140
- if (this.config.timeout && Number(this.config.timeout) >= 1000) {
141
- this.logger.warn(
142
- [
143
- 'Too big timeout value: ' + this.config.timeout,
144
- 'We changed time format to nginx-like one',
145
- '(see http://nginx.org/en/docs/syntax.html)',
146
- 'so please update your config accordingly',
147
- ].join('\n')
148
- );
149
- }
150
-
151
- // a bunch of different configurable timers
152
- this.maxage = parseInterval(setConfig(this.config, 'maxage', '2m'));
153
- // https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md
154
- this.timeout = {
155
- request: parseInterval(setConfig(this.config, 'timeout', '30s')),
156
- };
157
- debug('set timeout %s', this.timeout);
158
- this.max_fails = Number(setConfig(this.config, 'max_fails', this.config.max_fails ?? 2));
159
- this.fail_timeout = parseInterval(setConfig(this.config, 'fail_timeout', '5m'));
160
- this.strict_ssl = Boolean(setConfig(this.config, 'strict_ssl', true));
161
- this.retry = { limit: this.max_fails ?? 2 };
162
- }
163
-
164
- private getAgent() {
165
- if (!this.agent) {
166
- // TODO: the config.ca (certificates) is not yet injected here
167
- const agentInstance = new CustomAgents(this.config.url, this.proxy, this.agent_options);
168
- return agentInstance.get();
169
- } else {
170
- return this.agent;
171
- }
172
- }
173
-
174
- public getHeaders(headers = {}): gotHeaders {
175
- const accept = HEADERS.ACCEPT;
176
- const acceptEncoding = HEADERS.ACCEPT_ENCODING;
177
- const userAgent = HEADERS.USER_AGENT;
178
-
179
- headers[accept] = headers[accept] || contentTypeAccept;
180
- headers[acceptEncoding] = headers[acceptEncoding] || 'gzip';
181
- // registry.npmjs.org will only return search result if user-agent include string 'npm'
182
- headers[userAgent] = headers[userAgent] || `npm (${this.userAgent})`;
183
- return this.setAuthNext(headers);
184
- }
185
-
186
- /**
187
- * Validate configuration auth and assign Header authorization
188
- * @param {Object} headers
189
- * @return {Object}
190
- * @private
191
- */
192
- private setAuthNext(headers: gotHeaders): gotHeaders {
193
- const { auth } = this.config;
194
- if (typeof auth === 'undefined' || typeof headers[HEADERS.AUTHORIZATION] === 'string') {
195
- return headers;
196
- }
197
-
198
- if (_.isObject(auth) === false && _.isObject((auth as any).token) === false) {
199
- this._throwErrorAuth('Auth invalid');
200
- }
201
-
202
- // get NPM_TOKEN http://blog.npmjs.org/post/118393368555/deploying-with-npm-private-modules
203
- // or get other variable export in env
204
- // https://github.com/verdaccio/verdaccio/releases/tag/v2.5.0
205
- let token: any;
206
- const tokenConf: any = auth;
207
- if (_.isNil(tokenConf.token) === false && _.isString(tokenConf.token)) {
208
- token = tokenConf.token;
209
- } else if (_.isNil(tokenConf.token_env) === false) {
210
- if (typeof tokenConf.token_env === 'string') {
211
- token = process.env[tokenConf.token_env];
212
- } else if (typeof tokenConf.token_env === 'boolean' && tokenConf.token_env) {
213
- token = process.env.NPM_TOKEN;
214
- } else {
215
- this.logger.error(constants.ERROR_CODE.token_required);
216
- this._throwErrorAuth(constants.ERROR_CODE.token_required);
217
- }
218
- } else {
219
- token = process.env.NPM_TOKEN;
220
- }
221
-
222
- if (typeof token === 'undefined') {
223
- this._throwErrorAuth(constants.ERROR_CODE.token_required);
224
- }
225
-
226
- // define type Auth allow basic and bearer
227
- const type = tokenConf.type || TOKEN_BASIC;
228
- this._setHeaderAuthorization(headers, type, token);
229
-
230
- return headers;
231
- }
232
-
233
- /**
234
- * @param {string} message
235
- * @throws {Error}
236
- * @private
237
- */
238
- private _throwErrorAuth(message: string): Error {
239
- this.logger.error(message);
240
- throw new Error(message);
241
- }
242
-
243
- /**
244
- * Assign Header authorization with type authentication
245
- * @param {Object} headers
246
- * @param {string} type
247
- * @param {string} token
248
- * @private
249
- */
250
- private _setHeaderAuthorization(headers: any, type: string, token: any): void {
251
- const _type: string = type.toLowerCase();
252
-
253
- if (_type !== TOKEN_BEARER.toLowerCase() && _type !== TOKEN_BASIC.toLowerCase()) {
254
- this._throwErrorAuth(`Auth type '${_type}' not allowed`);
255
- }
256
-
257
- type = _.upperFirst(type);
258
- headers[HEADERS.AUTHORIZATION] = buildToken(type, token);
259
- }
260
-
261
- /**
262
- * It will add or override specified headers from config file.
263
- *
264
- * Eg:
265
- *
266
- * uplinks:
267
- npmjs:
268
- url: https://registry.npmjs.org/
269
- headers:
270
- Accept: "application/vnd.npm.install-v2+json; q=1.0"
271
- verdaccio-staging:
272
- url: https://mycompany.com/npm
273
- headers:
274
- Accept: "application/json"
275
- authorization: "Basic YourBase64EncodedCredentials=="
276
-
277
- * @param {Object} headers
278
- * @private
279
- * @deprecated use applyUplinkHeaders
280
- */
281
- private _overrideWithUpLinkConfLocaligHeaders(headers: Headers): any {
282
- if (!this.config.headers) {
283
- return headers;
284
- }
285
-
286
- // add/override headers specified in the config
287
- /* eslint guard-for-in: 0 */
288
- for (const key in this.config.headers) {
289
- headers[key] = this.config.headers[key];
290
- }
291
- }
292
-
293
- private applyUplinkHeaders(headers: gotHeaders): gotHeaders {
294
- if (!this.config.headers) {
295
- return headers;
296
- }
297
-
298
- // add/override headers specified in the config
299
- /* eslint guard-for-in: 0 */
300
- for (const key in this.config.headers) {
301
- headers[key] = this.config.headers[key];
302
- }
303
- return headers;
304
- }
305
-
306
- public async getRemoteMetadata(
307
- name: string,
308
- options: Partial<ISyncUplinksOptions>
309
- ): Promise<[Manifest, string]> {
310
- if (this._ifRequestFailure()) {
311
- throw errorUtils.getInternalError(API_ERROR.UPLINK_OFFLINE);
312
- }
313
-
314
- // FUTURE: allow mix headers that comes from the client
315
- debug('getting metadata for package %s', name);
316
- let headers = this.getHeaders(options?.headers);
317
- headers = this.addProxyHeaders(headers, options.remoteAddress);
318
- headers = this.applyUplinkHeaders(headers);
319
- // the following headers cannot be overwritten
320
- if (_.isNil(options.etag) === false) {
321
- headers[HEADERS.NONE_MATCH] = options.etag;
322
- headers[HEADERS.ACCEPT] = contentTypeAccept;
323
- }
324
- const method = options.method || 'GET';
325
- const uri = this.config.url + `/${encode(name)}`;
326
- debug('set retry limit is %s', this.retry.limit);
327
- let response;
328
- let responseLength = 0;
329
- try {
330
- const retry = options?.retry ?? this.retry;
331
- debug('retry initial count %s', retry);
332
- response = await got(uri, {
333
- headers,
334
- responseType: 'json',
335
- method,
336
- agent: this.agent,
337
- retry,
338
- timeout: this.timeout,
339
- hooks: {
340
- afterResponse: [
341
- (afterResponse) => {
342
- const code = afterResponse.statusCode;
343
- debug('after response code is %s', code);
344
- if (code >= HTTP_STATUS.OK && code < HTTP_STATUS.MULTIPLE_CHOICES) {
345
- if (this.failed_requests >= this.max_fails) {
346
- this.failed_requests = 0;
347
- this.logger.warn(
348
- {
349
- host: this.url.host,
350
- },
351
- 'host @{host} is now online'
352
- );
353
- }
354
- }
355
-
356
- return afterResponse;
357
- },
358
- ],
359
- beforeRetry: [
360
- (error: RequestError, count: number) => {
361
- debug('retry %s count: %s', uri, count);
362
- this.failed_requests = count ?? 0;
363
- this.logger.info(
364
- {
365
- request: {
366
- method: method,
367
- url: uri,
368
- },
369
- error: error.message,
370
- retryCount: this.failed_requests,
371
- },
372
- "retry @{retryCount} req: '@{request.method} @{request.url}'"
373
- );
374
- if (this.failed_requests >= this.max_fails) {
375
- this.logger.warn(
376
- {
377
- host: this.url.host,
378
- },
379
- 'host @{host} is now offline'
380
- );
381
- }
382
- },
383
- ],
384
- },
385
- })
386
- .on('request', () => {
387
- this.last_request_time = Date.now();
388
- })
389
- .on<any>('response', (eventResponse) => {
390
- const message = "@{!status}, req: '@{request.method} @{request.url}' (streaming)";
391
- this.logger.http(
392
- {
393
- request: {
394
- method: method,
395
- url: uri,
396
- },
397
- status: _.isNull(eventResponse) === false ? eventResponse.statusCode : 'ERR',
398
- },
399
- message
400
- );
401
- })
402
- .on('downloadProgress', (progress) => {
403
- if (progress.total) {
404
- debug('responseLength %s', progress.total);
405
- responseLength = progress.total;
406
- }
407
- });
408
- const etag = response.headers.etag as string;
409
- const data = response.body;
410
-
411
- // not modified status (304) registry does not return any payload
412
- // it is handled as an error
413
- if (response?.statusCode === HTTP_STATUS.NOT_MODIFIED) {
414
- throw errorUtils.getCode(HTTP_STATUS.NOT_MODIFIED, API_ERROR.NOT_MODIFIED_NO_DATA);
415
- }
416
-
417
- debug('uri %s success', uri);
418
- const message = "@{!status}, req: '@{request.method} @{request.url}'";
419
- this.logger.http(
420
- {
421
- // if error is null/false change this to undefined so it wont log
422
- request: { method: method, url: uri },
423
- status: response.statusCode,
424
- bytes: {
425
- in: options?.json ? JSON.stringify(options?.json).length : 0,
426
- out: responseLength || 0,
427
- },
428
- },
429
- message
430
- );
431
- return [data, etag];
432
- } catch (err: any) {
433
- debug('error %s on uri %s', err.code, uri);
434
- if (err.code === 'ERR_NON_2XX_3XX_RESPONSE') {
435
- const code = err.response.statusCode;
436
- debug('error code %s', code);
437
- if (code === HTTP_STATUS.NOT_FOUND) {
438
- throw errorUtils.getNotFound(errorUtils.API_ERROR.NOT_PACKAGE_UPLINK);
439
- }
440
-
441
- if (!(code >= HTTP_STATUS.OK && code < HTTP_STATUS.MULTIPLE_CHOICES)) {
442
- const error = errorUtils.getInternalError(
443
- `${errorUtils.API_ERROR.BAD_STATUS_CODE}: ${code}`
444
- );
445
- // we need this code to identify outside which status code triggered the error
446
- error.remoteStatus = code;
447
- throw error;
448
- }
449
- } else if (err.code === 'ETIMEDOUT') {
450
- debug('error code timeout');
451
- const code = err.code;
452
- const error = errorUtils.getInternalError(
453
- `${errorUtils.API_ERROR.SERVER_TIME_OUT}: ${code}`
454
- );
455
- // we need this code to identify outside which status code triggered the error
456
- error.remoteStatus = code;
457
- throw error;
458
- }
459
- throw err;
460
- }
461
- }
462
-
463
- // FIXME: handle stream and retry
464
- public fetchTarball(
465
- url: string,
466
- overrideOptions: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
467
- ): any {
468
- debug('fetching url for %s', url);
469
- const options = { ...this.config, ...overrideOptions };
470
- let headers = this.getHeaders(options?.headers);
471
- headers = this.addProxyHeaders(headers, options.remoteAddress);
472
- headers = this.applyUplinkHeaders(headers);
473
- // the following headers cannot be overwritten
474
- if (_.isNil(options.etag) === false) {
475
- headers[HEADERS.NONE_MATCH] = options.etag;
476
- headers[HEADERS.ACCEPT] = contentTypeAccept;
477
- }
478
- const method = 'GET';
479
- // const uri = this.config.url + `/${encode(name)}`;
480
- debug('request uri for %s', url);
481
-
482
- const readStream = got
483
- .stream(url, {
484
- headers,
485
- method,
486
- agent: this.agent,
487
- // FIXME: this should be taken from construtor as priority
488
- retry: this.retry ?? options?.retry,
489
- timeout: this.timeout,
490
- })
491
- .on('request', () => {
492
- this.last_request_time = Date.now();
493
- });
494
-
495
- return readStream;
496
- }
497
-
498
- /**
499
- * Perform a stream search.
500
- * @param {*} options request options
501
- * @return {Stream}
502
- */
503
- public async search({ url, abort, retry }: ProxySearchParams): Promise<Stream.Readable> {
504
- try {
505
- // Incoming URL is relative ie /-/v1/search...
506
- const uri = new URL(url, this.url).href;
507
- this.logger.http({ uri, uplink: this.upname }, 'search request to uplink @{uplink} - @{uri}');
508
- debug('searching on %o', uri);
509
- const response = got(uri, {
510
- signal: abort ? abort.signal : {},
511
- agent: this.agent,
512
- timeout: this.timeout,
513
- retry: retry ?? this.retry,
514
- });
515
-
516
- const res = await response.text();
517
- const total = JSON.parse(res).total;
518
- debug('number of packages found: %o', total);
519
- const streamSearch = new PassThrough({ objectMode: true });
520
- const streamResponse = Readable.from(res);
521
- // objects is one of the properties on the body, it ignores date and total
522
- streamResponse.pipe(JSONStream.parse('objects')).pipe(streamSearch, { end: true });
523
- return streamSearch;
524
- } catch (err: any) {
525
- debug('search error %s', err);
526
- if (err.response.statusCode === 409) {
527
- throw errorUtils.getInternalError(`bad status code ${err.response.statusCode} from uplink`);
528
- }
529
- this.logger.error(
530
- { errorMessage: err?.message, name: this.upname },
531
- 'proxy uplink @{name} search error: @{errorMessage}'
532
- );
533
- throw err;
534
- }
535
- }
536
-
537
- private addProxyHeaders(headers: gotHeaders, remoteAddress?: string): gotHeaders {
538
- // Only submit X-Forwarded-For field if we don't have a proxy selected
539
- // in the config file.
540
- //
541
- // Otherwise misconfigured proxy could return 407
542
- if (!this.proxy) {
543
- headers[HEADERS.FORWARDED_FOR] =
544
- (headers['x-forwarded-for'] ? headers['x-forwarded-for'] + ', ' : '') + remoteAddress;
545
- }
546
-
547
- // always attach Via header to avoid loops, even if we're not proxying
548
- headers['via'] = headers['via'] ? headers['via'] + ', ' : '';
549
- headers['via'] += '1.1 ' + this.server_id + ' (Verdaccio)';
550
-
551
- return headers;
552
- }
553
-
554
- /**
555
- * If the request failure.
556
- * @return {boolean}
557
- * @private
558
- */
559
- private _ifRequestFailure(): boolean {
560
- return (
561
- this.failed_requests >= this.max_fails &&
562
- Math.abs(Date.now() - (this.last_request_time as number)) < this.fail_timeout
563
- );
564
- }
565
-
566
- /**
567
- * Set up a proxy.
568
- * @param {*} hostname
569
- * @param {*} config
570
- * @param {*} mainconfig
571
- * @param {*} isHTTPS
572
- */
573
- private _setupProxy(
574
- hostname: string,
575
- config: UpLinkConfLocal,
576
- mainconfig: Config,
577
- isHTTPS: boolean
578
- ): void {
579
- let noProxyList;
580
- const proxy_key: string = isHTTPS ? 'https_proxy' : 'http_proxy';
581
- debug('looking for %o', proxy_key);
582
-
583
- // get http_proxy and no_proxy configs
584
- if (proxy_key in config) {
585
- this.proxy = config[proxy_key];
586
- debug('found %o in uplink config', this.proxy);
587
- } else if (proxy_key in mainconfig) {
588
- this.proxy = mainconfig[proxy_key];
589
- debug('found %o in main config', this.proxy);
590
- }
591
- if ('no_proxy' in config) {
592
- noProxyList = config.no_proxy;
593
- } else if ('no_proxy' in mainconfig) {
594
- noProxyList = mainconfig.no_proxy;
595
- }
596
-
597
- // use wget-like algorithm to determine if proxy shouldn't be used
598
- if (hostname[0] !== '.') {
599
- hostname = '.' + hostname;
600
- }
601
-
602
- if (_.isString(noProxyList) && noProxyList.length) {
603
- noProxyList = noProxyList.split(',');
604
- }
605
-
606
- if (_.isArray(noProxyList)) {
607
- for (let i = 0; i < noProxyList.length; i++) {
608
- let noProxyItem = noProxyList[i];
609
- if (noProxyItem[0] !== '.') {
610
- noProxyItem = '.' + noProxyItem;
611
- }
612
- if (hostname.endsWith(noProxyItem)) {
613
- if (this.proxy) {
614
- debug('not using proxy, excluded by @{rule}', noProxyItem);
615
- this.logger.debug(
616
- { url: this.url.href, rule: noProxyItem },
617
- 'not using proxy for @{url}, excluded by @{rule} rule'
618
- );
619
- this.proxy = undefined;
620
- }
621
- break;
622
- }
623
- }
624
- }
625
-
626
- // validate proxy protocol matches the proxy_key type
627
- if (this.proxy) {
628
- const proxyUrl = new URL(this.proxy);
629
- const expectedProtocol = isHTTPS ? 'https:' : 'http:';
630
- if (proxyUrl.protocol !== expectedProtocol) {
631
- this.logger.error(
632
- { proxy: this.proxy, expectedProtocol },
633
- `invalid protocol for ${proxy_key} - must be ${expectedProtocol}`
634
- );
635
- this.proxy = undefined;
636
- return;
637
- }
638
- }
639
-
640
- if (typeof this.proxy === 'string') {
641
- debug('using proxy @{proxy} for @{url}', this.proxy, this.url.href);
642
- this.logger.debug(
643
- { url: this.url.href, proxy: this.proxy },
644
- 'using proxy @{proxy} for @{url}'
645
- );
646
- }
647
- }
648
- }
649
-
650
- export { ProxyStorage };
@@ -1,42 +0,0 @@
1
- import { Config, Logger, Manifest } from '@verdaccio/types';
2
-
3
- import { IProxy, ProxyStorage } from './index';
4
-
5
- export interface ProxyInstanceList {
6
- [key: string]: IProxy;
7
- }
8
-
9
- /**
10
- * Set up uplinks for each proxy configuration.
11
- */
12
- export function setupUpLinks(config: Config, logger: Logger): ProxyInstanceList {
13
- const uplinks: ProxyInstanceList = {};
14
-
15
- for (const uplinkName in config.uplinks) {
16
- if (Object.prototype.hasOwnProperty.call(config.uplinks, uplinkName)) {
17
- // instance for each up-link definition
18
- const proxy: IProxy = new ProxyStorage(config.uplinks[uplinkName], config, logger);
19
- // TODO: review this can be inside ProxyStorage
20
- proxy.upname = uplinkName;
21
-
22
- uplinks[uplinkName] = proxy;
23
- }
24
- }
25
-
26
- return uplinks;
27
- }
28
-
29
- export function updateVersionsHiddenUpLinkNext(manifest: Manifest, upLink: IProxy): Manifest {
30
- const { versions } = manifest;
31
- const versionsList = Object.keys(versions);
32
- if (versionsList.length === 0) {
33
- return manifest;
34
- }
35
-
36
- for (const version of versionsList) {
37
- // holds a "hidden" value to be used by the package storage.
38
- versions[version][Symbol.for('__verdaccio_uplink')] = upLink.upname;
39
- }
40
-
41
- return { ...manifest, versions };
42
- }