@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,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.2.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)",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartregistry',
6
- version: '2.2.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
- const index = await this.storage.getCargoIndex(crateName);
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
- const crateFile = await this.storage.getCargoCrate(crateName, version);
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
@@ -3,4 +3,5 @@
3
3
  */
4
4
 
5
5
  export { CargoRegistry } from './classes.cargoregistry.js';
6
+ export { CargoUpstream } from './classes.cargoupstream.js';
6
7
  export * from './interfaces.cargo.js';
@@ -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(this.storage, this.authManager, ociBasePath, ociTokens);
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(this.storage, this.authManager, npmBasePath, registryUrl);
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(this.storage, this.authManager, mavenBasePath, registryUrl);
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(this.storage, this.authManager, cargoBasePath, registryUrl);
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(this.storage, this.authManager, composerBasePath, registryUrl);
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(this.storage, this.authManager, pypiBasePath, registryUrl);
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(this.storage, this.authManager, rubygemsBasePath, registryUrl);
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
- const metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
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 {