@push.rocks/smartregistry 2.2.3 → 2.3.0
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
- package/dist_ts/cargo/classes.cargoregistry.js +42 -4
- package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
- package/dist_ts/cargo/classes.cargoupstream.js +129 -0
- package/dist_ts/cargo/index.d.ts +1 -0
- package/dist_ts/cargo/index.js +2 -1
- package/dist_ts/classes.smartregistry.js +8 -8
- package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
- package/dist_ts/composer/classes.composerregistry.js +34 -3
- package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
- package/dist_ts/composer/classes.composerupstream.js +159 -0
- package/dist_ts/composer/index.d.ts +1 -0
- package/dist_ts/composer/index.js +2 -1
- package/dist_ts/core/interfaces.core.d.ts +3 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
- package/dist_ts/maven/classes.mavenregistry.js +69 -4
- package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
- package/dist_ts/maven/classes.mavenupstream.js +153 -0
- package/dist_ts/maven/index.d.ts +1 -0
- package/dist_ts/maven/index.js +2 -1
- package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
- package/dist_ts/npm/classes.npmregistry.js +55 -6
- package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
- package/dist_ts/npm/classes.npmupstream.js +206 -0
- package/dist_ts/npm/index.d.ts +1 -0
- package/dist_ts/npm/index.js +2 -1
- package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
- package/dist_ts/oci/classes.ociregistry.js +78 -17
- package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
- package/dist_ts/oci/classes.ociupstream.js +206 -0
- package/dist_ts/oci/index.d.ts +1 -0
- package/dist_ts/oci/index.js +2 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
- package/dist_ts/pypi/classes.pypiregistry.js +60 -4
- package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
- package/dist_ts/pypi/classes.pypiupstream.js +165 -0
- package/dist_ts/pypi/index.d.ts +1 -0
- package/dist_ts/pypi/index.js +2 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
- package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
- package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
- package/dist_ts/rubygems/index.d.ts +1 -0
- package/dist_ts/rubygems/index.js +2 -1
- package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
- package/dist_ts/upstream/classes.baseupstream.js +409 -0
- package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
- package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
- package/dist_ts/upstream/classes.upstreamcache.d.ts +123 -0
- package/dist_ts/upstream/classes.upstreamcache.js +328 -0
- package/dist_ts/upstream/index.d.ts +6 -0
- package/dist_ts/upstream/index.js +7 -0
- package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
- package/dist_ts/upstream/interfaces.upstream.js +23 -0
- package/package.json +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +48 -3
- package/ts/cargo/classes.cargoupstream.ts +159 -0
- package/ts/cargo/index.ts +1 -0
- package/ts/classes.smartregistry.ts +49 -7
- package/ts/composer/classes.composerregistry.ts +39 -2
- package/ts/composer/classes.composerupstream.ts +200 -0
- package/ts/composer/index.ts +1 -0
- package/ts/core/interfaces.core.ts +3 -0
- package/ts/index.ts +3 -0
- package/ts/maven/classes.mavenregistry.ts +84 -3
- package/ts/maven/classes.mavenupstream.ts +220 -0
- package/ts/maven/index.ts +1 -0
- package/ts/npm/classes.npmregistry.ts +61 -5
- package/ts/npm/classes.npmupstream.ts +260 -0
- package/ts/npm/index.ts +1 -0
- package/ts/oci/classes.ociregistry.ts +89 -17
- package/ts/oci/classes.ociupstream.ts +263 -0
- package/ts/oci/index.ts +1 -0
- package/ts/plugins.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +68 -3
- package/ts/pypi/classes.pypiupstream.ts +211 -0
- package/ts/pypi/index.ts +1 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
- package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
- package/ts/rubygems/index.ts +1 -0
- package/ts/upstream/classes.baseupstream.ts +521 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +423 -0
- package/ts/upstream/index.ts +11 -0
- package/ts/upstream/interfaces.upstream.ts +195 -0
|
@@ -3,6 +3,8 @@ import { BaseRegistry } from '../core/classes.baseregistry.js';
|
|
|
3
3
|
import { RegistryStorage } from '../core/classes.registrystorage.js';
|
|
4
4
|
import { AuthManager } from '../core/classes.authmanager.js';
|
|
5
5
|
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
|
7
|
+
import { NpmUpstream } from './classes.npmupstream.js';
|
|
6
8
|
import type {
|
|
7
9
|
IPackument,
|
|
8
10
|
INpmVersion,
|
|
@@ -25,12 +27,14 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
25
27
|
private basePath: string = '/npm';
|
|
26
28
|
private registryUrl: string;
|
|
27
29
|
private logger: Smartlog;
|
|
30
|
+
private upstream: NpmUpstream | null = null;
|
|
28
31
|
|
|
29
32
|
constructor(
|
|
30
33
|
storage: RegistryStorage,
|
|
31
34
|
authManager: AuthManager,
|
|
32
35
|
basePath: string = '/npm',
|
|
33
|
-
registryUrl: string = 'http://localhost:5000/npm'
|
|
36
|
+
registryUrl: string = 'http://localhost:5000/npm',
|
|
37
|
+
upstreamConfig?: IProtocolUpstreamConfig
|
|
34
38
|
) {
|
|
35
39
|
super();
|
|
36
40
|
this.storage = storage;
|
|
@@ -50,6 +54,14 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
50
54
|
}
|
|
51
55
|
});
|
|
52
56
|
this.logger.enableConsole();
|
|
57
|
+
|
|
58
|
+
// Initialize upstream if configured
|
|
59
|
+
if (upstreamConfig?.enabled) {
|
|
60
|
+
this.upstream = new NpmUpstream(upstreamConfig, registryUrl, this.logger);
|
|
61
|
+
this.logger.log('info', 'NPM upstream initialized', {
|
|
62
|
+
upstreams: upstreamConfig.upstreams.map(u => u.name),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
public async init(): Promise<void> {
|
|
@@ -209,13 +221,28 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
209
221
|
token: IAuthToken | null,
|
|
210
222
|
query: Record<string, string>
|
|
211
223
|
): Promise<IResponse> {
|
|
212
|
-
|
|
224
|
+
let packument = await this.storage.getNpmPackument(packageName);
|
|
213
225
|
this.logger.log('debug', `getPackument: ${packageName}`, {
|
|
214
226
|
packageName,
|
|
215
227
|
found: !!packument,
|
|
216
228
|
versions: packument ? Object.keys(packument.versions).length : 0
|
|
217
229
|
});
|
|
218
230
|
|
|
231
|
+
// If not found locally, try upstream
|
|
232
|
+
if (!packument && this.upstream) {
|
|
233
|
+
this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
|
|
234
|
+
const upstreamPackument = await this.upstream.fetchPackument(packageName);
|
|
235
|
+
if (upstreamPackument) {
|
|
236
|
+
this.logger.log('debug', `getPackument: found in upstream`, {
|
|
237
|
+
packageName,
|
|
238
|
+
versions: Object.keys(upstreamPackument.versions || {}).length
|
|
239
|
+
});
|
|
240
|
+
packument = upstreamPackument;
|
|
241
|
+
// Optionally cache the packument locally (without tarballs)
|
|
242
|
+
// We don't store tarballs here - they'll be fetched on demand
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
if (!packument) {
|
|
220
247
|
return {
|
|
221
248
|
status: 404,
|
|
@@ -255,11 +282,21 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
255
282
|
token: IAuthToken | null
|
|
256
283
|
): Promise<IResponse> {
|
|
257
284
|
this.logger.log('debug', 'handlePackageVersion', { packageName, version });
|
|
258
|
-
|
|
285
|
+
let packument = await this.storage.getNpmPackument(packageName);
|
|
259
286
|
this.logger.log('debug', 'handlePackageVersion packument', { found: !!packument });
|
|
260
287
|
if (packument) {
|
|
261
288
|
this.logger.log('debug', 'handlePackageVersion versions', { versions: Object.keys(packument.versions || {}) });
|
|
262
289
|
}
|
|
290
|
+
|
|
291
|
+
// If not found locally, try upstream
|
|
292
|
+
if (!packument && this.upstream) {
|
|
293
|
+
this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
|
|
294
|
+
const upstreamPackument = await this.upstream.fetchPackument(packageName);
|
|
295
|
+
if (upstreamPackument) {
|
|
296
|
+
packument = upstreamPackument;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
263
300
|
if (!packument) {
|
|
264
301
|
return {
|
|
265
302
|
status: 404,
|
|
@@ -529,7 +566,7 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
529
566
|
token: IAuthToken | null
|
|
530
567
|
): Promise<IResponse> {
|
|
531
568
|
// Extract version from filename: package-name-1.0.0.tgz
|
|
532
|
-
const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/);
|
|
569
|
+
const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/i);
|
|
533
570
|
if (!versionMatch) {
|
|
534
571
|
return {
|
|
535
572
|
status: 400,
|
|
@@ -539,7 +576,26 @@ export class NpmRegistry extends BaseRegistry {
|
|
|
539
576
|
}
|
|
540
577
|
|
|
541
578
|
const version = versionMatch[1];
|
|
542
|
-
|
|
579
|
+
let tarball = await this.storage.getNpmTarball(packageName, version);
|
|
580
|
+
|
|
581
|
+
// If not found locally, try upstream
|
|
582
|
+
if (!tarball && this.upstream) {
|
|
583
|
+
this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
|
|
584
|
+
packageName,
|
|
585
|
+
version,
|
|
586
|
+
});
|
|
587
|
+
const upstreamTarball = await this.upstream.fetchTarball(packageName, version);
|
|
588
|
+
if (upstreamTarball) {
|
|
589
|
+
tarball = upstreamTarball;
|
|
590
|
+
// Cache the tarball locally for future requests
|
|
591
|
+
await this.storage.putNpmTarball(packageName, version, tarball);
|
|
592
|
+
this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
|
|
593
|
+
packageName,
|
|
594
|
+
version,
|
|
595
|
+
size: tarball.length,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
543
599
|
|
|
544
600
|
if (!tarball) {
|
|
545
601
|
return {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
|
3
|
+
import type {
|
|
4
|
+
IProtocolUpstreamConfig,
|
|
5
|
+
IUpstreamFetchContext,
|
|
6
|
+
IUpstreamResult,
|
|
7
|
+
IUpstreamRegistryConfig,
|
|
8
|
+
} from '../upstream/interfaces.upstream.js';
|
|
9
|
+
import type { IPackument, INpmVersion } from './interfaces.npm.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* NPM-specific upstream implementation.
|
|
13
|
+
*
|
|
14
|
+
* Handles:
|
|
15
|
+
* - Package metadata (packument) fetching
|
|
16
|
+
* - Tarball proxying
|
|
17
|
+
* - Scoped package routing (@scope/* patterns)
|
|
18
|
+
* - NPM-specific URL rewriting
|
|
19
|
+
*/
|
|
20
|
+
export class NpmUpstream extends BaseUpstream {
|
|
21
|
+
protected readonly protocolName = 'npm';
|
|
22
|
+
|
|
23
|
+
/** Local registry URL for rewriting tarball URLs */
|
|
24
|
+
private readonly localRegistryUrl: string;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
config: IProtocolUpstreamConfig,
|
|
28
|
+
localRegistryUrl: string,
|
|
29
|
+
logger?: plugins.smartlog.Smartlog,
|
|
30
|
+
) {
|
|
31
|
+
super(config, logger);
|
|
32
|
+
this.localRegistryUrl = localRegistryUrl;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a packument from upstream registries.
|
|
37
|
+
*/
|
|
38
|
+
public async fetchPackument(packageName: string): Promise<IPackument | null> {
|
|
39
|
+
const context: IUpstreamFetchContext = {
|
|
40
|
+
protocol: 'npm',
|
|
41
|
+
resource: packageName,
|
|
42
|
+
resourceType: 'packument',
|
|
43
|
+
path: `/${encodeURIComponent(packageName).replace('%40', '@')}`,
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: {
|
|
46
|
+
'accept': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
query: {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = await this.fetch(context);
|
|
52
|
+
|
|
53
|
+
if (!result || !result.success) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse and process packument
|
|
58
|
+
let packument: IPackument;
|
|
59
|
+
if (Buffer.isBuffer(result.body)) {
|
|
60
|
+
packument = JSON.parse(result.body.toString('utf8'));
|
|
61
|
+
} else {
|
|
62
|
+
packument = result.body;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Rewrite tarball URLs to point to local registry
|
|
66
|
+
packument = this.rewriteTarballUrls(packument);
|
|
67
|
+
|
|
68
|
+
return packument;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fetch a specific version from upstream registries.
|
|
73
|
+
*/
|
|
74
|
+
public async fetchVersion(packageName: string, version: string): Promise<INpmVersion | null> {
|
|
75
|
+
const context: IUpstreamFetchContext = {
|
|
76
|
+
protocol: 'npm',
|
|
77
|
+
resource: packageName,
|
|
78
|
+
resourceType: 'version',
|
|
79
|
+
path: `/${encodeURIComponent(packageName).replace('%40', '@')}/${version}`,
|
|
80
|
+
method: 'GET',
|
|
81
|
+
headers: {
|
|
82
|
+
'accept': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
query: {},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = await this.fetch(context);
|
|
88
|
+
|
|
89
|
+
if (!result || !result.success) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let versionData: INpmVersion;
|
|
94
|
+
if (Buffer.isBuffer(result.body)) {
|
|
95
|
+
versionData = JSON.parse(result.body.toString('utf8'));
|
|
96
|
+
} else {
|
|
97
|
+
versionData = result.body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Rewrite tarball URL
|
|
101
|
+
if (versionData.dist?.tarball) {
|
|
102
|
+
versionData.dist.tarball = this.rewriteSingleTarballUrl(
|
|
103
|
+
packageName,
|
|
104
|
+
versionData.version,
|
|
105
|
+
versionData.dist.tarball,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return versionData;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch a tarball from upstream registries.
|
|
114
|
+
*/
|
|
115
|
+
public async fetchTarball(packageName: string, version: string): Promise<Buffer | null> {
|
|
116
|
+
// First, try to get the tarball URL from packument
|
|
117
|
+
const packument = await this.fetchPackument(packageName);
|
|
118
|
+
let tarballPath: string;
|
|
119
|
+
|
|
120
|
+
if (packument?.versions?.[version]?.dist?.tarball) {
|
|
121
|
+
// Extract path from original (upstream) tarball URL
|
|
122
|
+
const tarballUrl = packument.versions[version].dist.tarball;
|
|
123
|
+
try {
|
|
124
|
+
const url = new URL(tarballUrl);
|
|
125
|
+
tarballPath = url.pathname;
|
|
126
|
+
} catch {
|
|
127
|
+
// Fallback to standard NPM tarball path
|
|
128
|
+
tarballPath = this.buildTarballPath(packageName, version);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
tarballPath = this.buildTarballPath(packageName, version);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const context: IUpstreamFetchContext = {
|
|
135
|
+
protocol: 'npm',
|
|
136
|
+
resource: packageName,
|
|
137
|
+
resourceType: 'tarball',
|
|
138
|
+
path: tarballPath,
|
|
139
|
+
method: 'GET',
|
|
140
|
+
headers: {
|
|
141
|
+
'accept': 'application/octet-stream',
|
|
142
|
+
},
|
|
143
|
+
query: {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const result = await this.fetch(context);
|
|
147
|
+
|
|
148
|
+
if (!result || !result.success) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Search packages in upstream registries.
|
|
157
|
+
*/
|
|
158
|
+
public async search(text: string, size: number = 20, from: number = 0): Promise<any | null> {
|
|
159
|
+
const context: IUpstreamFetchContext = {
|
|
160
|
+
protocol: 'npm',
|
|
161
|
+
resource: '*',
|
|
162
|
+
resourceType: 'search',
|
|
163
|
+
path: '/-/v1/search',
|
|
164
|
+
method: 'GET',
|
|
165
|
+
headers: {
|
|
166
|
+
'accept': 'application/json',
|
|
167
|
+
},
|
|
168
|
+
query: {
|
|
169
|
+
text,
|
|
170
|
+
size: size.toString(),
|
|
171
|
+
from: from.toString(),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = await this.fetch(context);
|
|
176
|
+
|
|
177
|
+
if (!result || !result.success) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Buffer.isBuffer(result.body)) {
|
|
182
|
+
return JSON.parse(result.body.toString('utf8'));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result.body;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build the standard NPM tarball path.
|
|
190
|
+
*/
|
|
191
|
+
private buildTarballPath(packageName: string, version: string): string {
|
|
192
|
+
// NPM uses: /{package}/-/{package-name}-{version}.tgz
|
|
193
|
+
// For scoped packages: /@scope/name/-/name-version.tgz
|
|
194
|
+
if (packageName.startsWith('@')) {
|
|
195
|
+
const [scope, name] = packageName.split('/');
|
|
196
|
+
return `/${scope}/${name}/-/${name}-${version}.tgz`;
|
|
197
|
+
} else {
|
|
198
|
+
return `/${packageName}/-/${packageName}-${version}.tgz`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rewrite all tarball URLs in a packument to point to local registry.
|
|
204
|
+
*/
|
|
205
|
+
private rewriteTarballUrls(packument: IPackument): IPackument {
|
|
206
|
+
if (!packument.versions) {
|
|
207
|
+
return packument;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rewritten = { ...packument };
|
|
211
|
+
rewritten.versions = {};
|
|
212
|
+
|
|
213
|
+
for (const [version, versionData] of Object.entries(packument.versions)) {
|
|
214
|
+
const newVersionData = { ...versionData };
|
|
215
|
+
if (newVersionData.dist?.tarball) {
|
|
216
|
+
newVersionData.dist = {
|
|
217
|
+
...newVersionData.dist,
|
|
218
|
+
tarball: this.rewriteSingleTarballUrl(
|
|
219
|
+
packument.name,
|
|
220
|
+
version,
|
|
221
|
+
newVersionData.dist.tarball,
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
rewritten.versions[version] = newVersionData;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return rewritten;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Rewrite a single tarball URL to point to local registry.
|
|
233
|
+
*/
|
|
234
|
+
private rewriteSingleTarballUrl(
|
|
235
|
+
packageName: string,
|
|
236
|
+
version: string,
|
|
237
|
+
_originalUrl: string,
|
|
238
|
+
): string {
|
|
239
|
+
// Generate local tarball URL
|
|
240
|
+
// Format: {localRegistryUrl}/{package}/-/{package-name}-{version}.tgz
|
|
241
|
+
const safeName = packageName.replace('@', '').replace('/', '-');
|
|
242
|
+
return `${this.localRegistryUrl}/${packageName}/-/${safeName}-${version}.tgz`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Override URL building for NPM-specific handling.
|
|
247
|
+
*/
|
|
248
|
+
protected buildUpstreamUrl(
|
|
249
|
+
upstream: IUpstreamRegistryConfig,
|
|
250
|
+
context: IUpstreamFetchContext,
|
|
251
|
+
): string {
|
|
252
|
+
// NPM registries often don't have trailing slashes
|
|
253
|
+
let baseUrl = upstream.url;
|
|
254
|
+
if (baseUrl.endsWith('/')) {
|
|
255
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return `${baseUrl}${context.path}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
package/ts/npm/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { Smartlog } from '@push.rocks/smartlog';
|
|
1
2
|
import { BaseRegistry } from '../core/classes.baseregistry.js';
|
|
2
3
|
import { RegistryStorage } from '../core/classes.registrystorage.js';
|
|
3
4
|
import { AuthManager } from '../core/classes.authmanager.js';
|
|
4
5
|
import type { IRequestContext, IResponse, IAuthToken, IRegistryError } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
|
7
|
+
import { OciUpstream } from './classes.ociupstream.js';
|
|
5
8
|
import type {
|
|
6
9
|
IUploadSession,
|
|
7
10
|
IOciManifest,
|
|
@@ -21,18 +24,42 @@ export class OciRegistry extends BaseRegistry {
|
|
|
21
24
|
private basePath: string = '/oci';
|
|
22
25
|
private cleanupInterval?: NodeJS.Timeout;
|
|
23
26
|
private ociTokens?: { realm: string; service: string };
|
|
27
|
+
private upstream: OciUpstream | null = null;
|
|
28
|
+
private logger: Smartlog;
|
|
24
29
|
|
|
25
30
|
constructor(
|
|
26
31
|
storage: RegistryStorage,
|
|
27
32
|
authManager: AuthManager,
|
|
28
33
|
basePath: string = '/oci',
|
|
29
|
-
ociTokens?: { realm: string; service: string }
|
|
34
|
+
ociTokens?: { realm: string; service: string },
|
|
35
|
+
upstreamConfig?: IProtocolUpstreamConfig
|
|
30
36
|
) {
|
|
31
37
|
super();
|
|
32
38
|
this.storage = storage;
|
|
33
39
|
this.authManager = authManager;
|
|
34
40
|
this.basePath = basePath;
|
|
35
41
|
this.ociTokens = ociTokens;
|
|
42
|
+
|
|
43
|
+
// Initialize logger
|
|
44
|
+
this.logger = new Smartlog({
|
|
45
|
+
logContext: {
|
|
46
|
+
company: 'push.rocks',
|
|
47
|
+
companyunit: 'smartregistry',
|
|
48
|
+
containerName: 'oci-registry',
|
|
49
|
+
environment: (process.env.NODE_ENV as any) || 'development',
|
|
50
|
+
runtime: 'node',
|
|
51
|
+
zone: 'oci'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.logger.enableConsole();
|
|
55
|
+
|
|
56
|
+
// Initialize upstream if configured
|
|
57
|
+
if (upstreamConfig?.enabled) {
|
|
58
|
+
this.upstream = new OciUpstream(upstreamConfig, basePath, this.logger);
|
|
59
|
+
this.logger.log('info', 'OCI upstream initialized', {
|
|
60
|
+
upstreams: upstreamConfig.upstreams.map(u => u.name),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
public async init(): Promise<void> {
|
|
@@ -302,16 +329,50 @@ export class OciRegistry extends BaseRegistry {
|
|
|
302
329
|
if (!reference.startsWith('sha256:')) {
|
|
303
330
|
const tags = await this.getTagsData(repository);
|
|
304
331
|
digest = tags[reference];
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Try local storage first (if we have a digest)
|
|
335
|
+
let manifestData: Buffer | null = null;
|
|
336
|
+
let contentType: string | null = null;
|
|
337
|
+
|
|
338
|
+
if (digest) {
|
|
339
|
+
manifestData = await this.storage.getOciManifest(repository, digest);
|
|
340
|
+
if (manifestData) {
|
|
341
|
+
contentType = await this.storage.getOciManifestContentType(repository, digest);
|
|
342
|
+
if (!contentType) {
|
|
343
|
+
contentType = this.detectManifestContentType(manifestData);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If not found locally, try upstream
|
|
349
|
+
if (!manifestData && this.upstream) {
|
|
350
|
+
this.logger.log('debug', 'getManifest: fetching from upstream', { repository, reference });
|
|
351
|
+
const upstreamResult = await this.upstream.fetchManifest(repository, reference);
|
|
352
|
+
if (upstreamResult) {
|
|
353
|
+
manifestData = Buffer.from(JSON.stringify(upstreamResult.manifest), 'utf8');
|
|
354
|
+
contentType = upstreamResult.contentType;
|
|
355
|
+
digest = upstreamResult.digest;
|
|
356
|
+
|
|
357
|
+
// Cache the manifest locally
|
|
358
|
+
await this.storage.putOciManifest(repository, digest, manifestData, contentType);
|
|
359
|
+
|
|
360
|
+
// If reference is a tag, update tags mapping
|
|
361
|
+
if (!reference.startsWith('sha256:')) {
|
|
362
|
+
const tags = await this.getTagsData(repository);
|
|
363
|
+
tags[reference] = digest;
|
|
364
|
+
const tagsPath = `oci/tags/${repository}/tags.json`;
|
|
365
|
+
await this.storage.putObject(tagsPath, Buffer.from(JSON.stringify(tags), 'utf-8'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.logger.log('debug', 'getManifest: cached manifest locally', {
|
|
369
|
+
repository,
|
|
370
|
+
reference,
|
|
371
|
+
digest,
|
|
372
|
+
});
|
|
311
373
|
}
|
|
312
374
|
}
|
|
313
375
|
|
|
314
|
-
const manifestData = await this.storage.getOciManifest(repository, digest);
|
|
315
376
|
if (!manifestData) {
|
|
316
377
|
return {
|
|
317
378
|
status: 404,
|
|
@@ -320,17 +381,10 @@ export class OciRegistry extends BaseRegistry {
|
|
|
320
381
|
};
|
|
321
382
|
}
|
|
322
383
|
|
|
323
|
-
// Get stored content type, falling back to detecting from manifest content
|
|
324
|
-
let contentType = await this.storage.getOciManifestContentType(repository, digest);
|
|
325
|
-
if (!contentType) {
|
|
326
|
-
// Fallback: detect content type from manifest content
|
|
327
|
-
contentType = this.detectManifestContentType(manifestData);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
384
|
return {
|
|
331
385
|
status: 200,
|
|
332
386
|
headers: {
|
|
333
|
-
'Content-Type': contentType,
|
|
387
|
+
'Content-Type': contentType || 'application/vnd.oci.image.manifest.v1+json',
|
|
334
388
|
'Docker-Content-Digest': digest,
|
|
335
389
|
},
|
|
336
390
|
body: manifestData,
|
|
@@ -466,7 +520,25 @@ export class OciRegistry extends BaseRegistry {
|
|
|
466
520
|
return this.createUnauthorizedResponse(repository, 'pull');
|
|
467
521
|
}
|
|
468
522
|
|
|
469
|
-
|
|
523
|
+
// Try local storage first
|
|
524
|
+
let data = await this.storage.getOciBlob(digest);
|
|
525
|
+
|
|
526
|
+
// If not found locally, try upstream
|
|
527
|
+
if (!data && this.upstream) {
|
|
528
|
+
this.logger.log('debug', 'getBlob: fetching from upstream', { repository, digest });
|
|
529
|
+
const upstreamBlob = await this.upstream.fetchBlob(repository, digest);
|
|
530
|
+
if (upstreamBlob) {
|
|
531
|
+
data = upstreamBlob;
|
|
532
|
+
// Cache the blob locally (blobs are content-addressable and immutable)
|
|
533
|
+
await this.storage.putOciBlob(digest, data);
|
|
534
|
+
this.logger.log('debug', 'getBlob: cached blob locally', {
|
|
535
|
+
repository,
|
|
536
|
+
digest,
|
|
537
|
+
size: data.length,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
470
542
|
if (!data) {
|
|
471
543
|
return {
|
|
472
544
|
status: 404,
|