@push.rocks/smartregistry 2.6.0 → 2.8.1
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 +121 -48
- package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
- package/dist_ts/oci/classes.ociregistry.js +187 -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 +9 -11
- 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 +134 -49
- package/ts/oci/classes.ociregistry.ts +206 -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 +8 -10
- package/ts/upstream/classes.upstreamcache.ts +1 -1
- package/ts/upstream/interfaces.upstream.ts +82 -1
- package/npmextra.json +0 -18
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartregistry',
|
|
6
|
-
version: '2.
|
|
6
|
+
version: '2.8.1',
|
|
7
7
|
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
|
8
8
|
}
|
|
@@ -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
|
ICargoIndexEntry,
|
|
9
9
|
ICargoPublishMetadata,
|
|
@@ -27,20 +27,21 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
27
27
|
private basePath: string = '/cargo';
|
|
28
28
|
private registryUrl: string;
|
|
29
29
|
private logger: Smartlog;
|
|
30
|
-
private
|
|
30
|
+
private upstreamProvider: IUpstreamProvider | null = null;
|
|
31
31
|
|
|
32
32
|
constructor(
|
|
33
33
|
storage: RegistryStorage,
|
|
34
34
|
authManager: AuthManager,
|
|
35
35
|
basePath: string = '/cargo',
|
|
36
36
|
registryUrl: string = 'http://localhost:5000/cargo',
|
|
37
|
-
|
|
37
|
+
upstreamProvider?: IUpstreamProvider
|
|
38
38
|
) {
|
|
39
39
|
super();
|
|
40
40
|
this.storage = storage;
|
|
41
41
|
this.authManager = authManager;
|
|
42
42
|
this.basePath = basePath;
|
|
43
43
|
this.registryUrl = registryUrl;
|
|
44
|
+
this.upstreamProvider = upstreamProvider || null;
|
|
44
45
|
|
|
45
46
|
// Initialize logger
|
|
46
47
|
this.logger = new Smartlog({
|
|
@@ -54,20 +55,38 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
this.logger.enableConsole();
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Get upstream for a specific request.
|
|
62
|
+
* Calls the provider to resolve upstream config dynamically.
|
|
63
|
+
*/
|
|
64
|
+
private async getUpstreamForRequest(
|
|
65
|
+
resource: string,
|
|
66
|
+
resourceType: string,
|
|
67
|
+
method: string,
|
|
68
|
+
actor?: IRequestActor
|
|
69
|
+
): Promise<CargoUpstream | null> {
|
|
70
|
+
if (!this.upstreamProvider) return null;
|
|
71
|
+
|
|
72
|
+
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
|
73
|
+
protocol: 'cargo',
|
|
74
|
+
resource,
|
|
75
|
+
scope: resource, // For Cargo, crate name is the scope
|
|
76
|
+
actor,
|
|
77
|
+
method,
|
|
78
|
+
resourceType,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!config?.enabled) return null;
|
|
82
|
+
return new CargoUpstream(config, undefined, this.logger);
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
/**
|
|
65
86
|
* Clean up resources (timers, connections, etc.)
|
|
66
87
|
*/
|
|
67
88
|
public destroy(): void {
|
|
68
|
-
|
|
69
|
-
this.upstream.stop();
|
|
70
|
-
}
|
|
89
|
+
// No persistent upstream to clean up with dynamic provider
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
public async init(): Promise<void> {
|
|
@@ -94,6 +113,14 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
94
113
|
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
|
|
95
114
|
const token = authHeader ? await this.authManager.validateToken(authHeader, 'cargo') : null;
|
|
96
115
|
|
|
116
|
+
// Build actor from context and validated token
|
|
117
|
+
const actor: IRequestActor = {
|
|
118
|
+
...context.actor,
|
|
119
|
+
userId: token?.userId,
|
|
120
|
+
ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
|
|
121
|
+
userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
|
|
122
|
+
};
|
|
123
|
+
|
|
97
124
|
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
|
98
125
|
method: context.method,
|
|
99
126
|
path,
|
|
@@ -107,11 +134,11 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
107
134
|
|
|
108
135
|
// API endpoints
|
|
109
136
|
if (path.startsWith('/api/v1/')) {
|
|
110
|
-
return this.handleApiRequest(path, context, token);
|
|
137
|
+
return this.handleApiRequest(path, context, token, actor);
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
// Index files (sparse protocol)
|
|
114
|
-
return this.handleIndexRequest(path);
|
|
141
|
+
return this.handleIndexRequest(path, actor);
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
/**
|
|
@@ -132,7 +159,8 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
132
159
|
private async handleApiRequest(
|
|
133
160
|
path: string,
|
|
134
161
|
context: IRequestContext,
|
|
135
|
-
token: IAuthToken | null
|
|
162
|
+
token: IAuthToken | null,
|
|
163
|
+
actor?: IRequestActor
|
|
136
164
|
): Promise<IResponse> {
|
|
137
165
|
// Publish: PUT /api/v1/crates/new
|
|
138
166
|
if (path === '/api/v1/crates/new' && context.method === 'PUT') {
|
|
@@ -142,7 +170,7 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
142
170
|
// Download: GET /api/v1/crates/{crate}/{version}/download
|
|
143
171
|
const downloadMatch = path.match(/^\/api\/v1\/crates\/([^\/]+)\/([^\/]+)\/download$/);
|
|
144
172
|
if (downloadMatch && context.method === 'GET') {
|
|
145
|
-
return this.handleDownload(downloadMatch[1], downloadMatch[2]);
|
|
173
|
+
return this.handleDownload(downloadMatch[1], downloadMatch[2], actor);
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
// Yank: DELETE /api/v1/crates/{crate}/{version}/yank
|
|
@@ -175,7 +203,7 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
175
203
|
* Handle index file requests
|
|
176
204
|
* Paths: /1/{name}, /2/{name}, /3/{c}/{name}, /{p1}/{p2}/{name}
|
|
177
205
|
*/
|
|
178
|
-
private async handleIndexRequest(path: string): Promise<IResponse> {
|
|
206
|
+
private async handleIndexRequest(path: string, actor?: IRequestActor): Promise<IResponse> {
|
|
179
207
|
// Parse index paths to extract crate name
|
|
180
208
|
const pathParts = path.split('/').filter(p => p);
|
|
181
209
|
let crateName: string | null = null;
|
|
@@ -202,7 +230,7 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
202
230
|
};
|
|
203
231
|
}
|
|
204
232
|
|
|
205
|
-
return this.handleIndexFile(crateName);
|
|
233
|
+
return this.handleIndexFile(crateName, actor);
|
|
206
234
|
}
|
|
207
235
|
|
|
208
236
|
/**
|
|
@@ -224,23 +252,26 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
224
252
|
/**
|
|
225
253
|
* Serve index file for a crate
|
|
226
254
|
*/
|
|
227
|
-
private async handleIndexFile(crateName: string): Promise<IResponse> {
|
|
255
|
+
private async handleIndexFile(crateName: string, actor?: IRequestActor): Promise<IResponse> {
|
|
228
256
|
let index = await this.storage.getCargoIndex(crateName);
|
|
229
257
|
|
|
230
258
|
// Try upstream if not found locally
|
|
231
|
-
if (
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
259
|
+
if (!index || index.length === 0) {
|
|
260
|
+
const upstream = await this.getUpstreamForRequest(crateName, 'index', 'GET', actor);
|
|
261
|
+
if (upstream) {
|
|
262
|
+
const upstreamIndex = await upstream.fetchCrateIndex(crateName);
|
|
263
|
+
if (upstreamIndex) {
|
|
264
|
+
// Parse the newline-delimited JSON
|
|
265
|
+
const parsedIndex: ICargoIndexEntry[] = upstreamIndex
|
|
266
|
+
.split('\n')
|
|
267
|
+
.filter(line => line.trim())
|
|
268
|
+
.map(line => JSON.parse(line));
|
|
269
|
+
|
|
270
|
+
if (parsedIndex.length > 0) {
|
|
271
|
+
// Cache locally
|
|
272
|
+
await this.storage.putCargoIndex(crateName, parsedIndex);
|
|
273
|
+
index = parsedIndex;
|
|
274
|
+
}
|
|
244
275
|
}
|
|
245
276
|
}
|
|
246
277
|
}
|
|
@@ -339,7 +370,7 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
339
370
|
const parsed = this.parsePublishRequest(body);
|
|
340
371
|
metadata = parsed.metadata;
|
|
341
372
|
crateFile = parsed.crateFile;
|
|
342
|
-
} catch (error) {
|
|
373
|
+
} catch (error: any) {
|
|
343
374
|
this.logger.log('error', 'handlePublish: parse error', { error: error.message });
|
|
344
375
|
return {
|
|
345
376
|
status: 400,
|
|
@@ -431,15 +462,31 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
431
462
|
*/
|
|
432
463
|
private async handleDownload(
|
|
433
464
|
crateName: string,
|
|
434
|
-
version: string
|
|
465
|
+
version: string,
|
|
466
|
+
actor?: IRequestActor
|
|
435
467
|
): Promise<IResponse> {
|
|
436
468
|
this.logger.log('debug', 'handleDownload', { crate: crateName, version });
|
|
437
469
|
|
|
438
|
-
|
|
470
|
+
// Try streaming from local storage first
|
|
471
|
+
const streamResult = await this.storage.getCargoCrateStream(crateName, version);
|
|
472
|
+
|
|
473
|
+
if (streamResult) {
|
|
474
|
+
return {
|
|
475
|
+
status: 200,
|
|
476
|
+
headers: {
|
|
477
|
+
'Content-Type': 'application/gzip',
|
|
478
|
+
'Content-Length': streamResult.size.toString(),
|
|
479
|
+
'Content-Disposition': `attachment; filename="${crateName}-${version}.crate"`,
|
|
480
|
+
},
|
|
481
|
+
body: streamResult.stream,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
439
484
|
|
|
440
485
|
// Try upstream if not found locally
|
|
441
|
-
|
|
442
|
-
|
|
486
|
+
let crateFile: Buffer | null = null;
|
|
487
|
+
const upstream = await this.getUpstreamForRequest(crateName, 'crate', 'GET', actor);
|
|
488
|
+
if (upstream) {
|
|
489
|
+
crateFile = await upstream.fetchCrate(crateName, version);
|
|
443
490
|
if (crateFile) {
|
|
444
491
|
// Cache locally
|
|
445
492
|
await this.storage.putCargoCrate(crateName, version, crateFile);
|
|
@@ -612,7 +659,7 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
612
659
|
}
|
|
613
660
|
}
|
|
614
661
|
}
|
|
615
|
-
} catch (error) {
|
|
662
|
+
} catch (error: any) {
|
|
616
663
|
this.logger.log('error', 'handleSearch: error', { error: error.message });
|
|
617
664
|
}
|
|
618
665
|
|
|
@@ -2,6 +2,7 @@ import { RegistryStorage } from './core/classes.registrystorage.js';
|
|
|
2
2
|
import { AuthManager } from './core/classes.authmanager.js';
|
|
3
3
|
import { BaseRegistry } from './core/classes.baseregistry.js';
|
|
4
4
|
import type { IRegistryConfig, IRequestContext, IResponse } from './core/interfaces.core.js';
|
|
5
|
+
import { toReadableStream } from './core/helpers.stream.js';
|
|
5
6
|
import { OciRegistry } from './oci/classes.ociregistry.js';
|
|
6
7
|
import { NpmRegistry } from './npm/classes.npmregistry.js';
|
|
7
8
|
import { MavenRegistry } from './maven/classes.mavenregistry.js';
|
|
@@ -86,7 +87,7 @@ export class SmartRegistry {
|
|
|
86
87
|
this.authManager,
|
|
87
88
|
ociBasePath,
|
|
88
89
|
ociTokens,
|
|
89
|
-
this.config.
|
|
90
|
+
this.config.upstreamProvider
|
|
90
91
|
);
|
|
91
92
|
await ociRegistry.init();
|
|
92
93
|
this.registries.set('oci', ociRegistry);
|
|
@@ -95,13 +96,13 @@ export class SmartRegistry {
|
|
|
95
96
|
// Initialize NPM registry if enabled
|
|
96
97
|
if (this.config.npm?.enabled) {
|
|
97
98
|
const npmBasePath = this.config.npm.basePath ?? '/npm';
|
|
98
|
-
const registryUrl = `http://localhost:5000${npmBasePath}`;
|
|
99
|
+
const registryUrl = this.config.npm.registryUrl ?? `http://localhost:5000${npmBasePath}`;
|
|
99
100
|
const npmRegistry = new NpmRegistry(
|
|
100
101
|
this.storage,
|
|
101
102
|
this.authManager,
|
|
102
103
|
npmBasePath,
|
|
103
104
|
registryUrl,
|
|
104
|
-
this.config.
|
|
105
|
+
this.config.upstreamProvider
|
|
105
106
|
);
|
|
106
107
|
await npmRegistry.init();
|
|
107
108
|
this.registries.set('npm', npmRegistry);
|
|
@@ -110,13 +111,13 @@ export class SmartRegistry {
|
|
|
110
111
|
// Initialize Maven registry if enabled
|
|
111
112
|
if (this.config.maven?.enabled) {
|
|
112
113
|
const mavenBasePath = this.config.maven.basePath ?? '/maven';
|
|
113
|
-
const registryUrl = `http://localhost:5000${mavenBasePath}`;
|
|
114
|
+
const registryUrl = this.config.maven.registryUrl ?? `http://localhost:5000${mavenBasePath}`;
|
|
114
115
|
const mavenRegistry = new MavenRegistry(
|
|
115
116
|
this.storage,
|
|
116
117
|
this.authManager,
|
|
117
118
|
mavenBasePath,
|
|
118
119
|
registryUrl,
|
|
119
|
-
this.config.
|
|
120
|
+
this.config.upstreamProvider
|
|
120
121
|
);
|
|
121
122
|
await mavenRegistry.init();
|
|
122
123
|
this.registries.set('maven', mavenRegistry);
|
|
@@ -125,13 +126,13 @@ export class SmartRegistry {
|
|
|
125
126
|
// Initialize Cargo registry if enabled
|
|
126
127
|
if (this.config.cargo?.enabled) {
|
|
127
128
|
const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
|
|
128
|
-
const registryUrl = `http://localhost:5000${cargoBasePath}`;
|
|
129
|
+
const registryUrl = this.config.cargo.registryUrl ?? `http://localhost:5000${cargoBasePath}`;
|
|
129
130
|
const cargoRegistry = new CargoRegistry(
|
|
130
131
|
this.storage,
|
|
131
132
|
this.authManager,
|
|
132
133
|
cargoBasePath,
|
|
133
134
|
registryUrl,
|
|
134
|
-
this.config.
|
|
135
|
+
this.config.upstreamProvider
|
|
135
136
|
);
|
|
136
137
|
await cargoRegistry.init();
|
|
137
138
|
this.registries.set('cargo', cargoRegistry);
|
|
@@ -140,13 +141,13 @@ export class SmartRegistry {
|
|
|
140
141
|
// Initialize Composer registry if enabled
|
|
141
142
|
if (this.config.composer?.enabled) {
|
|
142
143
|
const composerBasePath = this.config.composer.basePath ?? '/composer';
|
|
143
|
-
const registryUrl = `http://localhost:5000${composerBasePath}`;
|
|
144
|
+
const registryUrl = this.config.composer.registryUrl ?? `http://localhost:5000${composerBasePath}`;
|
|
144
145
|
const composerRegistry = new ComposerRegistry(
|
|
145
146
|
this.storage,
|
|
146
147
|
this.authManager,
|
|
147
148
|
composerBasePath,
|
|
148
149
|
registryUrl,
|
|
149
|
-
this.config.
|
|
150
|
+
this.config.upstreamProvider
|
|
150
151
|
);
|
|
151
152
|
await composerRegistry.init();
|
|
152
153
|
this.registries.set('composer', composerRegistry);
|
|
@@ -155,13 +156,13 @@ export class SmartRegistry {
|
|
|
155
156
|
// Initialize PyPI registry if enabled
|
|
156
157
|
if (this.config.pypi?.enabled) {
|
|
157
158
|
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
158
|
-
const registryUrl = `http://localhost:5000`;
|
|
159
|
+
const registryUrl = this.config.pypi.registryUrl ?? `http://localhost:5000`;
|
|
159
160
|
const pypiRegistry = new PypiRegistry(
|
|
160
161
|
this.storage,
|
|
161
162
|
this.authManager,
|
|
162
163
|
pypiBasePath,
|
|
163
164
|
registryUrl,
|
|
164
|
-
this.config.
|
|
165
|
+
this.config.upstreamProvider
|
|
165
166
|
);
|
|
166
167
|
await pypiRegistry.init();
|
|
167
168
|
this.registries.set('pypi', pypiRegistry);
|
|
@@ -170,13 +171,13 @@ export class SmartRegistry {
|
|
|
170
171
|
// Initialize RubyGems registry if enabled
|
|
171
172
|
if (this.config.rubygems?.enabled) {
|
|
172
173
|
const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
|
|
173
|
-
const registryUrl = `http://localhost:5000${rubygemsBasePath}`;
|
|
174
|
+
const registryUrl = this.config.rubygems.registryUrl ?? `http://localhost:5000${rubygemsBasePath}`;
|
|
174
175
|
const rubygemsRegistry = new RubyGemsRegistry(
|
|
175
176
|
this.storage,
|
|
176
177
|
this.authManager,
|
|
177
178
|
rubygemsBasePath,
|
|
178
179
|
registryUrl,
|
|
179
|
-
this.config.
|
|
180
|
+
this.config.upstreamProvider
|
|
180
181
|
);
|
|
181
182
|
await rubygemsRegistry.init();
|
|
182
183
|
this.registries.set('rubygems', rubygemsRegistry);
|
|
@@ -191,75 +192,88 @@ export class SmartRegistry {
|
|
|
191
192
|
*/
|
|
192
193
|
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
|
193
194
|
const path = context.path;
|
|
195
|
+
let response: IResponse | undefined;
|
|
194
196
|
|
|
195
197
|
// Route to OCI registry
|
|
196
|
-
if (this.config.oci?.enabled && path.startsWith(this.config.oci.basePath)) {
|
|
198
|
+
if (!response && this.config.oci?.enabled && path.startsWith(this.config.oci.basePath)) {
|
|
197
199
|
const ociRegistry = this.registries.get('oci');
|
|
198
200
|
if (ociRegistry) {
|
|
199
|
-
|
|
201
|
+
response = await ociRegistry.handleRequest(context);
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
// Route to NPM registry
|
|
204
|
-
if (this.config.npm?.enabled && path.startsWith(this.config.npm.basePath)) {
|
|
206
|
+
if (!response && this.config.npm?.enabled && path.startsWith(this.config.npm.basePath)) {
|
|
205
207
|
const npmRegistry = this.registries.get('npm');
|
|
206
208
|
if (npmRegistry) {
|
|
207
|
-
|
|
209
|
+
response = await npmRegistry.handleRequest(context);
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
// Route to Maven registry
|
|
212
|
-
if (this.config.maven?.enabled && path.startsWith(this.config.maven.basePath)) {
|
|
214
|
+
if (!response && this.config.maven?.enabled && path.startsWith(this.config.maven.basePath)) {
|
|
213
215
|
const mavenRegistry = this.registries.get('maven');
|
|
214
216
|
if (mavenRegistry) {
|
|
215
|
-
|
|
217
|
+
response = await mavenRegistry.handleRequest(context);
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
// Route to Cargo registry
|
|
220
|
-
if (this.config.cargo?.enabled && path.startsWith(this.config.cargo.basePath)) {
|
|
222
|
+
if (!response && this.config.cargo?.enabled && path.startsWith(this.config.cargo.basePath)) {
|
|
221
223
|
const cargoRegistry = this.registries.get('cargo');
|
|
222
224
|
if (cargoRegistry) {
|
|
223
|
-
|
|
225
|
+
response = await cargoRegistry.handleRequest(context);
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
// Route to Composer registry
|
|
228
|
-
if (this.config.composer?.enabled && path.startsWith(this.config.composer.basePath)) {
|
|
230
|
+
if (!response && this.config.composer?.enabled && path.startsWith(this.config.composer.basePath)) {
|
|
229
231
|
const composerRegistry = this.registries.get('composer');
|
|
230
232
|
if (composerRegistry) {
|
|
231
|
-
|
|
233
|
+
response = await composerRegistry.handleRequest(context);
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
// Route to PyPI registry (also handles /simple prefix)
|
|
236
|
-
if (this.config.pypi?.enabled) {
|
|
238
|
+
if (!response && this.config.pypi?.enabled) {
|
|
237
239
|
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
238
240
|
if (path.startsWith(pypiBasePath) || path.startsWith('/simple')) {
|
|
239
241
|
const pypiRegistry = this.registries.get('pypi');
|
|
240
242
|
if (pypiRegistry) {
|
|
241
|
-
|
|
243
|
+
response = await pypiRegistry.handleRequest(context);
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
// Route to RubyGems registry
|
|
247
|
-
if (this.config.rubygems?.enabled && path.startsWith(this.config.rubygems.basePath)) {
|
|
249
|
+
if (!response && this.config.rubygems?.enabled && path.startsWith(this.config.rubygems.basePath)) {
|
|
248
250
|
const rubygemsRegistry = this.registries.get('rubygems');
|
|
249
251
|
if (rubygemsRegistry) {
|
|
250
|
-
|
|
252
|
+
response = await rubygemsRegistry.handleRequest(context);
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
// No matching registry
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
if (!response) {
|
|
258
|
+
response = {
|
|
259
|
+
status: 404,
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
body: {
|
|
262
|
+
error: 'NOT_FOUND',
|
|
263
|
+
message: 'No registry handler for this path',
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Normalize body to ReadableStream<Uint8Array> at the API boundary
|
|
269
|
+
if (response.body != null && !(response.body instanceof ReadableStream)) {
|
|
270
|
+
if (!Buffer.isBuffer(response.body) && typeof response.body === 'object' && !(response.body instanceof Uint8Array)) {
|
|
271
|
+
response.headers['Content-Type'] ??= 'application/json';
|
|
272
|
+
}
|
|
273
|
+
response.body = toReadableStream(response.body);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return response;
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
/**
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { BaseRegistry } from '../core/classes.baseregistry.js';
|
|
7
7
|
import type { RegistryStorage } from '../core/classes.registrystorage.js';
|
|
8
8
|
import type { AuthManager } from '../core/classes.authmanager.js';
|
|
9
|
-
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
10
|
-
import type {
|
|
9
|
+
import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
|
|
10
|
+
import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
|
|
11
11
|
import { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
|
12
12
|
import type {
|
|
13
13
|
IComposerPackage,
|
|
@@ -30,34 +30,66 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
30
30
|
private authManager: AuthManager;
|
|
31
31
|
private basePath: string = '/composer';
|
|
32
32
|
private registryUrl: string;
|
|
33
|
-
private
|
|
33
|
+
private upstreamProvider: IUpstreamProvider | null = null;
|
|
34
34
|
|
|
35
35
|
constructor(
|
|
36
36
|
storage: RegistryStorage,
|
|
37
37
|
authManager: AuthManager,
|
|
38
38
|
basePath: string = '/composer',
|
|
39
39
|
registryUrl: string = 'http://localhost:5000/composer',
|
|
40
|
-
|
|
40
|
+
upstreamProvider?: IUpstreamProvider
|
|
41
41
|
) {
|
|
42
42
|
super();
|
|
43
43
|
this.storage = storage;
|
|
44
44
|
this.authManager = authManager;
|
|
45
45
|
this.basePath = basePath;
|
|
46
46
|
this.registryUrl = registryUrl;
|
|
47
|
+
this.upstreamProvider = upstreamProvider || null;
|
|
48
|
+
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Extract scope from Composer package name.
|
|
52
|
+
* For Composer, vendor is the scope.
|
|
53
|
+
* @example "symfony" from "symfony/console"
|
|
54
|
+
*/
|
|
55
|
+
private extractScope(vendorPackage: string): string | null {
|
|
56
|
+
const slashIndex = vendorPackage.indexOf('/');
|
|
57
|
+
if (slashIndex > 0) {
|
|
58
|
+
return vendorPackage.substring(0, slashIndex);
|
|
51
59
|
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get upstream for a specific request.
|
|
65
|
+
* Calls the provider to resolve upstream config dynamically.
|
|
66
|
+
*/
|
|
67
|
+
private async getUpstreamForRequest(
|
|
68
|
+
resource: string,
|
|
69
|
+
resourceType: string,
|
|
70
|
+
method: string,
|
|
71
|
+
actor?: IRequestActor
|
|
72
|
+
): Promise<ComposerUpstream | null> {
|
|
73
|
+
if (!this.upstreamProvider) return null;
|
|
74
|
+
|
|
75
|
+
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
|
76
|
+
protocol: 'composer',
|
|
77
|
+
resource,
|
|
78
|
+
scope: this.extractScope(resource),
|
|
79
|
+
actor,
|
|
80
|
+
method,
|
|
81
|
+
resourceType,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!config?.enabled) return null;
|
|
85
|
+
return new ComposerUpstream(config);
|
|
52
86
|
}
|
|
53
87
|
|
|
54
88
|
/**
|
|
55
89
|
* Clean up resources (timers, connections, etc.)
|
|
56
90
|
*/
|
|
57
91
|
public destroy(): void {
|
|
58
|
-
|
|
59
|
-
this.upstream.stop();
|
|
60
|
-
}
|
|
92
|
+
// No persistent upstream to clean up with dynamic provider
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
public async init(): Promise<void> {
|
|
@@ -96,6 +128,14 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
// Build actor from context and validated token
|
|
132
|
+
const actor: IRequestActor = {
|
|
133
|
+
...context.actor,
|
|
134
|
+
userId: token?.userId,
|
|
135
|
+
ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
|
|
136
|
+
userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
|
|
137
|
+
};
|
|
138
|
+
|
|
99
139
|
// Root packages.json
|
|
100
140
|
if (path === '/packages.json' || path === '' || path === '/') {
|
|
101
141
|
return this.handlePackagesJson();
|
|
@@ -106,7 +146,7 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
106
146
|
if (metadataMatch) {
|
|
107
147
|
const [, vendorPackage, devSuffix] = metadataMatch;
|
|
108
148
|
const includeDev = !!devSuffix;
|
|
109
|
-
return this.handlePackageMetadata(vendorPackage, includeDev, token);
|
|
149
|
+
return this.handlePackageMetadata(vendorPackage, includeDev, token, actor);
|
|
110
150
|
}
|
|
111
151
|
|
|
112
152
|
// Package list: /packages/list.json?filter=vendor/*
|
|
@@ -176,26 +216,30 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
176
216
|
private async handlePackageMetadata(
|
|
177
217
|
vendorPackage: string,
|
|
178
218
|
includeDev: boolean,
|
|
179
|
-
token: IAuthToken | null
|
|
219
|
+
token: IAuthToken | null,
|
|
220
|
+
actor?: IRequestActor
|
|
180
221
|
): Promise<IResponse> {
|
|
181
222
|
// Read operations are public, no authentication required
|
|
182
223
|
let metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
|
183
224
|
|
|
184
225
|
// Try upstream if not found locally
|
|
185
|
-
if (!metadata
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
if (!metadata) {
|
|
227
|
+
const upstream = await this.getUpstreamForRequest(vendorPackage, 'metadata', 'GET', actor);
|
|
228
|
+
if (upstream) {
|
|
229
|
+
const [vendor, packageName] = vendorPackage.split('/');
|
|
230
|
+
if (vendor && packageName) {
|
|
231
|
+
const upstreamMetadata = includeDev
|
|
232
|
+
? await upstream.fetchPackageDevMetadata(vendor, packageName)
|
|
233
|
+
: await upstream.fetchPackageMetadata(vendor, packageName);
|
|
234
|
+
|
|
235
|
+
if (upstreamMetadata && upstreamMetadata.packages) {
|
|
236
|
+
// Store upstream metadata locally
|
|
237
|
+
metadata = {
|
|
238
|
+
packages: upstreamMetadata.packages,
|
|
239
|
+
lastModified: new Date().toUTCString(),
|
|
240
|
+
};
|
|
241
|
+
await this.storage.putComposerPackageMetadata(vendorPackage, metadata);
|
|
242
|
+
}
|
|
199
243
|
}
|
|
200
244
|
}
|
|
201
245
|
}
|
|
@@ -258,9 +302,9 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
258
302
|
token: IAuthToken | null
|
|
259
303
|
): Promise<IResponse> {
|
|
260
304
|
// Read operations are public, no authentication required
|
|
261
|
-
const
|
|
305
|
+
const streamResult = await this.storage.getComposerPackageZipStream(vendorPackage, reference);
|
|
262
306
|
|
|
263
|
-
if (!
|
|
307
|
+
if (!streamResult) {
|
|
264
308
|
return {
|
|
265
309
|
status: 404,
|
|
266
310
|
headers: {},
|
|
@@ -272,10 +316,10 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
272
316
|
status: 200,
|
|
273
317
|
headers: {
|
|
274
318
|
'Content-Type': 'application/zip',
|
|
275
|
-
'Content-Length':
|
|
319
|
+
'Content-Length': streamResult.size.toString(),
|
|
276
320
|
'Content-Disposition': `attachment; filename="${reference}.zip"`,
|
|
277
321
|
},
|
|
278
|
-
body:
|
|
322
|
+
body: streamResult.stream,
|
|
279
323
|
};
|
|
280
324
|
}
|
|
281
325
|
|