@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,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.3",
3
+ "version": "2.4.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.3',
6
+ version: '2.4.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';
@@ -11,8 +11,39 @@ import { PypiRegistry } from './pypi/classes.pypiregistry.js';
11
11
  import { RubyGemsRegistry } from './rubygems/classes.rubygemsregistry.js';
12
12
 
13
13
  /**
14
- * Main registry orchestrator
15
- * Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems)
14
+ * Main registry orchestrator.
15
+ * Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems).
16
+ *
17
+ * Supports pluggable authentication and storage hooks:
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Basic usage with default in-memory auth
22
+ * const registry = new SmartRegistry(config);
23
+ *
24
+ * // With custom auth provider (LDAP, OAuth, etc.)
25
+ * const registry = new SmartRegistry({
26
+ * ...config,
27
+ * authProvider: new LdapAuthProvider(ldapClient),
28
+ * });
29
+ *
30
+ * // With storage hooks for quota tracking
31
+ * const registry = new SmartRegistry({
32
+ * ...config,
33
+ * storageHooks: {
34
+ * beforePut: async (ctx) => {
35
+ * const quota = await getQuota(ctx.actor?.orgId);
36
+ * if (ctx.metadata?.size > quota) {
37
+ * return { allowed: false, reason: 'Quota exceeded' };
38
+ * }
39
+ * return { allowed: true };
40
+ * },
41
+ * afterPut: async (ctx) => {
42
+ * await auditLog('storage.put', ctx);
43
+ * }
44
+ * }
45
+ * });
46
+ * ```
16
47
  */
17
48
  export class SmartRegistry {
18
49
  private storage: RegistryStorage;
@@ -23,8 +54,12 @@ export class SmartRegistry {
23
54
 
24
55
  constructor(config: IRegistryConfig) {
25
56
  this.config = config;
26
- this.storage = new RegistryStorage(config.storage);
27
- this.authManager = new AuthManager(config.auth);
57
+
58
+ // Create storage with optional hooks
59
+ this.storage = new RegistryStorage(config.storage, config.storageHooks);
60
+
61
+ // Create auth manager with optional custom provider
62
+ this.authManager = new AuthManager(config.auth, config.authProvider);
28
63
  }
29
64
 
30
65
  /**
@@ -46,7 +81,13 @@ export class SmartRegistry {
46
81
  realm: this.config.auth.ociTokens.realm,
47
82
  service: this.config.auth.ociTokens.service,
48
83
  } : undefined;
49
- const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath, ociTokens);
84
+ const ociRegistry = new OciRegistry(
85
+ this.storage,
86
+ this.authManager,
87
+ ociBasePath,
88
+ ociTokens,
89
+ this.config.oci.upstream
90
+ );
50
91
  await ociRegistry.init();
51
92
  this.registries.set('oci', ociRegistry);
52
93
  }
@@ -55,7 +96,13 @@ export class SmartRegistry {
55
96
  if (this.config.npm?.enabled) {
56
97
  const npmBasePath = this.config.npm.basePath ?? '/npm';
57
98
  const registryUrl = `http://localhost:5000${npmBasePath}`; // TODO: Make configurable
58
- const npmRegistry = new NpmRegistry(this.storage, this.authManager, npmBasePath, registryUrl);
99
+ const npmRegistry = new NpmRegistry(
100
+ this.storage,
101
+ this.authManager,
102
+ npmBasePath,
103
+ registryUrl,
104
+ this.config.npm.upstream
105
+ );
59
106
  await npmRegistry.init();
60
107
  this.registries.set('npm', npmRegistry);
61
108
  }
@@ -64,7 +111,13 @@ export class SmartRegistry {
64
111
  if (this.config.maven?.enabled) {
65
112
  const mavenBasePath = this.config.maven.basePath ?? '/maven';
66
113
  const registryUrl = `http://localhost:5000${mavenBasePath}`; // TODO: Make configurable
67
- const mavenRegistry = new MavenRegistry(this.storage, this.authManager, mavenBasePath, registryUrl);
114
+ const mavenRegistry = new MavenRegistry(
115
+ this.storage,
116
+ this.authManager,
117
+ mavenBasePath,
118
+ registryUrl,
119
+ this.config.maven.upstream
120
+ );
68
121
  await mavenRegistry.init();
69
122
  this.registries.set('maven', mavenRegistry);
70
123
  }
@@ -73,7 +126,13 @@ export class SmartRegistry {
73
126
  if (this.config.cargo?.enabled) {
74
127
  const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
75
128
  const registryUrl = `http://localhost:5000${cargoBasePath}`; // TODO: Make configurable
76
- const cargoRegistry = new CargoRegistry(this.storage, this.authManager, cargoBasePath, registryUrl);
129
+ const cargoRegistry = new CargoRegistry(
130
+ this.storage,
131
+ this.authManager,
132
+ cargoBasePath,
133
+ registryUrl,
134
+ this.config.cargo.upstream
135
+ );
77
136
  await cargoRegistry.init();
78
137
  this.registries.set('cargo', cargoRegistry);
79
138
  }
@@ -82,7 +141,13 @@ export class SmartRegistry {
82
141
  if (this.config.composer?.enabled) {
83
142
  const composerBasePath = this.config.composer.basePath ?? '/composer';
84
143
  const registryUrl = `http://localhost:5000${composerBasePath}`; // TODO: Make configurable
85
- const composerRegistry = new ComposerRegistry(this.storage, this.authManager, composerBasePath, registryUrl);
144
+ const composerRegistry = new ComposerRegistry(
145
+ this.storage,
146
+ this.authManager,
147
+ composerBasePath,
148
+ registryUrl,
149
+ this.config.composer.upstream
150
+ );
86
151
  await composerRegistry.init();
87
152
  this.registries.set('composer', composerRegistry);
88
153
  }
@@ -91,7 +156,13 @@ export class SmartRegistry {
91
156
  if (this.config.pypi?.enabled) {
92
157
  const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
93
158
  const registryUrl = `http://localhost:5000`; // TODO: Make configurable
94
- const pypiRegistry = new PypiRegistry(this.storage, this.authManager, pypiBasePath, registryUrl);
159
+ const pypiRegistry = new PypiRegistry(
160
+ this.storage,
161
+ this.authManager,
162
+ pypiBasePath,
163
+ registryUrl,
164
+ this.config.pypi.upstream
165
+ );
95
166
  await pypiRegistry.init();
96
167
  this.registries.set('pypi', pypiRegistry);
97
168
  }
@@ -100,7 +171,13 @@ export class SmartRegistry {
100
171
  if (this.config.rubygems?.enabled) {
101
172
  const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
102
173
  const registryUrl = `http://localhost:5000${rubygemsBasePath}`; // TODO: Make configurable
103
- const rubygemsRegistry = new RubyGemsRegistry(this.storage, this.authManager, rubygemsBasePath, registryUrl);
174
+ const rubygemsRegistry = new RubyGemsRegistry(
175
+ this.storage,
176
+ this.authManager,
177
+ rubygemsBasePath,
178
+ registryUrl,
179
+ this.config.rubygems.upstream
180
+ );
104
181
  await rubygemsRegistry.init();
105
182
  this.registries.set('rubygems', rubygemsRegistry);
106
183
  }