@push.rocks/smartregistry 2.2.3 → 2.4.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 (110) 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.d.ts +33 -2
  9. package/dist_ts/classes.smartregistry.js +45 -12
  10. package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
  11. package/dist_ts/composer/classes.composerregistry.js +34 -3
  12. package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
  13. package/dist_ts/composer/classes.composerupstream.js +159 -0
  14. package/dist_ts/composer/index.d.ts +1 -0
  15. package/dist_ts/composer/index.js +2 -1
  16. package/dist_ts/core/classes.authmanager.d.ts +30 -80
  17. package/dist_ts/core/classes.authmanager.js +63 -337
  18. package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
  19. package/dist_ts/core/classes.defaultauthprovider.js +311 -0
  20. package/dist_ts/core/classes.registrystorage.d.ts +70 -4
  21. package/dist_ts/core/classes.registrystorage.js +165 -5
  22. package/dist_ts/core/index.d.ts +3 -0
  23. package/dist_ts/core/index.js +7 -2
  24. package/dist_ts/core/interfaces.auth.d.ts +83 -0
  25. package/dist_ts/core/interfaces.auth.js +2 -0
  26. package/dist_ts/core/interfaces.core.d.ts +38 -0
  27. package/dist_ts/core/interfaces.storage.d.ts +120 -0
  28. package/dist_ts/core/interfaces.storage.js +2 -0
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +3 -1
  31. package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
  32. package/dist_ts/maven/classes.mavenregistry.js +69 -4
  33. package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
  34. package/dist_ts/maven/classes.mavenupstream.js +153 -0
  35. package/dist_ts/maven/index.d.ts +1 -0
  36. package/dist_ts/maven/index.js +2 -1
  37. package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
  38. package/dist_ts/npm/classes.npmregistry.js +55 -6
  39. package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
  40. package/dist_ts/npm/classes.npmupstream.js +206 -0
  41. package/dist_ts/npm/index.d.ts +1 -0
  42. package/dist_ts/npm/index.js +2 -1
  43. package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
  44. package/dist_ts/oci/classes.ociregistry.js +78 -17
  45. package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
  46. package/dist_ts/oci/classes.ociupstream.js +206 -0
  47. package/dist_ts/oci/index.d.ts +1 -0
  48. package/dist_ts/oci/index.js +2 -1
  49. package/dist_ts/plugins.d.ts +4 -1
  50. package/dist_ts/plugins.js +6 -2
  51. package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
  52. package/dist_ts/pypi/classes.pypiregistry.js +60 -4
  53. package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
  54. package/dist_ts/pypi/classes.pypiupstream.js +165 -0
  55. package/dist_ts/pypi/index.d.ts +1 -0
  56. package/dist_ts/pypi/index.js +2 -1
  57. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
  58. package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
  59. package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
  60. package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
  61. package/dist_ts/rubygems/index.d.ts +1 -0
  62. package/dist_ts/rubygems/index.js +2 -1
  63. package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
  64. package/dist_ts/upstream/classes.baseupstream.js +411 -0
  65. package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
  66. package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
  67. package/dist_ts/upstream/classes.upstreamcache.d.ts +170 -0
  68. package/dist_ts/upstream/classes.upstreamcache.js +485 -0
  69. package/dist_ts/upstream/index.d.ts +6 -0
  70. package/dist_ts/upstream/index.js +7 -0
  71. package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
  72. package/dist_ts/upstream/interfaces.upstream.js +23 -0
  73. package/package.json +4 -2
  74. package/ts/00_commitinfo_data.ts +1 -1
  75. package/ts/cargo/classes.cargoregistry.ts +48 -3
  76. package/ts/cargo/classes.cargoupstream.ts +159 -0
  77. package/ts/cargo/index.ts +1 -0
  78. package/ts/classes.smartregistry.ts +88 -11
  79. package/ts/composer/classes.composerregistry.ts +39 -2
  80. package/ts/composer/classes.composerupstream.ts +200 -0
  81. package/ts/composer/index.ts +1 -0
  82. package/ts/core/classes.authmanager.ts +74 -412
  83. package/ts/core/classes.defaultauthprovider.ts +393 -0
  84. package/ts/core/classes.registrystorage.ts +199 -5
  85. package/ts/core/index.ts +8 -1
  86. package/ts/core/interfaces.auth.ts +91 -0
  87. package/ts/core/interfaces.core.ts +42 -0
  88. package/ts/core/interfaces.storage.ts +130 -0
  89. package/ts/index.ts +3 -0
  90. package/ts/maven/classes.mavenregistry.ts +84 -3
  91. package/ts/maven/classes.mavenupstream.ts +220 -0
  92. package/ts/maven/index.ts +1 -0
  93. package/ts/npm/classes.npmregistry.ts +61 -5
  94. package/ts/npm/classes.npmupstream.ts +260 -0
  95. package/ts/npm/index.ts +1 -0
  96. package/ts/oci/classes.ociregistry.ts +89 -17
  97. package/ts/oci/classes.ociupstream.ts +263 -0
  98. package/ts/oci/index.ts +1 -0
  99. package/ts/plugins.ts +7 -1
  100. package/ts/pypi/classes.pypiregistry.ts +68 -3
  101. package/ts/pypi/classes.pypiupstream.ts +211 -0
  102. package/ts/pypi/index.ts +1 -0
  103. package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
  104. package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
  105. package/ts/rubygems/index.ts +1 -0
  106. package/ts/upstream/classes.baseupstream.ts +526 -0
  107. package/ts/upstream/classes.circuitbreaker.ts +238 -0
  108. package/ts/upstream/classes.upstreamcache.ts +626 -0
  109. package/ts/upstream/index.ts +11 -0
  110. package/ts/upstream/interfaces.upstream.ts +195 -0
@@ -0,0 +1,91 @@
1
+ import type { IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
2
+
3
+ /**
4
+ * Options for creating a token
5
+ */
6
+ export interface ITokenOptions {
7
+ /** Whether the token is readonly */
8
+ readonly?: boolean;
9
+ /** Permission scopes */
10
+ scopes?: string[];
11
+ /** Expiration time in seconds */
12
+ expiresIn?: number;
13
+ }
14
+
15
+ /**
16
+ * Pluggable authentication provider interface.
17
+ * Implement this to integrate external auth systems (LDAP, OAuth, SSO, OIDC).
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * class LdapAuthProvider implements IAuthProvider {
22
+ * constructor(private ldap: LdapClient, private redis: RedisClient) {}
23
+ *
24
+ * async authenticate(credentials: ICredentials): Promise<string | null> {
25
+ * return await this.ldap.bind(credentials.username, credentials.password);
26
+ * }
27
+ *
28
+ * async validateToken(token: string): Promise<IAuthToken | null> {
29
+ * return await this.redis.get(`token:${token}`);
30
+ * }
31
+ * // ...
32
+ * }
33
+ * ```
34
+ */
35
+ export interface IAuthProvider {
36
+ /**
37
+ * Initialize the auth provider (optional)
38
+ */
39
+ init?(): Promise<void>;
40
+
41
+ /**
42
+ * Authenticate user credentials (login flow)
43
+ * @param credentials - Username and password
44
+ * @returns User ID on success, null on failure
45
+ */
46
+ authenticate(credentials: ICredentials): Promise<string | null>;
47
+
48
+ /**
49
+ * Validate an existing token
50
+ * @param token - Token string (UUID or JWT)
51
+ * @param protocol - Optional protocol hint for optimization
52
+ * @returns Auth token info or null if invalid
53
+ */
54
+ validateToken(token: string, protocol?: TRegistryProtocol): Promise<IAuthToken | null>;
55
+
56
+ /**
57
+ * Create a new token for a user
58
+ * @param userId - User ID
59
+ * @param protocol - Protocol type (npm, oci, maven, etc.)
60
+ * @param options - Token options (readonly, scopes, expiration)
61
+ * @returns Token string
62
+ */
63
+ createToken(userId: string, protocol: TRegistryProtocol, options?: ITokenOptions): Promise<string>;
64
+
65
+ /**
66
+ * Revoke a token
67
+ * @param token - Token string to revoke
68
+ */
69
+ revokeToken(token: string): Promise<void>;
70
+
71
+ /**
72
+ * Check if user has permission for an action
73
+ * @param token - Auth token (or null for anonymous)
74
+ * @param resource - Resource being accessed (e.g., "npm:package:lodash")
75
+ * @param action - Action being performed (read, write, push, pull, delete)
76
+ * @returns true if authorized
77
+ */
78
+ authorize(token: IAuthToken | null, resource: string, action: string): Promise<boolean>;
79
+
80
+ /**
81
+ * List all tokens for a user (optional)
82
+ * @param userId - User ID
83
+ * @returns List of token info
84
+ */
85
+ listUserTokens?(userId: string): Promise<Array<{
86
+ key: string;
87
+ readonly: boolean;
88
+ created: string;
89
+ protocol?: TRegistryProtocol;
90
+ }>>;
91
+ }
@@ -3,6 +3,9 @@
3
3
  */
4
4
 
5
5
  import type * as plugins from '../plugins.js';
6
+ import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
7
+ import type { IAuthProvider } from './interfaces.auth.js';
8
+ import type { IStorageHooks } from './interfaces.storage.js';
6
9
 
7
10
  /**
8
11
  * Registry protocol types
@@ -86,6 +89,8 @@ export interface IProtocolConfig {
86
89
  enabled: boolean;
87
90
  basePath: string;
88
91
  features?: Record<string, boolean>;
92
+ /** Upstream registry configuration for proxying/caching */
93
+ upstream?: IProtocolUpstreamConfig;
89
94
  }
90
95
 
91
96
  /**
@@ -94,6 +99,20 @@ export interface IProtocolConfig {
94
99
  export interface IRegistryConfig {
95
100
  storage: IStorageConfig;
96
101
  auth: IAuthConfig;
102
+
103
+ /**
104
+ * Custom authentication provider.
105
+ * If not provided, uses the default in-memory auth provider.
106
+ * Implement IAuthProvider to integrate LDAP, OAuth, SSO, etc.
107
+ */
108
+ authProvider?: IAuthProvider;
109
+
110
+ /**
111
+ * Storage event hooks for quota tracking, audit logging, etc.
112
+ * Called before/after storage operations.
113
+ */
114
+ storageHooks?: IStorageHooks;
115
+
97
116
  oci?: IProtocolConfig;
98
117
  npm?: IProtocolConfig;
99
118
  maven?: IProtocolConfig;
@@ -149,6 +168,24 @@ export interface IRegistryError {
149
168
  }>;
150
169
  }
151
170
 
171
+ /**
172
+ * Actor information - identifies who is performing the request
173
+ */
174
+ export interface IRequestActor {
175
+ /** User ID (from validated token) */
176
+ userId?: string;
177
+ /** Token ID/hash for audit purposes */
178
+ tokenId?: string;
179
+ /** Client IP address */
180
+ ip?: string;
181
+ /** Client User-Agent */
182
+ userAgent?: string;
183
+ /** Organization ID (for multi-tenant setups) */
184
+ orgId?: string;
185
+ /** Session ID */
186
+ sessionId?: string;
187
+ }
188
+
152
189
  /**
153
190
  * Base request context
154
191
  */
@@ -165,6 +202,11 @@ export interface IRequestContext {
165
202
  */
166
203
  rawBody?: Buffer;
167
204
  token?: string;
205
+ /**
206
+ * Actor information - identifies who is performing the request.
207
+ * Populated after authentication for audit logging, quota enforcement, etc.
208
+ */
209
+ actor?: IRequestActor;
168
210
  }
169
211
 
170
212
  /**
@@ -0,0 +1,130 @@
1
+ import type { TRegistryProtocol } from './interfaces.core.js';
2
+
3
+ /**
4
+ * Actor information from request context
5
+ */
6
+ export interface IStorageActor {
7
+ userId?: string;
8
+ tokenId?: string;
9
+ ip?: string;
10
+ userAgent?: string;
11
+ orgId?: string;
12
+ sessionId?: string;
13
+ }
14
+
15
+ /**
16
+ * Metadata about the storage operation
17
+ */
18
+ export interface IStorageMetadata {
19
+ /** Content type of the object */
20
+ contentType?: string;
21
+ /** Size in bytes */
22
+ size?: number;
23
+ /** Content digest (e.g., sha256:abc123) */
24
+ digest?: string;
25
+ /** Package/artifact name */
26
+ packageName?: string;
27
+ /** Version */
28
+ version?: string;
29
+ }
30
+
31
+ /**
32
+ * Context passed to storage hooks
33
+ */
34
+ export interface IStorageHookContext {
35
+ /** Type of operation */
36
+ operation: 'put' | 'delete' | 'get';
37
+ /** Storage key/path */
38
+ key: string;
39
+ /** Protocol that triggered this operation */
40
+ protocol: TRegistryProtocol;
41
+ /** Actor who performed the operation (if known) */
42
+ actor?: IStorageActor;
43
+ /** Metadata about the object */
44
+ metadata?: IStorageMetadata;
45
+ /** Timestamp of the operation */
46
+ timestamp: Date;
47
+ }
48
+
49
+ /**
50
+ * Result from a beforePut hook that can modify the operation
51
+ */
52
+ export interface IBeforePutResult {
53
+ /** Whether to allow the operation */
54
+ allowed: boolean;
55
+ /** Optional reason for rejection */
56
+ reason?: string;
57
+ /** Optional modified metadata */
58
+ metadata?: IStorageMetadata;
59
+ }
60
+
61
+ /**
62
+ * Result from a beforeDelete hook
63
+ */
64
+ export interface IBeforeDeleteResult {
65
+ /** Whether to allow the operation */
66
+ allowed: boolean;
67
+ /** Optional reason for rejection */
68
+ reason?: string;
69
+ }
70
+
71
+ /**
72
+ * Storage event hooks for quota tracking, audit logging, cache invalidation, etc.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const quotaHooks: IStorageHooks = {
77
+ * async beforePut(context) {
78
+ * const quota = await getQuota(context.actor?.orgId);
79
+ * const currentUsage = await getUsage(context.actor?.orgId);
80
+ * if (currentUsage + (context.metadata?.size || 0) > quota) {
81
+ * return { allowed: false, reason: 'Quota exceeded' };
82
+ * }
83
+ * return { allowed: true };
84
+ * },
85
+ *
86
+ * async afterPut(context) {
87
+ * await updateUsage(context.actor?.orgId, context.metadata?.size || 0);
88
+ * await auditLog('storage.put', context);
89
+ * },
90
+ *
91
+ * async afterDelete(context) {
92
+ * await invalidateCache(context.key);
93
+ * }
94
+ * };
95
+ * ```
96
+ */
97
+ export interface IStorageHooks {
98
+ /**
99
+ * Called before storing an object.
100
+ * Return { allowed: false } to reject the operation.
101
+ * Use for quota checks, virus scanning, validation, etc.
102
+ */
103
+ beforePut?(context: IStorageHookContext): Promise<IBeforePutResult>;
104
+
105
+ /**
106
+ * Called after successfully storing an object.
107
+ * Use for quota tracking, audit logging, notifications, etc.
108
+ */
109
+ afterPut?(context: IStorageHookContext): Promise<void>;
110
+
111
+ /**
112
+ * Called before deleting an object.
113
+ * Return { allowed: false } to reject the operation.
114
+ * Use for preventing deletion of protected resources.
115
+ */
116
+ beforeDelete?(context: IStorageHookContext): Promise<IBeforeDeleteResult>;
117
+
118
+ /**
119
+ * Called after successfully deleting an object.
120
+ * Use for quota updates, audit logging, cache invalidation, etc.
121
+ */
122
+ afterDelete?(context: IStorageHookContext): Promise<void>;
123
+
124
+ /**
125
+ * Called after reading an object.
126
+ * Use for access logging, analytics, etc.
127
+ * Note: This is called even for cache hits.
128
+ */
129
+ afterGet?(context: IStorageHookContext): Promise<void>;
130
+ }
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';