@push.rocks/smartregistry 2.2.2 → 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.
Files changed (91) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
  3. package/dist_ts/cargo/classes.cargoregistry.js +42 -4
  4. package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
  5. package/dist_ts/cargo/classes.cargoupstream.js +129 -0
  6. package/dist_ts/cargo/index.d.ts +1 -0
  7. package/dist_ts/cargo/index.js +2 -1
  8. package/dist_ts/classes.smartregistry.js +8 -8
  9. package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
  10. package/dist_ts/composer/classes.composerregistry.js +34 -3
  11. package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
  12. package/dist_ts/composer/classes.composerupstream.js +159 -0
  13. package/dist_ts/composer/index.d.ts +1 -0
  14. package/dist_ts/composer/index.js +2 -1
  15. package/dist_ts/core/interfaces.core.d.ts +3 -0
  16. package/dist_ts/index.d.ts +1 -0
  17. package/dist_ts/index.js +3 -1
  18. package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
  19. package/dist_ts/maven/classes.mavenregistry.js +69 -4
  20. package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
  21. package/dist_ts/maven/classes.mavenupstream.js +153 -0
  22. package/dist_ts/maven/index.d.ts +1 -0
  23. package/dist_ts/maven/index.js +2 -1
  24. package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
  25. package/dist_ts/npm/classes.npmregistry.js +55 -6
  26. package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
  27. package/dist_ts/npm/classes.npmupstream.js +206 -0
  28. package/dist_ts/npm/index.d.ts +1 -0
  29. package/dist_ts/npm/index.js +2 -1
  30. package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
  31. package/dist_ts/oci/classes.ociregistry.js +78 -17
  32. package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
  33. package/dist_ts/oci/classes.ociupstream.js +206 -0
  34. package/dist_ts/oci/index.d.ts +1 -0
  35. package/dist_ts/oci/index.js +2 -1
  36. package/dist_ts/plugins.d.ts +4 -1
  37. package/dist_ts/plugins.js +6 -2
  38. package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
  39. package/dist_ts/pypi/classes.pypiregistry.js +60 -4
  40. package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
  41. package/dist_ts/pypi/classes.pypiupstream.js +165 -0
  42. package/dist_ts/pypi/index.d.ts +1 -0
  43. package/dist_ts/pypi/index.js +2 -1
  44. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
  45. package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
  46. package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
  47. package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
  48. package/dist_ts/rubygems/index.d.ts +1 -0
  49. package/dist_ts/rubygems/index.js +2 -1
  50. package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
  51. package/dist_ts/upstream/classes.baseupstream.js +409 -0
  52. package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
  53. package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
  54. package/dist_ts/upstream/classes.upstreamcache.d.ts +123 -0
  55. package/dist_ts/upstream/classes.upstreamcache.js +328 -0
  56. package/dist_ts/upstream/index.d.ts +6 -0
  57. package/dist_ts/upstream/index.js +7 -0
  58. package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
  59. package/dist_ts/upstream/interfaces.upstream.js +23 -0
  60. package/package.json +4 -2
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/cargo/classes.cargoregistry.ts +48 -3
  63. package/ts/cargo/classes.cargoupstream.ts +159 -0
  64. package/ts/cargo/index.ts +1 -0
  65. package/ts/classes.smartregistry.ts +49 -7
  66. package/ts/composer/classes.composerregistry.ts +39 -2
  67. package/ts/composer/classes.composerupstream.ts +200 -0
  68. package/ts/composer/index.ts +1 -0
  69. package/ts/core/interfaces.core.ts +3 -0
  70. package/ts/index.ts +3 -0
  71. package/ts/maven/classes.mavenregistry.ts +84 -3
  72. package/ts/maven/classes.mavenupstream.ts +220 -0
  73. package/ts/maven/index.ts +1 -0
  74. package/ts/npm/classes.npmregistry.ts +61 -5
  75. package/ts/npm/classes.npmupstream.ts +260 -0
  76. package/ts/npm/index.ts +1 -0
  77. package/ts/oci/classes.ociregistry.ts +89 -17
  78. package/ts/oci/classes.ociupstream.ts +263 -0
  79. package/ts/oci/index.ts +1 -0
  80. package/ts/plugins.ts +7 -1
  81. package/ts/pypi/classes.pypiregistry.ts +68 -3
  82. package/ts/pypi/classes.pypiupstream.ts +211 -0
  83. package/ts/pypi/index.ts +1 -0
  84. package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
  85. package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
  86. package/ts/rubygems/index.ts +1 -0
  87. package/ts/upstream/classes.baseupstream.ts +521 -0
  88. package/ts/upstream/classes.circuitbreaker.ts +238 -0
  89. package/ts/upstream/classes.upstreamcache.ts +423 -0
  90. package/ts/upstream/index.ts +11 -0
  91. package/ts/upstream/interfaces.upstream.ts +195 -0
@@ -0,0 +1,200 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { BaseUpstream } from '../upstream/classes.baseupstream.js';
3
+ import type {
4
+ IProtocolUpstreamConfig,
5
+ IUpstreamFetchContext,
6
+ IUpstreamRegistryConfig,
7
+ } from '../upstream/interfaces.upstream.js';
8
+
9
+ /**
10
+ * Composer-specific upstream implementation.
11
+ *
12
+ * Handles:
13
+ * - Package metadata fetching (packages.json, provider-includes)
14
+ * - Package version metadata (p2/{vendor}/{package}.json)
15
+ * - Dist file (zip) proxying
16
+ * - Packagist v2 API support
17
+ */
18
+ export class ComposerUpstream extends BaseUpstream {
19
+ protected readonly protocolName = 'composer';
20
+
21
+ constructor(
22
+ config: IProtocolUpstreamConfig,
23
+ logger?: plugins.smartlog.Smartlog,
24
+ ) {
25
+ super(config, logger);
26
+ }
27
+
28
+ /**
29
+ * Fetch the root packages.json from upstream.
30
+ */
31
+ public async fetchPackagesJson(): Promise<any | null> {
32
+ const context: IUpstreamFetchContext = {
33
+ protocol: 'composer',
34
+ resource: '*',
35
+ resourceType: 'root',
36
+ path: '/packages.json',
37
+ method: 'GET',
38
+ headers: {
39
+ 'accept': 'application/json',
40
+ },
41
+ query: {},
42
+ };
43
+
44
+ const result = await this.fetch(context);
45
+
46
+ if (!result || !result.success) {
47
+ return null;
48
+ }
49
+
50
+ if (Buffer.isBuffer(result.body)) {
51
+ return JSON.parse(result.body.toString('utf8'));
52
+ }
53
+
54
+ return result.body;
55
+ }
56
+
57
+ /**
58
+ * Fetch package metadata using v2 API (p2/{vendor}/{package}.json).
59
+ */
60
+ public async fetchPackageMetadata(vendor: string, packageName: string): Promise<any | null> {
61
+ const fullName = `${vendor}/${packageName}`;
62
+ const path = `/p2/${vendor}/${packageName}.json`;
63
+
64
+ const context: IUpstreamFetchContext = {
65
+ protocol: 'composer',
66
+ resource: fullName,
67
+ resourceType: 'metadata',
68
+ path,
69
+ method: 'GET',
70
+ headers: {
71
+ 'accept': 'application/json',
72
+ },
73
+ query: {},
74
+ };
75
+
76
+ const result = await this.fetch(context);
77
+
78
+ if (!result || !result.success) {
79
+ return null;
80
+ }
81
+
82
+ if (Buffer.isBuffer(result.body)) {
83
+ return JSON.parse(result.body.toString('utf8'));
84
+ }
85
+
86
+ return result.body;
87
+ }
88
+
89
+ /**
90
+ * Fetch package metadata with dev versions (p2/{vendor}/{package}~dev.json).
91
+ */
92
+ public async fetchPackageDevMetadata(vendor: string, packageName: string): Promise<any | null> {
93
+ const fullName = `${vendor}/${packageName}`;
94
+ const path = `/p2/${vendor}/${packageName}~dev.json`;
95
+
96
+ const context: IUpstreamFetchContext = {
97
+ protocol: 'composer',
98
+ resource: fullName,
99
+ resourceType: 'metadata-dev',
100
+ path,
101
+ method: 'GET',
102
+ headers: {
103
+ 'accept': 'application/json',
104
+ },
105
+ query: {},
106
+ };
107
+
108
+ const result = await this.fetch(context);
109
+
110
+ if (!result || !result.success) {
111
+ return null;
112
+ }
113
+
114
+ if (Buffer.isBuffer(result.body)) {
115
+ return JSON.parse(result.body.toString('utf8'));
116
+ }
117
+
118
+ return result.body;
119
+ }
120
+
121
+ /**
122
+ * Fetch a provider-includes file.
123
+ */
124
+ public async fetchProviderIncludes(path: string): Promise<any | null> {
125
+ const context: IUpstreamFetchContext = {
126
+ protocol: 'composer',
127
+ resource: '*',
128
+ resourceType: 'provider',
129
+ path: path.startsWith('/') ? path : `/${path}`,
130
+ method: 'GET',
131
+ headers: {
132
+ 'accept': 'application/json',
133
+ },
134
+ query: {},
135
+ };
136
+
137
+ const result = await this.fetch(context);
138
+
139
+ if (!result || !result.success) {
140
+ return null;
141
+ }
142
+
143
+ if (Buffer.isBuffer(result.body)) {
144
+ return JSON.parse(result.body.toString('utf8'));
145
+ }
146
+
147
+ return result.body;
148
+ }
149
+
150
+ /**
151
+ * Fetch a dist file (zip) from upstream.
152
+ */
153
+ public async fetchDist(url: string): Promise<Buffer | null> {
154
+ // Parse the URL to get the path
155
+ let path: string;
156
+ try {
157
+ const parsed = new URL(url);
158
+ path = parsed.pathname;
159
+ } catch {
160
+ path = url;
161
+ }
162
+
163
+ const context: IUpstreamFetchContext = {
164
+ protocol: 'composer',
165
+ resource: '*',
166
+ resourceType: 'dist',
167
+ path,
168
+ method: 'GET',
169
+ headers: {
170
+ 'accept': 'application/zip, application/octet-stream',
171
+ },
172
+ query: {},
173
+ };
174
+
175
+ const result = await this.fetch(context);
176
+
177
+ if (!result || !result.success) {
178
+ return null;
179
+ }
180
+
181
+ return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
182
+ }
183
+
184
+ /**
185
+ * Override URL building for Composer-specific handling.
186
+ */
187
+ protected buildUpstreamUrl(
188
+ upstream: IUpstreamRegistryConfig,
189
+ context: IUpstreamFetchContext,
190
+ ): string {
191
+ let baseUrl = upstream.url;
192
+
193
+ // Remove trailing slash
194
+ if (baseUrl.endsWith('/')) {
195
+ baseUrl = baseUrl.slice(0, -1);
196
+ }
197
+
198
+ return `${baseUrl}${context.path}`;
199
+ }
200
+ }
@@ -4,5 +4,6 @@
4
4
  */
5
5
 
6
6
  export { ComposerRegistry } from './classes.composerregistry.js';
7
+ export { ComposerUpstream } from './classes.composerupstream.js';
7
8
  export * from './interfaces.composer.js';
8
9
  export * from './helpers.composer.js';
@@ -3,6 +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
7
 
7
8
  /**
8
9
  * Registry protocol types
@@ -86,6 +87,8 @@ export interface IProtocolConfig {
86
87
  enabled: boolean;
87
88
  basePath: string;
88
89
  features?: Record<string, boolean>;
90
+ /** Upstream registry configuration for proxying/caching */
91
+ upstream?: IProtocolUpstreamConfig;
89
92
  }
90
93
 
91
94
  /**
package/ts/index.ts CHANGED
@@ -9,6 +9,9 @@ export { SmartRegistry } from './classes.smartregistry.js';
9
9
  // Core infrastructure
10
10
  export * from './core/index.js';
11
11
 
12
+ // Upstream infrastructure
13
+ export * from './upstream/index.js';
14
+
12
15
  // OCI Registry
13
16
  export * from './oci/index.js';
14
17
 
@@ -7,6 +7,7 @@ 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
9
  import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
10
+ import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
10
11
  import { toBuffer } from '../core/helpers.buffer.js';
11
12
  import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
12
13
  import {
@@ -21,6 +22,7 @@ import {
21
22
  extractGAVFromPom,
22
23
  gavToPath,
23
24
  } from './helpers.maven.js';
25
+ import { MavenUpstream } from './classes.mavenupstream.js';
24
26
 
25
27
  /**
26
28
  * Maven Registry class
@@ -31,18 +33,34 @@ export class MavenRegistry extends BaseRegistry {
31
33
  private authManager: AuthManager;
32
34
  private basePath: string = '/maven';
33
35
  private registryUrl: string;
36
+ private upstream: MavenUpstream | null = null;
34
37
 
35
38
  constructor(
36
39
  storage: RegistryStorage,
37
40
  authManager: AuthManager,
38
41
  basePath: string,
39
- registryUrl: string
42
+ registryUrl: string,
43
+ upstreamConfig?: IProtocolUpstreamConfig
40
44
  ) {
41
45
  super();
42
46
  this.storage = storage;
43
47
  this.authManager = authManager;
44
48
  this.basePath = basePath;
45
49
  this.registryUrl = registryUrl;
50
+
51
+ // Initialize upstream if configured
52
+ if (upstreamConfig?.enabled) {
53
+ this.upstream = new MavenUpstream(upstreamConfig);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Clean up resources (timers, connections, etc.)
59
+ */
60
+ public destroy(): void {
61
+ if (this.upstream) {
62
+ this.upstream.stop();
63
+ }
46
64
  }
47
65
 
48
66
  public async init(): Promise<void> {
@@ -234,7 +252,23 @@ export class MavenRegistry extends BaseRegistry {
234
252
  version: string,
235
253
  filename: string
236
254
  ): Promise<IResponse> {
237
- const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
255
+ let data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
256
+
257
+ // Try upstream if not found locally
258
+ if (!data && this.upstream) {
259
+ // Parse the filename to extract extension and classifier
260
+ const { extension, classifier } = this.parseFilename(filename, artifactId, version);
261
+ if (extension) {
262
+ data = await this.upstream.fetchArtifact(groupId, artifactId, version, extension, classifier);
263
+ if (data) {
264
+ // Cache the artifact locally
265
+ await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
266
+ // Generate and store checksums
267
+ const checksums = await calculateChecksums(data);
268
+ await this.storeChecksums(groupId, artifactId, version, filename, checksums);
269
+ }
270
+ }
271
+ }
238
272
 
239
273
  if (!data) {
240
274
  return {
@@ -462,7 +496,17 @@ export class MavenRegistry extends BaseRegistry {
462
496
  // ========================================================================
463
497
 
464
498
  private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
465
- const metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
499
+ let metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
500
+
501
+ // 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);
508
+ }
509
+ }
466
510
 
467
511
  if (!metadataBuffer) {
468
512
  // Generate empty metadata if none exists
@@ -578,4 +622,41 @@ export class MavenRegistry extends BaseRegistry {
578
622
 
579
623
  return contentTypes[extension] || 'application/octet-stream';
580
624
  }
625
+
626
+ /**
627
+ * Parse a Maven filename to extract extension and classifier.
628
+ * Filename format: {artifactId}-{version}[-{classifier}].{extension}
629
+ */
630
+ private parseFilename(
631
+ filename: string,
632
+ artifactId: string,
633
+ version: string
634
+ ): { extension: string; classifier?: string } {
635
+ const prefix = `${artifactId}-${version}`;
636
+
637
+ if (!filename.startsWith(prefix)) {
638
+ // Fallback: just get the extension
639
+ const lastDot = filename.lastIndexOf('.');
640
+ return { extension: lastDot > 0 ? filename.slice(lastDot + 1) : '' };
641
+ }
642
+
643
+ const remainder = filename.slice(prefix.length);
644
+ // remainder is either ".extension" or "-classifier.extension"
645
+
646
+ if (remainder.startsWith('.')) {
647
+ return { extension: remainder.slice(1) };
648
+ }
649
+
650
+ if (remainder.startsWith('-')) {
651
+ const lastDot = remainder.lastIndexOf('.');
652
+ if (lastDot > 1) {
653
+ return {
654
+ classifier: remainder.slice(1, lastDot),
655
+ extension: remainder.slice(lastDot + 1),
656
+ };
657
+ }
658
+ }
659
+
660
+ return { extension: '' };
661
+ }
581
662
  }
@@ -0,0 +1,220 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { BaseUpstream } from '../upstream/classes.baseupstream.js';
3
+ import type {
4
+ IProtocolUpstreamConfig,
5
+ IUpstreamFetchContext,
6
+ IUpstreamRegistryConfig,
7
+ } from '../upstream/interfaces.upstream.js';
8
+ import type { IMavenCoordinate } from './interfaces.maven.js';
9
+
10
+ /**
11
+ * Maven-specific upstream implementation.
12
+ *
13
+ * Handles:
14
+ * - Artifact fetching (JAR, POM, WAR, etc.)
15
+ * - Metadata fetching (maven-metadata.xml)
16
+ * - Checksum files (.md5, .sha1, .sha256, .sha512)
17
+ * - SNAPSHOT version handling
18
+ * - Content-addressable caching for release artifacts
19
+ */
20
+ export class MavenUpstream extends BaseUpstream {
21
+ protected readonly protocolName = 'maven';
22
+
23
+ constructor(
24
+ config: IProtocolUpstreamConfig,
25
+ logger?: plugins.smartlog.Smartlog,
26
+ ) {
27
+ super(config, logger);
28
+ }
29
+
30
+ /**
31
+ * Fetch an artifact from upstream registries.
32
+ */
33
+ public async fetchArtifact(
34
+ groupId: string,
35
+ artifactId: string,
36
+ version: string,
37
+ extension: string,
38
+ classifier?: string,
39
+ ): Promise<Buffer | null> {
40
+ const path = this.buildArtifactPath(groupId, artifactId, version, extension, classifier);
41
+ const resource = `${groupId}:${artifactId}`;
42
+
43
+ const context: IUpstreamFetchContext = {
44
+ protocol: 'maven',
45
+ resource,
46
+ resourceType: 'artifact',
47
+ path,
48
+ method: 'GET',
49
+ headers: {},
50
+ query: {},
51
+ };
52
+
53
+ const result = await this.fetch(context);
54
+
55
+ if (!result || !result.success) {
56
+ return null;
57
+ }
58
+
59
+ return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
60
+ }
61
+
62
+ /**
63
+ * Fetch maven-metadata.xml from upstream.
64
+ */
65
+ public async fetchMetadata(groupId: string, artifactId: string, version?: string): Promise<string | null> {
66
+ const groupPath = groupId.replace(/\./g, '/');
67
+ let path: string;
68
+
69
+ if (version) {
70
+ // Version-level metadata (for SNAPSHOTs)
71
+ path = `/${groupPath}/${artifactId}/${version}/maven-metadata.xml`;
72
+ } else {
73
+ // Artifact-level metadata (lists all versions)
74
+ path = `/${groupPath}/${artifactId}/maven-metadata.xml`;
75
+ }
76
+
77
+ const resource = `${groupId}:${artifactId}`;
78
+
79
+ const context: IUpstreamFetchContext = {
80
+ protocol: 'maven',
81
+ resource,
82
+ resourceType: 'metadata',
83
+ path,
84
+ method: 'GET',
85
+ headers: {
86
+ 'accept': 'application/xml, text/xml',
87
+ },
88
+ query: {},
89
+ };
90
+
91
+ const result = await this.fetch(context);
92
+
93
+ if (!result || !result.success) {
94
+ return null;
95
+ }
96
+
97
+ if (Buffer.isBuffer(result.body)) {
98
+ return result.body.toString('utf8');
99
+ }
100
+
101
+ return typeof result.body === 'string' ? result.body : null;
102
+ }
103
+
104
+ /**
105
+ * Fetch a checksum file from upstream.
106
+ */
107
+ public async fetchChecksum(
108
+ groupId: string,
109
+ artifactId: string,
110
+ version: string,
111
+ extension: string,
112
+ checksumType: 'md5' | 'sha1' | 'sha256' | 'sha512',
113
+ classifier?: string,
114
+ ): Promise<string | null> {
115
+ const basePath = this.buildArtifactPath(groupId, artifactId, version, extension, classifier);
116
+ const path = `${basePath}.${checksumType}`;
117
+ const resource = `${groupId}:${artifactId}`;
118
+
119
+ const context: IUpstreamFetchContext = {
120
+ protocol: 'maven',
121
+ resource,
122
+ resourceType: 'checksum',
123
+ path,
124
+ method: 'GET',
125
+ headers: {
126
+ 'accept': 'text/plain',
127
+ },
128
+ query: {},
129
+ };
130
+
131
+ const result = await this.fetch(context);
132
+
133
+ if (!result || !result.success) {
134
+ return null;
135
+ }
136
+
137
+ if (Buffer.isBuffer(result.body)) {
138
+ return result.body.toString('utf8').trim();
139
+ }
140
+
141
+ return typeof result.body === 'string' ? result.body.trim() : null;
142
+ }
143
+
144
+ /**
145
+ * Check if an artifact exists in upstream (HEAD request).
146
+ */
147
+ public async headArtifact(
148
+ groupId: string,
149
+ artifactId: string,
150
+ version: string,
151
+ extension: string,
152
+ classifier?: string,
153
+ ): Promise<{ exists: boolean; size?: number; lastModified?: string } | null> {
154
+ const path = this.buildArtifactPath(groupId, artifactId, version, extension, classifier);
155
+ const resource = `${groupId}:${artifactId}`;
156
+
157
+ const context: IUpstreamFetchContext = {
158
+ protocol: 'maven',
159
+ resource,
160
+ resourceType: 'artifact',
161
+ path,
162
+ method: 'HEAD',
163
+ headers: {},
164
+ query: {},
165
+ };
166
+
167
+ const result = await this.fetch(context);
168
+
169
+ if (!result) {
170
+ return null;
171
+ }
172
+
173
+ if (!result.success) {
174
+ return { exists: false };
175
+ }
176
+
177
+ return {
178
+ exists: true,
179
+ size: result.headers['content-length'] ? parseInt(result.headers['content-length'], 10) : undefined,
180
+ lastModified: result.headers['last-modified'],
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Build the path for a Maven artifact.
186
+ */
187
+ private buildArtifactPath(
188
+ groupId: string,
189
+ artifactId: string,
190
+ version: string,
191
+ extension: string,
192
+ classifier?: string,
193
+ ): string {
194
+ const groupPath = groupId.replace(/\./g, '/');
195
+ let filename = `${artifactId}-${version}`;
196
+ if (classifier) {
197
+ filename += `-${classifier}`;
198
+ }
199
+ filename += `.${extension}`;
200
+
201
+ return `/${groupPath}/${artifactId}/${version}/${filename}`;
202
+ }
203
+
204
+ /**
205
+ * Override URL building for Maven-specific handling.
206
+ */
207
+ protected buildUpstreamUrl(
208
+ upstream: IUpstreamRegistryConfig,
209
+ context: IUpstreamFetchContext,
210
+ ): string {
211
+ let baseUrl = upstream.url;
212
+
213
+ // Remove trailing slash
214
+ if (baseUrl.endsWith('/')) {
215
+ baseUrl = baseUrl.slice(0, -1);
216
+ }
217
+
218
+ return `${baseUrl}${context.path}`;
219
+ }
220
+ }
package/ts/maven/index.ts CHANGED
@@ -3,5 +3,6 @@
3
3
  */
4
4
 
5
5
  export { MavenRegistry } from './classes.mavenregistry.js';
6
+ export { MavenUpstream } from './classes.mavenupstream.js';
6
7
  export * from './interfaces.maven.js';
7
8
  export * from './helpers.maven.js';