@push.rocks/smartregistry 2.6.0 → 2.8.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/.smartconfig.json +24 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +8 -3
- package/dist_ts/cargo/classes.cargoregistry.js +71 -33
- package/dist_ts/classes.smartregistry.js +48 -36
- package/dist_ts/composer/classes.composerregistry.d.ts +14 -3
- package/dist_ts/composer/classes.composerregistry.js +64 -28
- package/dist_ts/core/classes.registrystorage.d.ts +45 -0
- package/dist_ts/core/classes.registrystorage.js +116 -1
- package/dist_ts/core/helpers.stream.d.ts +20 -0
- package/dist_ts/core/helpers.stream.js +59 -0
- package/dist_ts/core/index.d.ts +1 -0
- package/dist_ts/core/index.js +3 -1
- package/dist_ts/core/interfaces.core.d.ts +28 -5
- package/dist_ts/maven/classes.mavenregistry.d.ts +14 -3
- package/dist_ts/maven/classes.mavenregistry.js +78 -27
- package/dist_ts/npm/classes.npmregistry.d.ts +14 -3
- package/dist_ts/npm/classes.npmregistry.js +104 -48
- package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
- package/dist_ts/oci/classes.ociregistry.js +186 -73
- package/dist_ts/oci/classes.ociupstream.d.ts +5 -2
- package/dist_ts/oci/classes.ociupstream.js +17 -10
- package/dist_ts/oci/interfaces.oci.d.ts +4 -0
- package/dist_ts/pypi/classes.pypiregistry.d.ts +8 -3
- package/dist_ts/pypi/classes.pypiregistry.js +88 -50
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +8 -3
- package/dist_ts/rubygems/classes.rubygemsregistry.js +61 -23
- package/dist_ts/rubygems/helpers.rubygems.js +3 -3
- package/dist_ts/upstream/classes.upstreamcache.js +2 -2
- package/dist_ts/upstream/interfaces.upstream.d.ts +72 -1
- package/dist_ts/upstream/interfaces.upstream.js +24 -1
- package/package.json +24 -20
- package/readme.md +354 -812
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +84 -37
- package/ts/classes.smartregistry.ts +49 -35
- package/ts/composer/classes.composerregistry.ts +74 -30
- package/ts/core/classes.registrystorage.ts +133 -2
- package/ts/core/helpers.stream.ts +63 -0
- package/ts/core/index.ts +3 -0
- package/ts/core/interfaces.core.ts +29 -5
- package/ts/maven/classes.mavenregistry.ts +89 -28
- package/ts/npm/classes.npmregistry.ts +118 -49
- package/ts/oci/classes.ociregistry.ts +205 -77
- package/ts/oci/classes.ociupstream.ts +18 -8
- package/ts/oci/interfaces.oci.ts +4 -0
- package/ts/pypi/classes.pypiregistry.ts +100 -54
- package/ts/rubygems/classes.rubygemsregistry.ts +69 -24
- package/ts/rubygems/helpers.rubygems.ts +2 -2
- package/ts/upstream/classes.upstreamcache.ts +1 -1
- package/ts/upstream/interfaces.upstream.ts +82 -1
- package/npmextra.json +0 -18
|
@@ -2,8 +2,8 @@ import { Smartlog } from '@push.rocks/smartlog';
|
|
|
2
2
|
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
|
-
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
-
import type {
|
|
5
|
+
import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
|
|
7
7
|
import { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
|
8
8
|
import type {
|
|
9
9
|
IPypiPackageMetadata,
|
|
@@ -24,20 +24,21 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
24
24
|
private basePath: string = '/pypi';
|
|
25
25
|
private registryUrl: string;
|
|
26
26
|
private logger: Smartlog;
|
|
27
|
-
private
|
|
27
|
+
private upstreamProvider: IUpstreamProvider | null = null;
|
|
28
28
|
|
|
29
29
|
constructor(
|
|
30
30
|
storage: RegistryStorage,
|
|
31
31
|
authManager: AuthManager,
|
|
32
32
|
basePath: string = '/pypi',
|
|
33
33
|
registryUrl: string = 'http://localhost:5000',
|
|
34
|
-
|
|
34
|
+
upstreamProvider?: IUpstreamProvider
|
|
35
35
|
) {
|
|
36
36
|
super();
|
|
37
37
|
this.storage = storage;
|
|
38
38
|
this.authManager = authManager;
|
|
39
39
|
this.basePath = basePath;
|
|
40
40
|
this.registryUrl = registryUrl;
|
|
41
|
+
this.upstreamProvider = upstreamProvider || null;
|
|
41
42
|
|
|
42
43
|
// Initialize logger
|
|
43
44
|
this.logger = new Smartlog({
|
|
@@ -51,20 +52,38 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
this.logger.enableConsole();
|
|
55
|
+
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Get upstream for a specific request.
|
|
59
|
+
* Calls the provider to resolve upstream config dynamically.
|
|
60
|
+
*/
|
|
61
|
+
private async getUpstreamForRequest(
|
|
62
|
+
resource: string,
|
|
63
|
+
resourceType: string,
|
|
64
|
+
method: string,
|
|
65
|
+
actor?: IRequestActor
|
|
66
|
+
): Promise<PypiUpstream | null> {
|
|
67
|
+
if (!this.upstreamProvider) return null;
|
|
68
|
+
|
|
69
|
+
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
|
70
|
+
protocol: 'pypi',
|
|
71
|
+
resource,
|
|
72
|
+
scope: resource, // For PyPI, package name is the scope
|
|
73
|
+
actor,
|
|
74
|
+
method,
|
|
75
|
+
resourceType,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!config?.enabled) return null;
|
|
79
|
+
return new PypiUpstream(config, this.registryUrl, this.logger);
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/**
|
|
62
83
|
* Clean up resources (timers, connections, etc.)
|
|
63
84
|
*/
|
|
64
85
|
public destroy(): void {
|
|
65
|
-
|
|
66
|
-
this.upstream.stop();
|
|
67
|
-
}
|
|
86
|
+
// No persistent upstream to clean up with dynamic provider
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
public async init(): Promise<void> {
|
|
@@ -84,15 +103,23 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
84
103
|
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
|
85
104
|
let path = context.path.replace(this.basePath, '');
|
|
86
105
|
|
|
106
|
+
// Extract token (Basic Auth or Bearer)
|
|
107
|
+
const token = await this.extractToken(context);
|
|
108
|
+
|
|
109
|
+
// Build actor from context and validated token
|
|
110
|
+
const actor: IRequestActor = {
|
|
111
|
+
...context.actor,
|
|
112
|
+
userId: token?.userId,
|
|
113
|
+
ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
|
|
114
|
+
userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
|
|
115
|
+
};
|
|
116
|
+
|
|
87
117
|
// Also handle /simple path prefix
|
|
88
118
|
if (path.startsWith('/simple')) {
|
|
89
119
|
path = path.replace('/simple', '');
|
|
90
|
-
return this.handleSimpleRequest(path, context);
|
|
120
|
+
return this.handleSimpleRequest(path, context, actor);
|
|
91
121
|
}
|
|
92
122
|
|
|
93
|
-
// Extract token (Basic Auth or Bearer)
|
|
94
|
-
const token = await this.extractToken(context);
|
|
95
|
-
|
|
96
123
|
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
|
97
124
|
method: context.method,
|
|
98
125
|
path,
|
|
@@ -119,7 +146,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
119
146
|
// Package file download: GET /packages/{package}/{filename}
|
|
120
147
|
const downloadMatch = path.match(/^\/packages\/([^\/]+)\/(.+)$/);
|
|
121
148
|
if (downloadMatch && context.method === 'GET') {
|
|
122
|
-
return this.handleDownload(downloadMatch[1], downloadMatch[2]);
|
|
149
|
+
return this.handleDownload(downloadMatch[1], downloadMatch[2], actor);
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
// Delete package: DELETE /packages/{package}
|
|
@@ -156,7 +183,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
156
183
|
/**
|
|
157
184
|
* Handle Simple API requests (PEP 503 HTML or PEP 691 JSON)
|
|
158
185
|
*/
|
|
159
|
-
private async handleSimpleRequest(path: string, context: IRequestContext): Promise<IResponse> {
|
|
186
|
+
private async handleSimpleRequest(path: string, context: IRequestContext, actor?: IRequestActor): Promise<IResponse> {
|
|
160
187
|
// Ensure path ends with / (PEP 503 requirement)
|
|
161
188
|
if (!path.endsWith('/') && !path.includes('.')) {
|
|
162
189
|
return {
|
|
@@ -174,7 +201,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
174
201
|
// Package index: /simple/{package}/
|
|
175
202
|
const packageMatch = path.match(/^\/([^\/]+)\/$/);
|
|
176
203
|
if (packageMatch) {
|
|
177
|
-
return this.handleSimplePackage(packageMatch[1], context);
|
|
204
|
+
return this.handleSimplePackage(packageMatch[1], context, actor);
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
return {
|
|
@@ -228,46 +255,49 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
228
255
|
* Handle Simple API package index
|
|
229
256
|
* Returns HTML (PEP 503) or JSON (PEP 691) based on Accept header
|
|
230
257
|
*/
|
|
231
|
-
private async handleSimplePackage(packageName: string, context: IRequestContext): Promise<IResponse> {
|
|
258
|
+
private async handleSimplePackage(packageName: string, context: IRequestContext, actor?: IRequestActor): Promise<IResponse> {
|
|
232
259
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
233
260
|
|
|
234
261
|
// Get package metadata
|
|
235
262
|
let metadata = await this.storage.getPypiPackageMetadata(normalized);
|
|
236
263
|
|
|
237
264
|
// Try upstream if not found locally
|
|
238
|
-
if (!metadata
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
if (!metadata) {
|
|
266
|
+
const upstream = await this.getUpstreamForRequest(normalized, 'simple', 'GET', actor);
|
|
267
|
+
if (upstream) {
|
|
268
|
+
const upstreamHtml = await upstream.fetchSimplePackage(normalized);
|
|
269
|
+
if (upstreamHtml) {
|
|
270
|
+
// Parse the HTML to extract file information and cache it
|
|
271
|
+
// For now, just return the upstream HTML directly (caching can be improved later)
|
|
272
|
+
const acceptHeader = context.headers['accept'] || context.headers['Accept'] || '';
|
|
273
|
+
const preferJson = acceptHeader.includes('application/vnd.pypi.simple') &&
|
|
274
|
+
acceptHeader.includes('json');
|
|
275
|
+
|
|
276
|
+
if (preferJson) {
|
|
277
|
+
// Try to get JSON format from upstream
|
|
278
|
+
const upstreamJson = await upstream.fetchPackageJson(normalized);
|
|
279
|
+
if (upstreamJson) {
|
|
280
|
+
return {
|
|
281
|
+
status: 200,
|
|
282
|
+
headers: {
|
|
283
|
+
'Content-Type': 'application/vnd.pypi.simple.v1+json',
|
|
284
|
+
'Cache-Control': 'public, max-age=300'
|
|
285
|
+
},
|
|
286
|
+
body: upstreamJson,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
259
289
|
}
|
|
260
|
-
}
|
|
261
290
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
// Return HTML format
|
|
292
|
+
return {
|
|
293
|
+
status: 200,
|
|
294
|
+
headers: {
|
|
295
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
296
|
+
'Cache-Control': 'public, max-age=300'
|
|
297
|
+
},
|
|
298
|
+
body: upstreamHtml,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
271
301
|
}
|
|
272
302
|
}
|
|
273
303
|
|
|
@@ -503,13 +533,29 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
503
533
|
/**
|
|
504
534
|
* Handle package download
|
|
505
535
|
*/
|
|
506
|
-
private async handleDownload(packageName: string, filename: string): Promise<IResponse> {
|
|
536
|
+
private async handleDownload(packageName: string, filename: string, actor?: IRequestActor): Promise<IResponse> {
|
|
507
537
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
508
|
-
|
|
538
|
+
|
|
539
|
+
// Try streaming from local storage first
|
|
540
|
+
const streamResult = await this.storage.getPypiPackageFileStream(normalized, filename);
|
|
541
|
+
|
|
542
|
+
if (streamResult) {
|
|
543
|
+
return {
|
|
544
|
+
status: 200,
|
|
545
|
+
headers: {
|
|
546
|
+
'Content-Type': 'application/octet-stream',
|
|
547
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
548
|
+
'Content-Length': streamResult.size.toString()
|
|
549
|
+
},
|
|
550
|
+
body: streamResult.stream,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
509
553
|
|
|
510
554
|
// Try upstream if not found locally
|
|
511
|
-
|
|
512
|
-
|
|
555
|
+
let fileData: Buffer | null = null;
|
|
556
|
+
const upstream = await this.getUpstreamForRequest(normalized, 'file', 'GET', actor);
|
|
557
|
+
if (upstream) {
|
|
558
|
+
fileData = await upstream.fetchPackageFile(normalized, filename);
|
|
513
559
|
if (fileData) {
|
|
514
560
|
// Cache locally
|
|
515
561
|
await this.storage.putPypiPackageFile(normalized, filename, fileData);
|
|
@@ -2,8 +2,8 @@ import { Smartlog } from '@push.rocks/smartlog';
|
|
|
2
2
|
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
|
-
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
-
import type {
|
|
5
|
+
import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
|
|
7
7
|
import type {
|
|
8
8
|
IRubyGemsMetadata,
|
|
9
9
|
IRubyGemsVersionMetadata,
|
|
@@ -25,20 +25,21 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
25
25
|
private basePath: string = '/rubygems';
|
|
26
26
|
private registryUrl: string;
|
|
27
27
|
private logger: Smartlog;
|
|
28
|
-
private
|
|
28
|
+
private upstreamProvider: IUpstreamProvider | null = null;
|
|
29
29
|
|
|
30
30
|
constructor(
|
|
31
31
|
storage: RegistryStorage,
|
|
32
32
|
authManager: AuthManager,
|
|
33
33
|
basePath: string = '/rubygems',
|
|
34
34
|
registryUrl: string = 'http://localhost:5000/rubygems',
|
|
35
|
-
|
|
35
|
+
upstreamProvider?: IUpstreamProvider
|
|
36
36
|
) {
|
|
37
37
|
super();
|
|
38
38
|
this.storage = storage;
|
|
39
39
|
this.authManager = authManager;
|
|
40
40
|
this.basePath = basePath;
|
|
41
41
|
this.registryUrl = registryUrl;
|
|
42
|
+
this.upstreamProvider = upstreamProvider || null;
|
|
42
43
|
|
|
43
44
|
// Initialize logger
|
|
44
45
|
this.logger = new Smartlog({
|
|
@@ -52,20 +53,38 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
this.logger.enableConsole();
|
|
56
|
+
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Get upstream for a specific request.
|
|
60
|
+
* Calls the provider to resolve upstream config dynamically.
|
|
61
|
+
*/
|
|
62
|
+
private async getUpstreamForRequest(
|
|
63
|
+
resource: string,
|
|
64
|
+
resourceType: string,
|
|
65
|
+
method: string,
|
|
66
|
+
actor?: IRequestActor
|
|
67
|
+
): Promise<RubygemsUpstream | null> {
|
|
68
|
+
if (!this.upstreamProvider) return null;
|
|
69
|
+
|
|
70
|
+
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
|
71
|
+
protocol: 'rubygems',
|
|
72
|
+
resource,
|
|
73
|
+
scope: resource, // gem name is the scope
|
|
74
|
+
actor,
|
|
75
|
+
method,
|
|
76
|
+
resourceType,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!config?.enabled) return null;
|
|
80
|
+
return new RubygemsUpstream(config, this.logger);
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
/**
|
|
63
84
|
* Clean up resources (timers, connections, etc.)
|
|
64
85
|
*/
|
|
65
86
|
public destroy(): void {
|
|
66
|
-
|
|
67
|
-
this.upstream.stop();
|
|
68
|
-
}
|
|
87
|
+
// No persistent upstream to clean up with dynamic provider
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
public async init(): Promise<void> {
|
|
@@ -95,6 +114,14 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
95
114
|
// Extract token (Authorization header)
|
|
96
115
|
const token = await this.extractToken(context);
|
|
97
116
|
|
|
117
|
+
// Build actor from context and validated token
|
|
118
|
+
const actor: IRequestActor = {
|
|
119
|
+
...context.actor,
|
|
120
|
+
userId: token?.userId,
|
|
121
|
+
ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
|
|
122
|
+
userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
|
|
123
|
+
};
|
|
124
|
+
|
|
98
125
|
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
|
99
126
|
method: context.method,
|
|
100
127
|
path,
|
|
@@ -113,13 +140,13 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
113
140
|
// Info file: GET /info/{gem}
|
|
114
141
|
const infoMatch = path.match(/^\/info\/([^\/]+)$/);
|
|
115
142
|
if (infoMatch && context.method === 'GET') {
|
|
116
|
-
return this.handleInfoFile(infoMatch[1]);
|
|
143
|
+
return this.handleInfoFile(infoMatch[1], actor);
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
// Gem download: GET /gems/{gem}-{version}[-{platform}].gem
|
|
120
147
|
const downloadMatch = path.match(/^\/gems\/(.+\.gem)$/);
|
|
121
148
|
if (downloadMatch && context.method === 'GET') {
|
|
122
|
-
return this.handleDownload(downloadMatch[1]);
|
|
149
|
+
return this.handleDownload(downloadMatch[1], actor);
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
// Legacy specs endpoints (Marshal format)
|
|
@@ -232,16 +259,19 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
232
259
|
/**
|
|
233
260
|
* Handle /info/{gem} endpoint (Compact Index)
|
|
234
261
|
*/
|
|
235
|
-
private async handleInfoFile(gemName: string): Promise<IResponse> {
|
|
262
|
+
private async handleInfoFile(gemName: string, actor?: IRequestActor): Promise<IResponse> {
|
|
236
263
|
let content = await this.storage.getRubyGemsInfo(gemName);
|
|
237
264
|
|
|
238
265
|
// Try upstream if not found locally
|
|
239
|
-
if (!content
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
if (!content) {
|
|
267
|
+
const upstream = await this.getUpstreamForRequest(gemName, 'info', 'GET', actor);
|
|
268
|
+
if (upstream) {
|
|
269
|
+
const upstreamInfo = await upstream.fetchInfo(gemName);
|
|
270
|
+
if (upstreamInfo) {
|
|
271
|
+
// Cache locally
|
|
272
|
+
await this.storage.putRubyGemsInfo(gemName, upstreamInfo);
|
|
273
|
+
content = upstreamInfo;
|
|
274
|
+
}
|
|
245
275
|
}
|
|
246
276
|
}
|
|
247
277
|
|
|
@@ -267,21 +297,36 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
267
297
|
/**
|
|
268
298
|
* Handle gem file download
|
|
269
299
|
*/
|
|
270
|
-
private async handleDownload(filename: string): Promise<IResponse> {
|
|
300
|
+
private async handleDownload(filename: string, actor?: IRequestActor): Promise<IResponse> {
|
|
271
301
|
const parsed = helpers.parseGemFilename(filename);
|
|
272
302
|
if (!parsed) {
|
|
273
303
|
return this.errorResponse(400, 'Invalid gem filename');
|
|
274
304
|
}
|
|
275
305
|
|
|
276
|
-
|
|
306
|
+
// Try streaming from local storage first
|
|
307
|
+
const streamResult = await this.storage.getRubyGemsGemStream(
|
|
277
308
|
parsed.name,
|
|
278
309
|
parsed.version,
|
|
279
310
|
parsed.platform
|
|
280
311
|
);
|
|
281
312
|
|
|
313
|
+
if (streamResult) {
|
|
314
|
+
return {
|
|
315
|
+
status: 200,
|
|
316
|
+
headers: {
|
|
317
|
+
'Content-Type': 'application/octet-stream',
|
|
318
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
319
|
+
'Content-Length': streamResult.size.toString()
|
|
320
|
+
},
|
|
321
|
+
body: streamResult.stream,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
282
325
|
// Try upstream if not found locally
|
|
283
|
-
|
|
284
|
-
|
|
326
|
+
let gemData: Buffer | null = null;
|
|
327
|
+
const upstream = await this.getUpstreamForRequest(parsed.name, 'gem', 'GET', actor);
|
|
328
|
+
if (upstream) {
|
|
329
|
+
gemData = await upstream.fetchGem(parsed.name, parsed.version);
|
|
285
330
|
if (gemData) {
|
|
286
331
|
// Cache locally
|
|
287
332
|
await this.storage.putRubyGemsGem(parsed.name, parsed.version, gemData, parsed.platform);
|
|
@@ -427,7 +427,7 @@ export async function extractGemMetadata(gemData: Buffer): Promise<{
|
|
|
427
427
|
// Step 2: Decompress the gzipped metadata
|
|
428
428
|
const gzipTools = new plugins.smartarchive.GzipTools();
|
|
429
429
|
const metadataYaml = await gzipTools.decompress(metadataFile.contentBuffer);
|
|
430
|
-
const yamlContent = metadataYaml.toString('utf-8');
|
|
430
|
+
const yamlContent = Buffer.from(metadataYaml).toString('utf-8');
|
|
431
431
|
|
|
432
432
|
// Step 3: Parse the YAML to extract name, version, platform
|
|
433
433
|
// Look for name: field in YAML
|
|
@@ -503,7 +503,7 @@ export async function generateSpecsGz(specs: Array<[string, string, string]>): P
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
const uncompressed = Buffer.concat(parts);
|
|
506
|
-
return gzipTools.compress(uncompressed);
|
|
506
|
+
return Buffer.from(await gzipTools.compress(uncompressed));
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
/**
|
|
@@ -105,7 +105,7 @@ export class UpstreamCache {
|
|
|
105
105
|
|
|
106
106
|
// If not in memory and we have storage, check S3
|
|
107
107
|
if (!entry && this.storage) {
|
|
108
|
-
entry = await this.loadFromStorage(key);
|
|
108
|
+
entry = (await this.loadFromStorage(key)) ?? undefined;
|
|
109
109
|
if (entry) {
|
|
110
110
|
// Promote to memory cache
|
|
111
111
|
this.memoryCache.set(key, entry);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TRegistryProtocol } from '../core/interfaces.core.js';
|
|
1
|
+
import type { TRegistryProtocol, IRequestActor } from '../core/interfaces.core.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Scope rule for routing requests to specific upstreams.
|
|
@@ -86,6 +86,8 @@ export interface IUpstreamRegistryConfig {
|
|
|
86
86
|
cache?: Partial<IUpstreamCacheConfig>;
|
|
87
87
|
/** Resilience configuration overrides */
|
|
88
88
|
resilience?: Partial<IUpstreamResilienceConfig>;
|
|
89
|
+
/** API path prefix for OCI registries (default: /v2). Useful for registries behind reverse proxies. */
|
|
90
|
+
apiPrefix?: string;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -146,6 +148,8 @@ export interface IUpstreamFetchContext {
|
|
|
146
148
|
headers: Record<string, string>;
|
|
147
149
|
/** Query parameters */
|
|
148
150
|
query: Record<string, string>;
|
|
151
|
+
/** Actor performing the request (for cache key isolation) */
|
|
152
|
+
actor?: IRequestActor;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -193,3 +197,80 @@ export const DEFAULT_RESILIENCE_CONFIG: IUpstreamResilienceConfig = {
|
|
|
193
197
|
circuitBreakerThreshold: 5,
|
|
194
198
|
circuitBreakerResetMs: 30000,
|
|
195
199
|
};
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Upstream Provider Interfaces
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Context for resolving upstream configuration.
|
|
207
|
+
* Passed to IUpstreamProvider per-request to enable dynamic upstream routing.
|
|
208
|
+
*/
|
|
209
|
+
export interface IUpstreamResolutionContext {
|
|
210
|
+
/** Protocol being accessed */
|
|
211
|
+
protocol: TRegistryProtocol;
|
|
212
|
+
/** Resource identifier (package name, repository, coordinates, etc.) */
|
|
213
|
+
resource: string;
|
|
214
|
+
/** Extracted scope (e.g., "company" from "@company/pkg", "myorg" from "myorg/image") */
|
|
215
|
+
scope: string | null;
|
|
216
|
+
/** Actor performing the request */
|
|
217
|
+
actor?: IRequestActor;
|
|
218
|
+
/** HTTP method */
|
|
219
|
+
method: string;
|
|
220
|
+
/** Resource type (packument, tarball, manifest, blob, etc.) */
|
|
221
|
+
resourceType: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Dynamic upstream configuration provider.
|
|
226
|
+
* Implement this interface to provide per-request upstream routing
|
|
227
|
+
* based on actor context (user, organization, etc.)
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* class OrgUpstreamProvider implements IUpstreamProvider {
|
|
232
|
+
* constructor(private db: Database) {}
|
|
233
|
+
*
|
|
234
|
+
* async resolveUpstreamConfig(ctx: IUpstreamResolutionContext) {
|
|
235
|
+
* if (ctx.actor?.orgId) {
|
|
236
|
+
* const orgConfig = await this.db.getOrgUpstream(ctx.actor.orgId, ctx.protocol);
|
|
237
|
+
* if (orgConfig) return orgConfig;
|
|
238
|
+
* }
|
|
239
|
+
* return this.db.getDefaultUpstream(ctx.protocol);
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export interface IUpstreamProvider {
|
|
245
|
+
/** Optional initialization */
|
|
246
|
+
init?(): Promise<void>;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Resolve upstream configuration for a request.
|
|
250
|
+
* @param context - Information about the current request
|
|
251
|
+
* @returns Upstream config to use, or null to skip upstream lookup
|
|
252
|
+
*/
|
|
253
|
+
resolveUpstreamConfig(context: IUpstreamResolutionContext): Promise<IProtocolUpstreamConfig | null>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Static upstream provider for simple configurations.
|
|
258
|
+
* Use this when you have fixed upstream registries that don't change per-request.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const provider = new StaticUpstreamProvider({
|
|
263
|
+
* npm: {
|
|
264
|
+
* enabled: true,
|
|
265
|
+
* upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true, auth: { type: 'none' } }],
|
|
266
|
+
* },
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export class StaticUpstreamProvider implements IUpstreamProvider {
|
|
271
|
+
constructor(private configs: Partial<Record<TRegistryProtocol, IProtocolUpstreamConfig>>) {}
|
|
272
|
+
|
|
273
|
+
async resolveUpstreamConfig(ctx: IUpstreamResolutionContext): Promise<IProtocolUpstreamConfig | null> {
|
|
274
|
+
return this.configs[ctx.protocol] ?? null;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/npmextra.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"gitzone": {
|
|
3
|
-
"projectType": "npm",
|
|
4
|
-
"module": {
|
|
5
|
-
"githost": "code.foss.global",
|
|
6
|
-
"gitscope": "push.rocks",
|
|
7
|
-
"gitrepo": "smartregistry",
|
|
8
|
-
"description": "a registry for npm modules and oci images",
|
|
9
|
-
"npmPackagename": "@push.rocks/smartregistry",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"projectDomain": "push.rocks"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"npmci": {
|
|
15
|
-
"npmGlobalTools": [],
|
|
16
|
-
"npmAccessLevel": "public"
|
|
17
|
-
}
|
|
18
|
-
}
|