@push.rocks/smartregistry 2.2.3 → 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.
- 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.js +8 -8
- 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/interfaces.core.d.ts +3 -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 +409 -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 +123 -0
- package/dist_ts/upstream/classes.upstreamcache.js +328 -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 +49 -7
- 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/interfaces.core.ts +3 -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 +521 -0
- package/ts/upstream/classes.circuitbreaker.ts +238 -0
- package/ts/upstream/classes.upstreamcache.ts +423 -0
- package/ts/upstream/index.ts +11 -0
- package/ts/upstream/interfaces.upstream.ts +195 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { TRegistryProtocol } from '../core/interfaces.core.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scope rule for routing requests to specific upstreams.
|
|
4
|
+
* Uses glob patterns for flexible matching.
|
|
5
|
+
*/
|
|
6
|
+
export interface IUpstreamScopeRule {
|
|
7
|
+
/** Glob pattern (e.g., "@company/*", "com.example.*", "library/*") */
|
|
8
|
+
pattern: string;
|
|
9
|
+
/** Whether matching resources should be included or excluded */
|
|
10
|
+
action: 'include' | 'exclude';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Authentication configuration for an upstream registry.
|
|
14
|
+
* Supports multiple auth strategies.
|
|
15
|
+
*/
|
|
16
|
+
export interface IUpstreamAuthConfig {
|
|
17
|
+
/** Authentication type */
|
|
18
|
+
type: 'none' | 'basic' | 'bearer' | 'api-key';
|
|
19
|
+
/** Username for basic auth */
|
|
20
|
+
username?: string;
|
|
21
|
+
/** Password for basic auth */
|
|
22
|
+
password?: string;
|
|
23
|
+
/** Token for bearer or api-key auth */
|
|
24
|
+
token?: string;
|
|
25
|
+
/** Custom header name for api-key auth (default: 'Authorization') */
|
|
26
|
+
headerName?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cache configuration for upstream content.
|
|
30
|
+
*/
|
|
31
|
+
export interface IUpstreamCacheConfig {
|
|
32
|
+
/** Whether caching is enabled */
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
/** Default TTL in seconds for mutable content (default: 300 = 5 min) */
|
|
35
|
+
defaultTtlSeconds: number;
|
|
36
|
+
/** TTL in seconds for immutable/content-addressable content (default: 2592000 = 30 days) */
|
|
37
|
+
immutableTtlSeconds: number;
|
|
38
|
+
/** Whether to serve stale content while revalidating in background */
|
|
39
|
+
staleWhileRevalidate: boolean;
|
|
40
|
+
/** Maximum age in seconds for stale content (default: 3600 = 1 hour) */
|
|
41
|
+
staleMaxAgeSeconds: number;
|
|
42
|
+
/** TTL in seconds for negative cache entries (404s) (default: 60 = 1 min) */
|
|
43
|
+
negativeCacheTtlSeconds: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resilience configuration for upstream requests.
|
|
47
|
+
*/
|
|
48
|
+
export interface IUpstreamResilienceConfig {
|
|
49
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
52
|
+
maxRetries: number;
|
|
53
|
+
/** Initial retry delay in milliseconds (default: 1000) */
|
|
54
|
+
retryDelayMs: number;
|
|
55
|
+
/** Maximum retry delay in milliseconds (default: 30000) */
|
|
56
|
+
retryMaxDelayMs: number;
|
|
57
|
+
/** Number of failures before circuit breaker opens (default: 5) */
|
|
58
|
+
circuitBreakerThreshold: number;
|
|
59
|
+
/** Time in milliseconds before circuit breaker attempts reset (default: 30000) */
|
|
60
|
+
circuitBreakerResetMs: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Configuration for a single upstream registry.
|
|
64
|
+
*/
|
|
65
|
+
export interface IUpstreamRegistryConfig {
|
|
66
|
+
/** Unique identifier for this upstream */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Human-readable name */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Base URL of the upstream registry (e.g., "https://registry.npmjs.org") */
|
|
71
|
+
url: string;
|
|
72
|
+
/** Priority for routing (lower = higher priority, 1 = first) */
|
|
73
|
+
priority: number;
|
|
74
|
+
/** Whether this upstream is enabled */
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
/** Scope rules for routing (empty = match all) */
|
|
77
|
+
scopeRules?: IUpstreamScopeRule[];
|
|
78
|
+
/** Authentication configuration */
|
|
79
|
+
auth: IUpstreamAuthConfig;
|
|
80
|
+
/** Cache configuration overrides */
|
|
81
|
+
cache?: Partial<IUpstreamCacheConfig>;
|
|
82
|
+
/** Resilience configuration overrides */
|
|
83
|
+
resilience?: Partial<IUpstreamResilienceConfig>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Protocol-level upstream configuration.
|
|
87
|
+
* Configures upstream behavior for a specific protocol (npm, oci, etc.)
|
|
88
|
+
*/
|
|
89
|
+
export interface IProtocolUpstreamConfig {
|
|
90
|
+
/** Whether upstream is enabled for this protocol */
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
/** List of upstream registries, ordered by priority */
|
|
93
|
+
upstreams: IUpstreamRegistryConfig[];
|
|
94
|
+
/** Protocol-level cache configuration defaults */
|
|
95
|
+
cache?: Partial<IUpstreamCacheConfig>;
|
|
96
|
+
/** Protocol-level resilience configuration defaults */
|
|
97
|
+
resilience?: Partial<IUpstreamResilienceConfig>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Result of an upstream fetch operation.
|
|
101
|
+
*/
|
|
102
|
+
export interface IUpstreamResult {
|
|
103
|
+
/** Whether the fetch was successful (2xx status) */
|
|
104
|
+
success: boolean;
|
|
105
|
+
/** HTTP status code */
|
|
106
|
+
status: number;
|
|
107
|
+
/** Response headers */
|
|
108
|
+
headers: Record<string, string>;
|
|
109
|
+
/** Response body (Buffer for binary, object for JSON) */
|
|
110
|
+
body?: Buffer | any;
|
|
111
|
+
/** ID of the upstream that served the request */
|
|
112
|
+
upstreamId: string;
|
|
113
|
+
/** Whether the response was served from cache */
|
|
114
|
+
fromCache: boolean;
|
|
115
|
+
/** Request latency in milliseconds */
|
|
116
|
+
latencyMs: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Circuit breaker state.
|
|
120
|
+
*/
|
|
121
|
+
export type TCircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
122
|
+
/**
|
|
123
|
+
* Context for an upstream fetch request.
|
|
124
|
+
*/
|
|
125
|
+
export interface IUpstreamFetchContext {
|
|
126
|
+
/** Protocol type */
|
|
127
|
+
protocol: TRegistryProtocol;
|
|
128
|
+
/** Resource identifier (package name, artifact name, etc.) */
|
|
129
|
+
resource: string;
|
|
130
|
+
/** Type of resource being fetched (packument, tarball, manifest, blob, etc.) */
|
|
131
|
+
resourceType: string;
|
|
132
|
+
/** Original request path */
|
|
133
|
+
path: string;
|
|
134
|
+
/** HTTP method */
|
|
135
|
+
method: string;
|
|
136
|
+
/** Request headers */
|
|
137
|
+
headers: Record<string, string>;
|
|
138
|
+
/** Query parameters */
|
|
139
|
+
query: Record<string, string>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Cache entry stored in the upstream cache.
|
|
143
|
+
*/
|
|
144
|
+
export interface ICacheEntry {
|
|
145
|
+
/** Cached data */
|
|
146
|
+
data: Buffer;
|
|
147
|
+
/** Content type of the cached data */
|
|
148
|
+
contentType: string;
|
|
149
|
+
/** Original response headers */
|
|
150
|
+
headers: Record<string, string>;
|
|
151
|
+
/** When the entry was cached */
|
|
152
|
+
cachedAt: Date;
|
|
153
|
+
/** When the entry expires */
|
|
154
|
+
expiresAt?: Date;
|
|
155
|
+
/** ETag for conditional requests */
|
|
156
|
+
etag?: string;
|
|
157
|
+
/** ID of the upstream that provided the data */
|
|
158
|
+
upstreamId: string;
|
|
159
|
+
/** Whether the entry is stale but still usable */
|
|
160
|
+
stale?: boolean;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Default cache configuration values.
|
|
164
|
+
*/
|
|
165
|
+
export declare const DEFAULT_CACHE_CONFIG: IUpstreamCacheConfig;
|
|
166
|
+
/**
|
|
167
|
+
* Default resilience configuration values.
|
|
168
|
+
*/
|
|
169
|
+
export declare const DEFAULT_RESILIENCE_CONFIG: IUpstreamResilienceConfig;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default cache configuration values.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_CACHE_CONFIG = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
defaultTtlSeconds: 300, // 5 minutes
|
|
7
|
+
immutableTtlSeconds: 2592000, // 30 days
|
|
8
|
+
staleWhileRevalidate: true,
|
|
9
|
+
staleMaxAgeSeconds: 3600, // 1 hour
|
|
10
|
+
negativeCacheTtlSeconds: 60, // 1 minute
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Default resilience configuration values.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_RESILIENCE_CONFIG = {
|
|
16
|
+
timeoutMs: 30000,
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
retryDelayMs: 1000,
|
|
19
|
+
retryMaxDelayMs: 30000,
|
|
20
|
+
circuitBreakerThreshold: 5,
|
|
21
|
+
circuitBreakerResetMs: 30000,
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy51cHN0cmVhbS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3Vwc3RyZWFtL2ludGVyZmFjZXMudXBzdHJlYW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBNEtBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQXlCO0lBQ3hELE9BQU8sRUFBRSxJQUFJO0lBQ2IsaUJBQWlCLEVBQUUsR0FBRyxFQUFZLFlBQVk7SUFDOUMsbUJBQW1CLEVBQUUsT0FBTyxFQUFNLFVBQVU7SUFDNUMsb0JBQW9CLEVBQUUsSUFBSTtJQUMxQixrQkFBa0IsRUFBRSxJQUFJLEVBQVUsU0FBUztJQUMzQyx1QkFBdUIsRUFBRSxFQUFFLEVBQU8sV0FBVztDQUM5QyxDQUFDO0FBRUY7O0dBRUc7QUFDSCxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBOEI7SUFDbEUsU0FBUyxFQUFFLEtBQUs7SUFDaEIsVUFBVSxFQUFFLENBQUM7SUFDYixZQUFZLEVBQUUsSUFBSTtJQUNsQixlQUFlLEVBQUUsS0FBSztJQUN0Qix1QkFBdUIsRUFBRSxDQUFDO0lBQzFCLHFCQUFxQixFQUFFLEtBQUs7Q0FDN0IsQ0FBQyJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartregistry",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -42,8 +42,10 @@
|
|
|
42
42
|
"@push.rocks/smartbucket": "^4.3.0",
|
|
43
43
|
"@push.rocks/smartlog": "^3.1.10",
|
|
44
44
|
"@push.rocks/smartpath": "^6.0.0",
|
|
45
|
+
"@push.rocks/smartrequest": "^5.0.1",
|
|
45
46
|
"@tsclass/tsclass": "^9.3.0",
|
|
46
|
-
"adm-zip": "^0.5.10"
|
|
47
|
+
"adm-zip": "^0.5.10",
|
|
48
|
+
"minimatch": "^10.1.1"
|
|
47
49
|
},
|
|
48
50
|
"scripts": {
|
|
49
51
|
"test": "(tstest test/ --verbose --logfile --timeout 240)",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartregistry',
|
|
6
|
-
version: '2.
|
|
6
|
+
version: '2.3.0',
|
|
7
7
|
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
|
8
8
|
}
|
|
@@ -3,6 +3,7 @@ import { BaseRegistry } from '../core/classes.baseregistry.js';
|
|
|
3
3
|
import { RegistryStorage } from '../core/classes.registrystorage.js';
|
|
4
4
|
import { AuthManager } from '../core/classes.authmanager.js';
|
|
5
5
|
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
+
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
|
6
7
|
import type {
|
|
7
8
|
ICargoIndexEntry,
|
|
8
9
|
ICargoPublishMetadata,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
ICargoSearchResponse,
|
|
14
15
|
ICargoSearchResult,
|
|
15
16
|
} from './interfaces.cargo.js';
|
|
17
|
+
import { CargoUpstream } from './classes.cargoupstream.js';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Cargo/crates.io registry implementation
|
|
@@ -25,12 +27,14 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
25
27
|
private basePath: string = '/cargo';
|
|
26
28
|
private registryUrl: string;
|
|
27
29
|
private logger: Smartlog;
|
|
30
|
+
private upstream: CargoUpstream | null = null;
|
|
28
31
|
|
|
29
32
|
constructor(
|
|
30
33
|
storage: RegistryStorage,
|
|
31
34
|
authManager: AuthManager,
|
|
32
35
|
basePath: string = '/cargo',
|
|
33
|
-
registryUrl: string = 'http://localhost:5000/cargo'
|
|
36
|
+
registryUrl: string = 'http://localhost:5000/cargo',
|
|
37
|
+
upstreamConfig?: IProtocolUpstreamConfig
|
|
34
38
|
) {
|
|
35
39
|
super();
|
|
36
40
|
this.storage = storage;
|
|
@@ -50,6 +54,20 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
50
54
|
}
|
|
51
55
|
});
|
|
52
56
|
this.logger.enableConsole();
|
|
57
|
+
|
|
58
|
+
// Initialize upstream if configured
|
|
59
|
+
if (upstreamConfig?.enabled) {
|
|
60
|
+
this.upstream = new CargoUpstream(upstreamConfig, undefined, this.logger);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clean up resources (timers, connections, etc.)
|
|
66
|
+
*/
|
|
67
|
+
public destroy(): void {
|
|
68
|
+
if (this.upstream) {
|
|
69
|
+
this.upstream.stop();
|
|
70
|
+
}
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
public async init(): Promise<void> {
|
|
@@ -207,7 +225,25 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
207
225
|
* Serve index file for a crate
|
|
208
226
|
*/
|
|
209
227
|
private async handleIndexFile(crateName: string): Promise<IResponse> {
|
|
210
|
-
|
|
228
|
+
let index = await this.storage.getCargoIndex(crateName);
|
|
229
|
+
|
|
230
|
+
// Try upstream if not found locally
|
|
231
|
+
if ((!index || index.length === 0) && this.upstream) {
|
|
232
|
+
const upstreamIndex = await this.upstream.fetchCrateIndex(crateName);
|
|
233
|
+
if (upstreamIndex) {
|
|
234
|
+
// Parse the newline-delimited JSON
|
|
235
|
+
const parsedIndex: ICargoIndexEntry[] = upstreamIndex
|
|
236
|
+
.split('\n')
|
|
237
|
+
.filter(line => line.trim())
|
|
238
|
+
.map(line => JSON.parse(line));
|
|
239
|
+
|
|
240
|
+
if (parsedIndex.length > 0) {
|
|
241
|
+
// Cache locally
|
|
242
|
+
await this.storage.putCargoIndex(crateName, parsedIndex);
|
|
243
|
+
index = parsedIndex;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
211
247
|
|
|
212
248
|
if (!index || index.length === 0) {
|
|
213
249
|
return {
|
|
@@ -399,7 +435,16 @@ export class CargoRegistry extends BaseRegistry {
|
|
|
399
435
|
): Promise<IResponse> {
|
|
400
436
|
this.logger.log('debug', 'handleDownload', { crate: crateName, version });
|
|
401
437
|
|
|
402
|
-
|
|
438
|
+
let crateFile = await this.storage.getCargoCrate(crateName, version);
|
|
439
|
+
|
|
440
|
+
// Try upstream if not found locally
|
|
441
|
+
if (!crateFile && this.upstream) {
|
|
442
|
+
crateFile = await this.upstream.fetchCrate(crateName, version);
|
|
443
|
+
if (crateFile) {
|
|
444
|
+
// Cache locally
|
|
445
|
+
await this.storage.putCargoCrate(crateName, version, crateFile);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
403
448
|
|
|
404
449
|
if (!crateFile) {
|
|
405
450
|
return {
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
* Cargo-specific upstream implementation.
|
|
11
|
+
*
|
|
12
|
+
* Handles:
|
|
13
|
+
* - Crate metadata (index) fetching
|
|
14
|
+
* - Crate file (.crate) downloading
|
|
15
|
+
* - Sparse index protocol support
|
|
16
|
+
* - Content-addressable caching for .crate files
|
|
17
|
+
*/
|
|
18
|
+
export class CargoUpstream extends BaseUpstream {
|
|
19
|
+
protected readonly protocolName = 'cargo';
|
|
20
|
+
|
|
21
|
+
/** Base URL for crate downloads (may differ from index URL) */
|
|
22
|
+
private readonly downloadUrl: string;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
config: IProtocolUpstreamConfig,
|
|
26
|
+
downloadUrl?: string,
|
|
27
|
+
logger?: plugins.smartlog.Smartlog,
|
|
28
|
+
) {
|
|
29
|
+
super(config, logger);
|
|
30
|
+
// Default to crates.io download URL if not specified
|
|
31
|
+
this.downloadUrl = downloadUrl || 'https://static.crates.io/crates';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch crate metadata from the sparse index.
|
|
36
|
+
*/
|
|
37
|
+
public async fetchCrateIndex(crateName: string): Promise<string | null> {
|
|
38
|
+
const path = this.buildIndexPath(crateName);
|
|
39
|
+
|
|
40
|
+
const context: IUpstreamFetchContext = {
|
|
41
|
+
protocol: 'cargo',
|
|
42
|
+
resource: crateName,
|
|
43
|
+
resourceType: 'index',
|
|
44
|
+
path,
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: {
|
|
47
|
+
'accept': 'text/plain',
|
|
48
|
+
},
|
|
49
|
+
query: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await this.fetch(context);
|
|
53
|
+
|
|
54
|
+
if (!result || !result.success) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Buffer.isBuffer(result.body)) {
|
|
59
|
+
return result.body.toString('utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return typeof result.body === 'string' ? result.body : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch a crate file from upstream.
|
|
67
|
+
*/
|
|
68
|
+
public async fetchCrate(crateName: string, version: string): Promise<Buffer | null> {
|
|
69
|
+
// Crate downloads typically go to a different URL than the index
|
|
70
|
+
const path = `/${crateName}/${crateName}-${version}.crate`;
|
|
71
|
+
|
|
72
|
+
const context: IUpstreamFetchContext = {
|
|
73
|
+
protocol: 'cargo',
|
|
74
|
+
resource: crateName,
|
|
75
|
+
resourceType: 'crate',
|
|
76
|
+
path,
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: {
|
|
79
|
+
'accept': 'application/octet-stream',
|
|
80
|
+
},
|
|
81
|
+
query: {},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Use special handling for crate downloads
|
|
85
|
+
const result = await this.fetchCrateFile(crateName, version);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fetch crate file directly from the download URL.
|
|
91
|
+
*/
|
|
92
|
+
private async fetchCrateFile(crateName: string, version: string): Promise<Buffer | null> {
|
|
93
|
+
const context: IUpstreamFetchContext = {
|
|
94
|
+
protocol: 'cargo',
|
|
95
|
+
resource: crateName,
|
|
96
|
+
resourceType: 'crate',
|
|
97
|
+
path: `/${crateName}/${crateName}-${version}.crate`,
|
|
98
|
+
method: 'GET',
|
|
99
|
+
headers: {
|
|
100
|
+
'accept': 'application/octet-stream',
|
|
101
|
+
},
|
|
102
|
+
query: {},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await this.fetch(context);
|
|
106
|
+
|
|
107
|
+
if (!result || !result.success) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the sparse index path for a crate.
|
|
116
|
+
*
|
|
117
|
+
* Path structure:
|
|
118
|
+
* - 1 char: /1/{name}
|
|
119
|
+
* - 2 chars: /2/{name}
|
|
120
|
+
* - 3 chars: /3/{first char}/{name}
|
|
121
|
+
* - 4+ chars: /{first 2}/{next 2}/{name}
|
|
122
|
+
*/
|
|
123
|
+
private buildIndexPath(crateName: string): string {
|
|
124
|
+
const lowerName = crateName.toLowerCase();
|
|
125
|
+
const len = lowerName.length;
|
|
126
|
+
|
|
127
|
+
if (len === 1) {
|
|
128
|
+
return `/1/${lowerName}`;
|
|
129
|
+
} else if (len === 2) {
|
|
130
|
+
return `/2/${lowerName}`;
|
|
131
|
+
} else if (len === 3) {
|
|
132
|
+
return `/3/${lowerName[0]}/${lowerName}`;
|
|
133
|
+
} else {
|
|
134
|
+
return `/${lowerName.slice(0, 2)}/${lowerName.slice(2, 4)}/${lowerName}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Override URL building for Cargo-specific handling.
|
|
140
|
+
*/
|
|
141
|
+
protected buildUpstreamUrl(
|
|
142
|
+
upstream: IUpstreamRegistryConfig,
|
|
143
|
+
context: IUpstreamFetchContext,
|
|
144
|
+
): string {
|
|
145
|
+
let baseUrl = upstream.url;
|
|
146
|
+
|
|
147
|
+
// For crate downloads, use the download URL
|
|
148
|
+
if (context.resourceType === 'crate') {
|
|
149
|
+
baseUrl = this.downloadUrl;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Remove trailing slash
|
|
153
|
+
if (baseUrl.endsWith('/')) {
|
|
154
|
+
baseUrl = baseUrl.slice(0, -1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `${baseUrl}${context.path}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/ts/cargo/index.ts
CHANGED
|
@@ -46,7 +46,13 @@ export class SmartRegistry {
|
|
|
46
46
|
realm: this.config.auth.ociTokens.realm,
|
|
47
47
|
service: this.config.auth.ociTokens.service,
|
|
48
48
|
} : undefined;
|
|
49
|
-
const ociRegistry = new OciRegistry(
|
|
49
|
+
const ociRegistry = new OciRegistry(
|
|
50
|
+
this.storage,
|
|
51
|
+
this.authManager,
|
|
52
|
+
ociBasePath,
|
|
53
|
+
ociTokens,
|
|
54
|
+
this.config.oci.upstream
|
|
55
|
+
);
|
|
50
56
|
await ociRegistry.init();
|
|
51
57
|
this.registries.set('oci', ociRegistry);
|
|
52
58
|
}
|
|
@@ -55,7 +61,13 @@ export class SmartRegistry {
|
|
|
55
61
|
if (this.config.npm?.enabled) {
|
|
56
62
|
const npmBasePath = this.config.npm.basePath ?? '/npm';
|
|
57
63
|
const registryUrl = `http://localhost:5000${npmBasePath}`; // TODO: Make configurable
|
|
58
|
-
const npmRegistry = new NpmRegistry(
|
|
64
|
+
const npmRegistry = new NpmRegistry(
|
|
65
|
+
this.storage,
|
|
66
|
+
this.authManager,
|
|
67
|
+
npmBasePath,
|
|
68
|
+
registryUrl,
|
|
69
|
+
this.config.npm.upstream
|
|
70
|
+
);
|
|
59
71
|
await npmRegistry.init();
|
|
60
72
|
this.registries.set('npm', npmRegistry);
|
|
61
73
|
}
|
|
@@ -64,7 +76,13 @@ export class SmartRegistry {
|
|
|
64
76
|
if (this.config.maven?.enabled) {
|
|
65
77
|
const mavenBasePath = this.config.maven.basePath ?? '/maven';
|
|
66
78
|
const registryUrl = `http://localhost:5000${mavenBasePath}`; // TODO: Make configurable
|
|
67
|
-
const mavenRegistry = new MavenRegistry(
|
|
79
|
+
const mavenRegistry = new MavenRegistry(
|
|
80
|
+
this.storage,
|
|
81
|
+
this.authManager,
|
|
82
|
+
mavenBasePath,
|
|
83
|
+
registryUrl,
|
|
84
|
+
this.config.maven.upstream
|
|
85
|
+
);
|
|
68
86
|
await mavenRegistry.init();
|
|
69
87
|
this.registries.set('maven', mavenRegistry);
|
|
70
88
|
}
|
|
@@ -73,7 +91,13 @@ export class SmartRegistry {
|
|
|
73
91
|
if (this.config.cargo?.enabled) {
|
|
74
92
|
const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
|
|
75
93
|
const registryUrl = `http://localhost:5000${cargoBasePath}`; // TODO: Make configurable
|
|
76
|
-
const cargoRegistry = new CargoRegistry(
|
|
94
|
+
const cargoRegistry = new CargoRegistry(
|
|
95
|
+
this.storage,
|
|
96
|
+
this.authManager,
|
|
97
|
+
cargoBasePath,
|
|
98
|
+
registryUrl,
|
|
99
|
+
this.config.cargo.upstream
|
|
100
|
+
);
|
|
77
101
|
await cargoRegistry.init();
|
|
78
102
|
this.registries.set('cargo', cargoRegistry);
|
|
79
103
|
}
|
|
@@ -82,7 +106,13 @@ export class SmartRegistry {
|
|
|
82
106
|
if (this.config.composer?.enabled) {
|
|
83
107
|
const composerBasePath = this.config.composer.basePath ?? '/composer';
|
|
84
108
|
const registryUrl = `http://localhost:5000${composerBasePath}`; // TODO: Make configurable
|
|
85
|
-
const composerRegistry = new ComposerRegistry(
|
|
109
|
+
const composerRegistry = new ComposerRegistry(
|
|
110
|
+
this.storage,
|
|
111
|
+
this.authManager,
|
|
112
|
+
composerBasePath,
|
|
113
|
+
registryUrl,
|
|
114
|
+
this.config.composer.upstream
|
|
115
|
+
);
|
|
86
116
|
await composerRegistry.init();
|
|
87
117
|
this.registries.set('composer', composerRegistry);
|
|
88
118
|
}
|
|
@@ -91,7 +121,13 @@ export class SmartRegistry {
|
|
|
91
121
|
if (this.config.pypi?.enabled) {
|
|
92
122
|
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
93
123
|
const registryUrl = `http://localhost:5000`; // TODO: Make configurable
|
|
94
|
-
const pypiRegistry = new PypiRegistry(
|
|
124
|
+
const pypiRegistry = new PypiRegistry(
|
|
125
|
+
this.storage,
|
|
126
|
+
this.authManager,
|
|
127
|
+
pypiBasePath,
|
|
128
|
+
registryUrl,
|
|
129
|
+
this.config.pypi.upstream
|
|
130
|
+
);
|
|
95
131
|
await pypiRegistry.init();
|
|
96
132
|
this.registries.set('pypi', pypiRegistry);
|
|
97
133
|
}
|
|
@@ -100,7 +136,13 @@ export class SmartRegistry {
|
|
|
100
136
|
if (this.config.rubygems?.enabled) {
|
|
101
137
|
const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
|
|
102
138
|
const registryUrl = `http://localhost:5000${rubygemsBasePath}`; // TODO: Make configurable
|
|
103
|
-
const rubygemsRegistry = new RubyGemsRegistry(
|
|
139
|
+
const rubygemsRegistry = new RubyGemsRegistry(
|
|
140
|
+
this.storage,
|
|
141
|
+
this.authManager,
|
|
142
|
+
rubygemsBasePath,
|
|
143
|
+
registryUrl,
|
|
144
|
+
this.config.rubygems.upstream
|
|
145
|
+
);
|
|
104
146
|
await rubygemsRegistry.init();
|
|
105
147
|
this.registries.set('rubygems', rubygemsRegistry);
|
|
106
148
|
}
|
|
@@ -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 { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
|
11
12
|
import type {
|
|
12
13
|
IComposerPackage,
|
|
@@ -22,24 +23,41 @@ import {
|
|
|
22
23
|
generatePackagesJson,
|
|
23
24
|
sortVersions,
|
|
24
25
|
} from './helpers.composer.js';
|
|
26
|
+
import { ComposerUpstream } from './classes.composerupstream.js';
|
|
25
27
|
|
|
26
28
|
export class ComposerRegistry extends BaseRegistry {
|
|
27
29
|
private storage: RegistryStorage;
|
|
28
30
|
private authManager: AuthManager;
|
|
29
31
|
private basePath: string = '/composer';
|
|
30
32
|
private registryUrl: string;
|
|
33
|
+
private upstream: ComposerUpstream | null = null;
|
|
31
34
|
|
|
32
35
|
constructor(
|
|
33
36
|
storage: RegistryStorage,
|
|
34
37
|
authManager: AuthManager,
|
|
35
38
|
basePath: string = '/composer',
|
|
36
|
-
registryUrl: string = 'http://localhost:5000/composer'
|
|
39
|
+
registryUrl: string = 'http://localhost:5000/composer',
|
|
40
|
+
upstreamConfig?: IProtocolUpstreamConfig
|
|
37
41
|
) {
|
|
38
42
|
super();
|
|
39
43
|
this.storage = storage;
|
|
40
44
|
this.authManager = authManager;
|
|
41
45
|
this.basePath = basePath;
|
|
42
46
|
this.registryUrl = registryUrl;
|
|
47
|
+
|
|
48
|
+
// Initialize upstream if configured
|
|
49
|
+
if (upstreamConfig?.enabled) {
|
|
50
|
+
this.upstream = new ComposerUpstream(upstreamConfig);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clean up resources (timers, connections, etc.)
|
|
56
|
+
*/
|
|
57
|
+
public destroy(): void {
|
|
58
|
+
if (this.upstream) {
|
|
59
|
+
this.upstream.stop();
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
public async init(): Promise<void> {
|
|
@@ -161,7 +179,26 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
161
179
|
token: IAuthToken | null
|
|
162
180
|
): Promise<IResponse> {
|
|
163
181
|
// Read operations are public, no authentication required
|
|
164
|
-
|
|
182
|
+
let metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
|
183
|
+
|
|
184
|
+
// Try upstream if not found locally
|
|
185
|
+
if (!metadata && this.upstream) {
|
|
186
|
+
const [vendor, packageName] = vendorPackage.split('/');
|
|
187
|
+
if (vendor && packageName) {
|
|
188
|
+
const upstreamMetadata = includeDev
|
|
189
|
+
? await this.upstream.fetchPackageDevMetadata(vendor, packageName)
|
|
190
|
+
: await this.upstream.fetchPackageMetadata(vendor, packageName);
|
|
191
|
+
|
|
192
|
+
if (upstreamMetadata && upstreamMetadata.packages) {
|
|
193
|
+
// Store upstream metadata locally
|
|
194
|
+
metadata = {
|
|
195
|
+
packages: upstreamMetadata.packages,
|
|
196
|
+
lastModified: new Date().toUTCString(),
|
|
197
|
+
};
|
|
198
|
+
await this.storage.putComposerPackageMetadata(vendorPackage, metadata);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
165
202
|
|
|
166
203
|
if (!metadata) {
|
|
167
204
|
return {
|