@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.
Files changed (52) hide show
  1. package/.smartconfig.json +24 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/cargo/classes.cargoregistry.d.ts +8 -3
  4. package/dist_ts/cargo/classes.cargoregistry.js +71 -33
  5. package/dist_ts/classes.smartregistry.js +48 -36
  6. package/dist_ts/composer/classes.composerregistry.d.ts +14 -3
  7. package/dist_ts/composer/classes.composerregistry.js +64 -28
  8. package/dist_ts/core/classes.registrystorage.d.ts +45 -0
  9. package/dist_ts/core/classes.registrystorage.js +116 -1
  10. package/dist_ts/core/helpers.stream.d.ts +20 -0
  11. package/dist_ts/core/helpers.stream.js +59 -0
  12. package/dist_ts/core/index.d.ts +1 -0
  13. package/dist_ts/core/index.js +3 -1
  14. package/dist_ts/core/interfaces.core.d.ts +28 -5
  15. package/dist_ts/maven/classes.mavenregistry.d.ts +14 -3
  16. package/dist_ts/maven/classes.mavenregistry.js +78 -27
  17. package/dist_ts/npm/classes.npmregistry.d.ts +14 -3
  18. package/dist_ts/npm/classes.npmregistry.js +104 -48
  19. package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
  20. package/dist_ts/oci/classes.ociregistry.js +186 -73
  21. package/dist_ts/oci/classes.ociupstream.d.ts +5 -2
  22. package/dist_ts/oci/classes.ociupstream.js +17 -10
  23. package/dist_ts/oci/interfaces.oci.d.ts +4 -0
  24. package/dist_ts/pypi/classes.pypiregistry.d.ts +8 -3
  25. package/dist_ts/pypi/classes.pypiregistry.js +88 -50
  26. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +8 -3
  27. package/dist_ts/rubygems/classes.rubygemsregistry.js +61 -23
  28. package/dist_ts/rubygems/helpers.rubygems.js +3 -3
  29. package/dist_ts/upstream/classes.upstreamcache.js +2 -2
  30. package/dist_ts/upstream/interfaces.upstream.d.ts +72 -1
  31. package/dist_ts/upstream/interfaces.upstream.js +24 -1
  32. package/package.json +24 -20
  33. package/readme.md +354 -812
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/cargo/classes.cargoregistry.ts +84 -37
  36. package/ts/classes.smartregistry.ts +49 -35
  37. package/ts/composer/classes.composerregistry.ts +74 -30
  38. package/ts/core/classes.registrystorage.ts +133 -2
  39. package/ts/core/helpers.stream.ts +63 -0
  40. package/ts/core/index.ts +3 -0
  41. package/ts/core/interfaces.core.ts +29 -5
  42. package/ts/maven/classes.mavenregistry.ts +89 -28
  43. package/ts/npm/classes.npmregistry.ts +118 -49
  44. package/ts/oci/classes.ociregistry.ts +205 -77
  45. package/ts/oci/classes.ociupstream.ts +18 -8
  46. package/ts/oci/interfaces.oci.ts +4 -0
  47. package/ts/pypi/classes.pypiregistry.ts +100 -54
  48. package/ts/rubygems/classes.rubygemsregistry.ts +69 -24
  49. package/ts/rubygems/helpers.rubygems.ts +2 -2
  50. package/ts/upstream/classes.upstreamcache.ts +1 -1
  51. package/ts/upstream/interfaces.upstream.ts +82 -1
  52. package/npmextra.json +0 -18
@@ -34,8 +34,8 @@ import type {
34
34
  * ```
35
35
  */
36
36
  export class RegistryStorage implements IStorageBackend {
37
- private smartBucket: plugins.smartbucket.SmartBucket;
38
- private bucket: plugins.smartbucket.Bucket;
37
+ private smartBucket!: plugins.smartbucket.SmartBucket;
38
+ private bucket!: plugins.smartbucket.Bucket;
39
39
  private bucketName: string;
40
40
  private hooks?: IStorageHooks;
41
41
 
@@ -1266,4 +1266,135 @@ export class RegistryStorage implements IStorageBackend {
1266
1266
  private getRubyGemsMetadataPath(gemName: string): string {
1267
1267
  return `rubygems/metadata/${gemName}/metadata.json`;
1268
1268
  }
1269
+
1270
+ // ========================================================================
1271
+ // STREAMING METHODS (Web Streams API)
1272
+ // ========================================================================
1273
+
1274
+ /**
1275
+ * Get an object as a ReadableStream. Returns null if not found.
1276
+ */
1277
+ public async getObjectStream(key: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1278
+ try {
1279
+ const stat = await this.bucket.fastStat({ path: key });
1280
+ const size = stat.ContentLength ?? 0;
1281
+ const stream = await this.bucket.fastGetStream({ path: key }, 'webstream');
1282
+
1283
+ // Call afterGet hook (non-blocking)
1284
+ if (this.hooks?.afterGet) {
1285
+ const context = this.currentContext;
1286
+ if (context) {
1287
+ this.hooks.afterGet({
1288
+ operation: 'get',
1289
+ key,
1290
+ protocol: context.protocol,
1291
+ actor: context.actor,
1292
+ metadata: context.metadata,
1293
+ timestamp: new Date(),
1294
+ }).catch(() => {});
1295
+ }
1296
+ }
1297
+
1298
+ return { stream: stream as ReadableStream<Uint8Array>, size };
1299
+ } catch {
1300
+ return null;
1301
+ }
1302
+ }
1303
+
1304
+ /**
1305
+ * Store an object from a ReadableStream.
1306
+ */
1307
+ public async putObjectStream(key: string, stream: ReadableStream<Uint8Array>): Promise<void> {
1308
+ if (this.hooks?.beforePut) {
1309
+ const context = this.currentContext;
1310
+ if (context) {
1311
+ const hookContext: IStorageHookContext = {
1312
+ operation: 'put',
1313
+ key,
1314
+ protocol: context.protocol,
1315
+ actor: context.actor,
1316
+ metadata: context.metadata,
1317
+ timestamp: new Date(),
1318
+ };
1319
+ const result = await this.hooks.beforePut(hookContext);
1320
+ if (!result.allowed) {
1321
+ throw new Error(result.reason || 'Storage operation denied by hook');
1322
+ }
1323
+ }
1324
+ }
1325
+
1326
+ // Convert WebStream to Node Readable at the S3 SDK boundary
1327
+ // AWS SDK v3 PutObjectCommand requires a Node.js Readable (not WebStream)
1328
+ const { Readable } = await import('stream');
1329
+ const nodeStream = Readable.fromWeb(stream as any);
1330
+ await this.bucket.fastPutStream({
1331
+ path: key,
1332
+ readableStream: nodeStream,
1333
+ overwrite: true,
1334
+ });
1335
+
1336
+ if (this.hooks?.afterPut) {
1337
+ const context = this.currentContext;
1338
+ if (context) {
1339
+ this.hooks.afterPut({
1340
+ operation: 'put',
1341
+ key,
1342
+ protocol: context.protocol,
1343
+ actor: context.actor,
1344
+ metadata: context.metadata,
1345
+ timestamp: new Date(),
1346
+ }).catch(() => {});
1347
+ }
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * Get object size without reading data (S3 HEAD request).
1353
+ */
1354
+ public async getObjectSize(key: string): Promise<number | null> {
1355
+ try {
1356
+ const stat = await this.bucket.fastStat({ path: key });
1357
+ return stat.ContentLength ?? null;
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
1362
+
1363
+ // ---- Protocol-specific streaming wrappers ----
1364
+
1365
+ public async getOciBlobStream(digest: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1366
+ return this.getObjectStream(this.getOciBlobPath(digest));
1367
+ }
1368
+
1369
+ public async putOciBlobStream(digest: string, stream: ReadableStream<Uint8Array>): Promise<void> {
1370
+ return this.putObjectStream(this.getOciBlobPath(digest), stream);
1371
+ }
1372
+
1373
+ public async getOciBlobSize(digest: string): Promise<number | null> {
1374
+ return this.getObjectSize(this.getOciBlobPath(digest));
1375
+ }
1376
+
1377
+ public async getNpmTarballStream(packageName: string, version: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1378
+ return this.getObjectStream(this.getNpmTarballPath(packageName, version));
1379
+ }
1380
+
1381
+ public async getMavenArtifactStream(groupId: string, artifactId: string, version: string, filename: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1382
+ return this.getObjectStream(this.getMavenArtifactPath(groupId, artifactId, version, filename));
1383
+ }
1384
+
1385
+ public async getCargoCrateStream(crateName: string, version: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1386
+ return this.getObjectStream(this.getCargoCratePath(crateName, version));
1387
+ }
1388
+
1389
+ public async getComposerPackageZipStream(vendorPackage: string, reference: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1390
+ return this.getObjectStream(this.getComposerZipPath(vendorPackage, reference));
1391
+ }
1392
+
1393
+ public async getPypiPackageFileStream(packageName: string, filename: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1394
+ return this.getObjectStream(this.getPypiPackageFilePath(packageName, filename));
1395
+ }
1396
+
1397
+ public async getRubyGemsGemStream(gemName: string, version: string, platform?: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null> {
1398
+ return this.getObjectStream(this.getRubyGemsGemPath(gemName, version, platform));
1399
+ }
1269
1400
  }
@@ -0,0 +1,63 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ /**
4
+ * Convert Buffer, Uint8Array, string, or JSON object to a ReadableStream<Uint8Array>.
5
+ */
6
+ export function toReadableStream(data: Buffer | Uint8Array | string | object): ReadableStream<Uint8Array> {
7
+ const buf = Buffer.isBuffer(data)
8
+ ? data
9
+ : data instanceof Uint8Array
10
+ ? Buffer.from(data)
11
+ : typeof data === 'string'
12
+ ? Buffer.from(data, 'utf-8')
13
+ : Buffer.from(JSON.stringify(data), 'utf-8');
14
+ return new ReadableStream<Uint8Array>({
15
+ start(controller) {
16
+ controller.enqueue(new Uint8Array(buf));
17
+ controller.close();
18
+ },
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Consume a ReadableStream into a Buffer.
24
+ */
25
+ export async function streamToBuffer(stream: ReadableStream<Uint8Array>): Promise<Buffer> {
26
+ const reader = stream.getReader();
27
+ const chunks: Uint8Array[] = [];
28
+ while (true) {
29
+ const { done, value } = await reader.read();
30
+ if (done) break;
31
+ if (value) chunks.push(value);
32
+ }
33
+ return Buffer.concat(chunks);
34
+ }
35
+
36
+ /**
37
+ * Consume a ReadableStream into a parsed JSON object.
38
+ */
39
+ export async function streamToJson<T = any>(stream: ReadableStream<Uint8Array>): Promise<T> {
40
+ const buf = await streamToBuffer(stream);
41
+ return JSON.parse(buf.toString('utf-8'));
42
+ }
43
+
44
+ /**
45
+ * Create a TransformStream that incrementally hashes data passing through.
46
+ * Data flows through unchanged; the digest is available after the stream completes.
47
+ */
48
+ export function createHashTransform(algorithm: string = 'sha256'): {
49
+ transform: TransformStream<Uint8Array, Uint8Array>;
50
+ getDigest: () => string;
51
+ } {
52
+ const hash = crypto.createHash(algorithm);
53
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
54
+ transform(chunk, controller) {
55
+ hash.update(chunk);
56
+ controller.enqueue(chunk);
57
+ },
58
+ });
59
+ return {
60
+ transform,
61
+ getDigest: () => hash.digest('hex'),
62
+ };
63
+ }
package/ts/core/index.ts CHANGED
@@ -12,6 +12,9 @@ export { DefaultAuthProvider } from './classes.defaultauthprovider.js';
12
12
  // Storage interfaces and hooks
13
13
  export * from './interfaces.storage.js';
14
14
 
15
+ // Stream helpers
16
+ export { toReadableStream, streamToBuffer, streamToJson, createHashTransform } from './helpers.stream.js';
17
+
15
18
  // Classes
16
19
  export { BaseRegistry } from './classes.baseregistry.js';
17
20
  export { RegistryStorage } from './classes.registrystorage.js';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type * as plugins from '../plugins.js';
6
- import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
6
+ import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
7
7
  import type { IAuthProvider } from './interfaces.auth.js';
8
8
  import type { IStorageHooks } from './interfaces.storage.js';
9
9
 
@@ -88,9 +88,8 @@ export interface IAuthConfig {
88
88
  export interface IProtocolConfig {
89
89
  enabled: boolean;
90
90
  basePath: string;
91
+ registryUrl?: string;
91
92
  features?: Record<string, boolean>;
92
- /** Upstream registry configuration for proxying/caching */
93
- upstream?: IProtocolUpstreamConfig;
94
93
  }
95
94
 
96
95
  /**
@@ -113,6 +112,13 @@ export interface IRegistryConfig {
113
112
  */
114
113
  storageHooks?: IStorageHooks;
115
114
 
115
+ /**
116
+ * Dynamic upstream configuration provider.
117
+ * Called per-request to resolve which upstream registries to use.
118
+ * Use StaticUpstreamProvider for simple static configurations.
119
+ */
120
+ upstreamProvider?: IUpstreamProvider;
121
+
116
122
  oci?: IProtocolConfig;
117
123
  npm?: IProtocolConfig;
118
124
  maven?: IProtocolConfig;
@@ -155,6 +161,21 @@ export interface IStorageBackend {
155
161
  * Get object metadata
156
162
  */
157
163
  getMetadata(key: string): Promise<Record<string, string> | null>;
164
+
165
+ /**
166
+ * Get an object as a ReadableStream. Returns null if not found.
167
+ */
168
+ getObjectStream?(key: string): Promise<{ stream: ReadableStream<Uint8Array>; size: number } | null>;
169
+
170
+ /**
171
+ * Store an object from a ReadableStream.
172
+ */
173
+ putObjectStream?(key: string, stream: ReadableStream<Uint8Array>): Promise<void>;
174
+
175
+ /**
176
+ * Get object size without reading data (S3 HEAD request).
177
+ */
178
+ getObjectSize?(key: string): Promise<number | null>;
158
179
  }
159
180
 
160
181
  /**
@@ -210,10 +231,13 @@ export interface IRequestContext {
210
231
  }
211
232
 
212
233
  /**
213
- * Base response structure
234
+ * Base response structure.
235
+ * `body` is always a `ReadableStream<Uint8Array>` at the public API boundary.
236
+ * Internal handlers may return Buffer/string/object — the SmartRegistry orchestrator
237
+ * auto-wraps them via `toReadableStream()` before returning to the caller.
214
238
  */
215
239
  export interface IResponse {
216
240
  status: number;
217
241
  headers: Record<string, string>;
218
- body?: any;
242
+ body?: ReadableStream<Uint8Array> | any;
219
243
  }
@@ -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 { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
9
+ import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
10
+ import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
11
11
  import { toBuffer } from '../core/helpers.buffer.js';
12
12
  import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
13
13
  import {
@@ -33,34 +33,64 @@ export class MavenRegistry extends BaseRegistry {
33
33
  private authManager: AuthManager;
34
34
  private basePath: string = '/maven';
35
35
  private registryUrl: string;
36
- private upstream: MavenUpstream | null = null;
36
+ private upstreamProvider: IUpstreamProvider | null = null;
37
37
 
38
38
  constructor(
39
39
  storage: RegistryStorage,
40
40
  authManager: AuthManager,
41
41
  basePath: string,
42
42
  registryUrl: string,
43
- upstreamConfig?: IProtocolUpstreamConfig
43
+ upstreamProvider?: IUpstreamProvider
44
44
  ) {
45
45
  super();
46
46
  this.storage = storage;
47
47
  this.authManager = authManager;
48
48
  this.basePath = basePath;
49
49
  this.registryUrl = registryUrl;
50
+ this.upstreamProvider = upstreamProvider || null;
51
+ }
50
52
 
51
- // Initialize upstream if configured
52
- if (upstreamConfig?.enabled) {
53
- this.upstream = new MavenUpstream(upstreamConfig);
54
- }
53
+ /**
54
+ * Extract scope from Maven coordinates.
55
+ * For Maven, the groupId is the scope.
56
+ * @example "com.example" from "com.example:my-lib"
57
+ */
58
+ private extractScope(groupId: string): string | null {
59
+ return groupId || null;
60
+ }
61
+
62
+ /**
63
+ * Get upstream for a specific request.
64
+ * Calls the provider to resolve upstream config dynamically.
65
+ */
66
+ private async getUpstreamForRequest(
67
+ resource: string,
68
+ resourceType: string,
69
+ method: string,
70
+ actor?: IRequestActor
71
+ ): Promise<MavenUpstream | null> {
72
+ if (!this.upstreamProvider) return null;
73
+
74
+ // For Maven, resource is "groupId:artifactId"
75
+ const [groupId] = resource.split(':');
76
+ const config = await this.upstreamProvider.resolveUpstreamConfig({
77
+ protocol: 'maven',
78
+ resource,
79
+ scope: this.extractScope(groupId),
80
+ actor,
81
+ method,
82
+ resourceType,
83
+ });
84
+
85
+ if (!config?.enabled) return null;
86
+ return new MavenUpstream(config);
55
87
  }
56
88
 
57
89
  /**
58
90
  * Clean up resources (timers, connections, etc.)
59
91
  */
60
92
  public destroy(): void {
61
- if (this.upstream) {
62
- this.upstream.stop();
63
- }
93
+ // No persistent upstream to clean up with dynamic provider
64
94
  }
65
95
 
66
96
  public async init(): Promise<void> {
@@ -85,13 +115,21 @@ export class MavenRegistry extends BaseRegistry {
85
115
  token = await this.authManager.validateToken(tokenString, 'maven');
86
116
  }
87
117
 
118
+ // Build actor from context and validated token
119
+ const actor: IRequestActor = {
120
+ ...context.actor,
121
+ userId: token?.userId,
122
+ ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
123
+ userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
124
+ };
125
+
88
126
  // Parse path to determine request type
89
127
  const coordinate = pathToGAV(path);
90
128
 
91
129
  if (!coordinate) {
92
130
  // Not a valid artifact path, could be metadata or root
93
131
  if (path.endsWith('/maven-metadata.xml')) {
94
- return this.handleMetadataRequest(context.method, path, token);
132
+ return this.handleMetadataRequest(context.method, path, token, actor);
95
133
  }
96
134
 
97
135
  return {
@@ -108,7 +146,7 @@ export class MavenRegistry extends BaseRegistry {
108
146
  }
109
147
 
110
148
  // Handle artifact requests (JAR, POM, WAR, etc.)
111
- return this.handleArtifactRequest(context.method, coordinate, token, context.body);
149
+ return this.handleArtifactRequest(context.method, coordinate, token, context.body, actor);
112
150
  }
113
151
 
114
152
  protected async checkPermission(
@@ -128,7 +166,8 @@ export class MavenRegistry extends BaseRegistry {
128
166
  method: string,
129
167
  coordinate: IMavenCoordinate,
130
168
  token: IAuthToken | null,
131
- body?: Buffer | any
169
+ body?: Buffer | any,
170
+ actor?: IRequestActor
132
171
  ): Promise<IResponse> {
133
172
  const { groupId, artifactId, version } = coordinate;
134
173
  const filename = buildFilename(coordinate);
@@ -139,7 +178,7 @@ export class MavenRegistry extends BaseRegistry {
139
178
  case 'HEAD':
140
179
  // Maven repositories typically allow anonymous reads
141
180
  return method === 'GET'
142
- ? this.getArtifact(groupId, artifactId, version, filename)
181
+ ? this.getArtifact(groupId, artifactId, version, filename, actor)
143
182
  : this.headArtifact(groupId, artifactId, version, filename);
144
183
 
145
184
  case 'PUT':
@@ -211,7 +250,8 @@ export class MavenRegistry extends BaseRegistry {
211
250
  private async handleMetadataRequest(
212
251
  method: string,
213
252
  path: string,
214
- token: IAuthToken | null
253
+ token: IAuthToken | null,
254
+ actor?: IRequestActor
215
255
  ): Promise<IResponse> {
216
256
  // Parse path to extract groupId and artifactId
217
257
  // Path format: /com/example/my-lib/maven-metadata.xml
@@ -232,7 +272,7 @@ export class MavenRegistry extends BaseRegistry {
232
272
  if (method === 'GET') {
233
273
  // Metadata is usually public (read permission optional)
234
274
  // Some registries allow anonymous metadata access
235
- return this.getMetadata(groupId, artifactId);
275
+ return this.getMetadata(groupId, artifactId, actor);
236
276
  }
237
277
 
238
278
  return {
@@ -250,16 +290,33 @@ export class MavenRegistry extends BaseRegistry {
250
290
  groupId: string,
251
291
  artifactId: string,
252
292
  version: string,
253
- filename: string
293
+ filename: string,
294
+ actor?: IRequestActor
254
295
  ): Promise<IResponse> {
255
- let data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
296
+ // Try local storage first (streaming)
297
+ const streamResult = await this.storage.getMavenArtifactStream(groupId, artifactId, version, filename);
298
+ if (streamResult) {
299
+ const ext = filename.split('.').pop() || '';
300
+ const contentType = this.getContentType(ext);
301
+ return {
302
+ status: 200,
303
+ headers: {
304
+ 'Content-Type': contentType,
305
+ 'Content-Length': streamResult.size.toString(),
306
+ },
307
+ body: streamResult.stream,
308
+ };
309
+ }
256
310
 
257
311
  // Try upstream if not found locally
258
- if (!data && this.upstream) {
312
+ let data: Buffer | null = null;
313
+ const resource = `${groupId}:${artifactId}`;
314
+ const upstream = await this.getUpstreamForRequest(resource, 'artifact', 'GET', actor);
315
+ if (upstream) {
259
316
  // Parse the filename to extract extension and classifier
260
317
  const { extension, classifier } = this.parseFilename(filename, artifactId, version);
261
318
  if (extension) {
262
- data = await this.upstream.fetchArtifact(groupId, artifactId, version, extension, classifier);
319
+ data = await upstream.fetchArtifact(groupId, artifactId, version, extension, classifier);
263
320
  if (data) {
264
321
  // Cache the artifact locally
265
322
  await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
@@ -495,16 +552,20 @@ export class MavenRegistry extends BaseRegistry {
495
552
  // METADATA OPERATIONS
496
553
  // ========================================================================
497
554
 
498
- private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
555
+ private async getMetadata(groupId: string, artifactId: string, actor?: IRequestActor): Promise<IResponse> {
499
556
  let metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
500
557
 
501
558
  // Try upstream if not found locally
502
- if (!metadataBuffer && this.upstream) {
503
- const upstreamMetadata = await this.upstream.fetchMetadata(groupId, artifactId);
504
- if (upstreamMetadata) {
505
- metadataBuffer = Buffer.from(upstreamMetadata, 'utf-8');
506
- // Cache the metadata locally
507
- await this.storage.putMavenMetadata(groupId, artifactId, metadataBuffer);
559
+ if (!metadataBuffer) {
560
+ const resource = `${groupId}:${artifactId}`;
561
+ const upstream = await this.getUpstreamForRequest(resource, 'metadata', 'GET', actor);
562
+ if (upstream) {
563
+ const upstreamMetadata = await upstream.fetchMetadata(groupId, artifactId);
564
+ if (upstreamMetadata) {
565
+ metadataBuffer = Buffer.from(upstreamMetadata, 'utf-8');
566
+ // Cache the metadata locally
567
+ await this.storage.putMavenMetadata(groupId, artifactId, metadataBuffer);
568
+ }
508
569
  }
509
570
  }
510
571