@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +7 -1
- package/dist_ts/cargo/classes.cargoregistry.js +42 -4
- package/dist_ts/cargo/classes.cargoupstream.d.ts +44 -0
- package/dist_ts/cargo/classes.cargoupstream.js +129 -0
- package/dist_ts/cargo/index.d.ts +1 -0
- package/dist_ts/cargo/index.js +2 -1
- package/dist_ts/classes.smartregistry.d.ts +33 -2
- package/dist_ts/classes.smartregistry.js +45 -12
- package/dist_ts/composer/classes.composerregistry.d.ts +7 -1
- package/dist_ts/composer/classes.composerregistry.js +34 -3
- package/dist_ts/composer/classes.composerupstream.d.ts +40 -0
- package/dist_ts/composer/classes.composerupstream.js +159 -0
- package/dist_ts/composer/index.d.ts +1 -0
- package/dist_ts/composer/index.js +2 -1
- package/dist_ts/core/classes.authmanager.d.ts +30 -80
- package/dist_ts/core/classes.authmanager.js +63 -337
- package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
- package/dist_ts/core/classes.defaultauthprovider.js +311 -0
- package/dist_ts/core/classes.registrystorage.d.ts +70 -4
- package/dist_ts/core/classes.registrystorage.js +165 -5
- package/dist_ts/core/index.d.ts +3 -0
- package/dist_ts/core/index.js +7 -2
- package/dist_ts/core/interfaces.auth.d.ts +83 -0
- package/dist_ts/core/interfaces.auth.js +2 -0
- package/dist_ts/core/interfaces.core.d.ts +38 -0
- package/dist_ts/core/interfaces.storage.d.ts +120 -0
- package/dist_ts/core/interfaces.storage.js +2 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/maven/classes.mavenregistry.d.ts +12 -1
- package/dist_ts/maven/classes.mavenregistry.js +69 -4
- package/dist_ts/maven/classes.mavenupstream.d.ts +45 -0
- package/dist_ts/maven/classes.mavenupstream.js +153 -0
- package/dist_ts/maven/index.d.ts +1 -0
- package/dist_ts/maven/index.js +2 -1
- package/dist_ts/npm/classes.npmregistry.d.ts +3 -1
- package/dist_ts/npm/classes.npmregistry.js +55 -6
- package/dist_ts/npm/classes.npmupstream.d.ts +51 -0
- package/dist_ts/npm/classes.npmupstream.js +206 -0
- package/dist_ts/npm/index.d.ts +1 -0
- package/dist_ts/npm/index.js +2 -1
- package/dist_ts/oci/classes.ociregistry.d.ts +4 -1
- package/dist_ts/oci/classes.ociregistry.js +78 -17
- package/dist_ts/oci/classes.ociupstream.d.ts +62 -0
- package/dist_ts/oci/classes.ociupstream.js +206 -0
- package/dist_ts/oci/index.d.ts +1 -0
- package/dist_ts/oci/index.js +2 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +7 -1
- package/dist_ts/pypi/classes.pypiregistry.js +60 -4
- package/dist_ts/pypi/classes.pypiupstream.d.ts +48 -0
- package/dist_ts/pypi/classes.pypiupstream.js +165 -0
- package/dist_ts/pypi/index.d.ts +1 -0
- package/dist_ts/pypi/index.js +2 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +7 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.js +35 -4
- package/dist_ts/rubygems/classes.rubygemsupstream.d.ts +47 -0
- package/dist_ts/rubygems/classes.rubygemsupstream.js +184 -0
- package/dist_ts/rubygems/index.d.ts +1 -0
- package/dist_ts/rubygems/index.js +2 -1
- package/dist_ts/upstream/classes.baseupstream.d.ts +112 -0
- package/dist_ts/upstream/classes.baseupstream.js +411 -0
- package/dist_ts/upstream/classes.circuitbreaker.d.ts +111 -0
- package/dist_ts/upstream/classes.circuitbreaker.js +192 -0
- package/dist_ts/upstream/classes.upstreamcache.d.ts +170 -0
- package/dist_ts/upstream/classes.upstreamcache.js +485 -0
- package/dist_ts/upstream/index.d.ts +6 -0
- package/dist_ts/upstream/index.js +7 -0
- package/dist_ts/upstream/interfaces.upstream.d.ts +169 -0
- package/dist_ts/upstream/interfaces.upstream.js +23 -0
- package/package.json +4 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +48 -3
- package/ts/cargo/classes.cargoupstream.ts +159 -0
- package/ts/cargo/index.ts +1 -0
- package/ts/classes.smartregistry.ts +88 -11
- package/ts/composer/classes.composerregistry.ts +39 -2
- package/ts/composer/classes.composerupstream.ts +200 -0
- package/ts/composer/index.ts +1 -0
- package/ts/core/classes.authmanager.ts +74 -412
- package/ts/core/classes.defaultauthprovider.ts +393 -0
- package/ts/core/classes.registrystorage.ts +199 -5
- package/ts/core/index.ts +8 -1
- package/ts/core/interfaces.auth.ts +91 -0
- package/ts/core/interfaces.core.ts +42 -0
- package/ts/core/interfaces.storage.ts +130 -0
- package/ts/index.ts +3 -0
- package/ts/maven/classes.mavenregistry.ts +84 -3
- package/ts/maven/classes.mavenupstream.ts +220 -0
- package/ts/maven/index.ts +1 -0
- package/ts/npm/classes.npmregistry.ts +61 -5
- package/ts/npm/classes.npmupstream.ts +260 -0
- package/ts/npm/index.ts +1 -0
- package/ts/oci/classes.ociregistry.ts +89 -17
- package/ts/oci/classes.ociupstream.ts +263 -0
- package/ts/oci/index.ts +1 -0
- package/ts/plugins.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +68 -3
- package/ts/pypi/classes.pypiupstream.ts +211 -0
- package/ts/pypi/index.ts +1 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +40 -3
- package/ts/rubygems/classes.rubygemsupstream.ts +230 -0
- package/ts/rubygems/index.ts +1 -0
- package/ts/upstream/classes.baseupstream.ts +526 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +626 -0
- package/ts/upstream/index.ts +11 -0
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|