@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
@@ -3,6 +3,8 @@ 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';
7
+ import { NpmUpstream } from './classes.npmupstream.js';
6
8
  import type {
7
9
  IPackument,
8
10
  INpmVersion,
@@ -25,12 +27,14 @@ export class NpmRegistry extends BaseRegistry {
25
27
  private basePath: string = '/npm';
26
28
  private registryUrl: string;
27
29
  private logger: Smartlog;
30
+ private upstream: NpmUpstream | null = null;
28
31
 
29
32
  constructor(
30
33
  storage: RegistryStorage,
31
34
  authManager: AuthManager,
32
35
  basePath: string = '/npm',
33
- registryUrl: string = 'http://localhost:5000/npm'
36
+ registryUrl: string = 'http://localhost:5000/npm',
37
+ upstreamConfig?: IProtocolUpstreamConfig
34
38
  ) {
35
39
  super();
36
40
  this.storage = storage;
@@ -50,6 +54,14 @@ export class NpmRegistry 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 NpmUpstream(upstreamConfig, registryUrl, this.logger);
61
+ this.logger.log('info', 'NPM upstream initialized', {
62
+ upstreams: upstreamConfig.upstreams.map(u => u.name),
63
+ });
64
+ }
53
65
  }
54
66
 
55
67
  public async init(): Promise<void> {
@@ -209,13 +221,28 @@ export class NpmRegistry extends BaseRegistry {
209
221
  token: IAuthToken | null,
210
222
  query: Record<string, string>
211
223
  ): Promise<IResponse> {
212
- const packument = await this.storage.getNpmPackument(packageName);
224
+ let packument = await this.storage.getNpmPackument(packageName);
213
225
  this.logger.log('debug', `getPackument: ${packageName}`, {
214
226
  packageName,
215
227
  found: !!packument,
216
228
  versions: packument ? Object.keys(packument.versions).length : 0
217
229
  });
218
230
 
231
+ // If not found locally, try upstream
232
+ if (!packument && this.upstream) {
233
+ this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
234
+ const upstreamPackument = await this.upstream.fetchPackument(packageName);
235
+ if (upstreamPackument) {
236
+ this.logger.log('debug', `getPackument: found in upstream`, {
237
+ packageName,
238
+ versions: Object.keys(upstreamPackument.versions || {}).length
239
+ });
240
+ packument = upstreamPackument;
241
+ // Optionally cache the packument locally (without tarballs)
242
+ // We don't store tarballs here - they'll be fetched on demand
243
+ }
244
+ }
245
+
219
246
  if (!packument) {
220
247
  return {
221
248
  status: 404,
@@ -255,11 +282,21 @@ export class NpmRegistry extends BaseRegistry {
255
282
  token: IAuthToken | null
256
283
  ): Promise<IResponse> {
257
284
  this.logger.log('debug', 'handlePackageVersion', { packageName, version });
258
- const packument = await this.storage.getNpmPackument(packageName);
285
+ let packument = await this.storage.getNpmPackument(packageName);
259
286
  this.logger.log('debug', 'handlePackageVersion packument', { found: !!packument });
260
287
  if (packument) {
261
288
  this.logger.log('debug', 'handlePackageVersion versions', { versions: Object.keys(packument.versions || {}) });
262
289
  }
290
+
291
+ // If not found locally, try upstream
292
+ if (!packument && this.upstream) {
293
+ this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
294
+ const upstreamPackument = await this.upstream.fetchPackument(packageName);
295
+ if (upstreamPackument) {
296
+ packument = upstreamPackument;
297
+ }
298
+ }
299
+
263
300
  if (!packument) {
264
301
  return {
265
302
  status: 404,
@@ -529,7 +566,7 @@ export class NpmRegistry extends BaseRegistry {
529
566
  token: IAuthToken | null
530
567
  ): Promise<IResponse> {
531
568
  // Extract version from filename: package-name-1.0.0.tgz
532
- const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/);
569
+ const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/i);
533
570
  if (!versionMatch) {
534
571
  return {
535
572
  status: 400,
@@ -539,7 +576,26 @@ export class NpmRegistry extends BaseRegistry {
539
576
  }
540
577
 
541
578
  const version = versionMatch[1];
542
- const tarball = await this.storage.getNpmTarball(packageName, version);
579
+ let tarball = await this.storage.getNpmTarball(packageName, version);
580
+
581
+ // If not found locally, try upstream
582
+ if (!tarball && this.upstream) {
583
+ this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
584
+ packageName,
585
+ version,
586
+ });
587
+ const upstreamTarball = await this.upstream.fetchTarball(packageName, version);
588
+ if (upstreamTarball) {
589
+ tarball = upstreamTarball;
590
+ // Cache the tarball locally for future requests
591
+ await this.storage.putNpmTarball(packageName, version, tarball);
592
+ this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
593
+ packageName,
594
+ version,
595
+ size: tarball.length,
596
+ });
597
+ }
598
+ }
543
599
 
544
600
  if (!tarball) {
545
601
  return {
@@ -0,0 +1,260 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { BaseUpstream } from '../upstream/classes.baseupstream.js';
3
+ import type {
4
+ IProtocolUpstreamConfig,
5
+ IUpstreamFetchContext,
6
+ IUpstreamResult,
7
+ IUpstreamRegistryConfig,
8
+ } from '../upstream/interfaces.upstream.js';
9
+ import type { IPackument, INpmVersion } from './interfaces.npm.js';
10
+
11
+ /**
12
+ * NPM-specific upstream implementation.
13
+ *
14
+ * Handles:
15
+ * - Package metadata (packument) fetching
16
+ * - Tarball proxying
17
+ * - Scoped package routing (@scope/* patterns)
18
+ * - NPM-specific URL rewriting
19
+ */
20
+ export class NpmUpstream extends BaseUpstream {
21
+ protected readonly protocolName = 'npm';
22
+
23
+ /** Local registry URL for rewriting tarball URLs */
24
+ private readonly localRegistryUrl: string;
25
+
26
+ constructor(
27
+ config: IProtocolUpstreamConfig,
28
+ localRegistryUrl: string,
29
+ logger?: plugins.smartlog.Smartlog,
30
+ ) {
31
+ super(config, logger);
32
+ this.localRegistryUrl = localRegistryUrl;
33
+ }
34
+
35
+ /**
36
+ * Fetch a packument from upstream registries.
37
+ */
38
+ public async fetchPackument(packageName: string): Promise<IPackument | null> {
39
+ const context: IUpstreamFetchContext = {
40
+ protocol: 'npm',
41
+ resource: packageName,
42
+ resourceType: 'packument',
43
+ path: `/${encodeURIComponent(packageName).replace('%40', '@')}`,
44
+ method: 'GET',
45
+ headers: {
46
+ 'accept': 'application/json',
47
+ },
48
+ query: {},
49
+ };
50
+
51
+ const result = await this.fetch(context);
52
+
53
+ if (!result || !result.success) {
54
+ return null;
55
+ }
56
+
57
+ // Parse and process packument
58
+ let packument: IPackument;
59
+ if (Buffer.isBuffer(result.body)) {
60
+ packument = JSON.parse(result.body.toString('utf8'));
61
+ } else {
62
+ packument = result.body;
63
+ }
64
+
65
+ // Rewrite tarball URLs to point to local registry
66
+ packument = this.rewriteTarballUrls(packument);
67
+
68
+ return packument;
69
+ }
70
+
71
+ /**
72
+ * Fetch a specific version from upstream registries.
73
+ */
74
+ public async fetchVersion(packageName: string, version: string): Promise<INpmVersion | null> {
75
+ const context: IUpstreamFetchContext = {
76
+ protocol: 'npm',
77
+ resource: packageName,
78
+ resourceType: 'version',
79
+ path: `/${encodeURIComponent(packageName).replace('%40', '@')}/${version}`,
80
+ method: 'GET',
81
+ headers: {
82
+ 'accept': 'application/json',
83
+ },
84
+ query: {},
85
+ };
86
+
87
+ const result = await this.fetch(context);
88
+
89
+ if (!result || !result.success) {
90
+ return null;
91
+ }
92
+
93
+ let versionData: INpmVersion;
94
+ if (Buffer.isBuffer(result.body)) {
95
+ versionData = JSON.parse(result.body.toString('utf8'));
96
+ } else {
97
+ versionData = result.body;
98
+ }
99
+
100
+ // Rewrite tarball URL
101
+ if (versionData.dist?.tarball) {
102
+ versionData.dist.tarball = this.rewriteSingleTarballUrl(
103
+ packageName,
104
+ versionData.version,
105
+ versionData.dist.tarball,
106
+ );
107
+ }
108
+
109
+ return versionData;
110
+ }
111
+
112
+ /**
113
+ * Fetch a tarball from upstream registries.
114
+ */
115
+ public async fetchTarball(packageName: string, version: string): Promise<Buffer | null> {
116
+ // First, try to get the tarball URL from packument
117
+ const packument = await this.fetchPackument(packageName);
118
+ let tarballPath: string;
119
+
120
+ if (packument?.versions?.[version]?.dist?.tarball) {
121
+ // Extract path from original (upstream) tarball URL
122
+ const tarballUrl = packument.versions[version].dist.tarball;
123
+ try {
124
+ const url = new URL(tarballUrl);
125
+ tarballPath = url.pathname;
126
+ } catch {
127
+ // Fallback to standard NPM tarball path
128
+ tarballPath = this.buildTarballPath(packageName, version);
129
+ }
130
+ } else {
131
+ tarballPath = this.buildTarballPath(packageName, version);
132
+ }
133
+
134
+ const context: IUpstreamFetchContext = {
135
+ protocol: 'npm',
136
+ resource: packageName,
137
+ resourceType: 'tarball',
138
+ path: tarballPath,
139
+ method: 'GET',
140
+ headers: {
141
+ 'accept': 'application/octet-stream',
142
+ },
143
+ query: {},
144
+ };
145
+
146
+ const result = await this.fetch(context);
147
+
148
+ if (!result || !result.success) {
149
+ return null;
150
+ }
151
+
152
+ return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
153
+ }
154
+
155
+ /**
156
+ * Search packages in upstream registries.
157
+ */
158
+ public async search(text: string, size: number = 20, from: number = 0): Promise<any | null> {
159
+ const context: IUpstreamFetchContext = {
160
+ protocol: 'npm',
161
+ resource: '*',
162
+ resourceType: 'search',
163
+ path: '/-/v1/search',
164
+ method: 'GET',
165
+ headers: {
166
+ 'accept': 'application/json',
167
+ },
168
+ query: {
169
+ text,
170
+ size: size.toString(),
171
+ from: from.toString(),
172
+ },
173
+ };
174
+
175
+ const result = await this.fetch(context);
176
+
177
+ if (!result || !result.success) {
178
+ return null;
179
+ }
180
+
181
+ if (Buffer.isBuffer(result.body)) {
182
+ return JSON.parse(result.body.toString('utf8'));
183
+ }
184
+
185
+ return result.body;
186
+ }
187
+
188
+ /**
189
+ * Build the standard NPM tarball path.
190
+ */
191
+ private buildTarballPath(packageName: string, version: string): string {
192
+ // NPM uses: /{package}/-/{package-name}-{version}.tgz
193
+ // For scoped packages: /@scope/name/-/name-version.tgz
194
+ if (packageName.startsWith('@')) {
195
+ const [scope, name] = packageName.split('/');
196
+ return `/${scope}/${name}/-/${name}-${version}.tgz`;
197
+ } else {
198
+ return `/${packageName}/-/${packageName}-${version}.tgz`;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Rewrite all tarball URLs in a packument to point to local registry.
204
+ */
205
+ private rewriteTarballUrls(packument: IPackument): IPackument {
206
+ if (!packument.versions) {
207
+ return packument;
208
+ }
209
+
210
+ const rewritten = { ...packument };
211
+ rewritten.versions = {};
212
+
213
+ for (const [version, versionData] of Object.entries(packument.versions)) {
214
+ const newVersionData = { ...versionData };
215
+ if (newVersionData.dist?.tarball) {
216
+ newVersionData.dist = {
217
+ ...newVersionData.dist,
218
+ tarball: this.rewriteSingleTarballUrl(
219
+ packument.name,
220
+ version,
221
+ newVersionData.dist.tarball,
222
+ ),
223
+ };
224
+ }
225
+ rewritten.versions[version] = newVersionData;
226
+ }
227
+
228
+ return rewritten;
229
+ }
230
+
231
+ /**
232
+ * Rewrite a single tarball URL to point to local registry.
233
+ */
234
+ private rewriteSingleTarballUrl(
235
+ packageName: string,
236
+ version: string,
237
+ _originalUrl: string,
238
+ ): string {
239
+ // Generate local tarball URL
240
+ // Format: {localRegistryUrl}/{package}/-/{package-name}-{version}.tgz
241
+ const safeName = packageName.replace('@', '').replace('/', '-');
242
+ return `${this.localRegistryUrl}/${packageName}/-/${safeName}-${version}.tgz`;
243
+ }
244
+
245
+ /**
246
+ * Override URL building for NPM-specific handling.
247
+ */
248
+ protected buildUpstreamUrl(
249
+ upstream: IUpstreamRegistryConfig,
250
+ context: IUpstreamFetchContext,
251
+ ): string {
252
+ // NPM registries often don't have trailing slashes
253
+ let baseUrl = upstream.url;
254
+ if (baseUrl.endsWith('/')) {
255
+ baseUrl = baseUrl.slice(0, -1);
256
+ }
257
+
258
+ return `${baseUrl}${context.path}`;
259
+ }
260
+ }
package/ts/npm/index.ts CHANGED
@@ -3,4 +3,5 @@
3
3
  */
4
4
 
5
5
  export { NpmRegistry } from './classes.npmregistry.js';
6
+ export { NpmUpstream } from './classes.npmupstream.js';
6
7
  export * from './interfaces.npm.js';
@@ -1,7 +1,10 @@
1
+ import { Smartlog } from '@push.rocks/smartlog';
1
2
  import { BaseRegistry } from '../core/classes.baseregistry.js';
2
3
  import { RegistryStorage } from '../core/classes.registrystorage.js';
3
4
  import { AuthManager } from '../core/classes.authmanager.js';
4
5
  import type { IRequestContext, IResponse, IAuthToken, IRegistryError } from '../core/interfaces.core.js';
6
+ import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
7
+ import { OciUpstream } from './classes.ociupstream.js';
5
8
  import type {
6
9
  IUploadSession,
7
10
  IOciManifest,
@@ -21,18 +24,42 @@ export class OciRegistry extends BaseRegistry {
21
24
  private basePath: string = '/oci';
22
25
  private cleanupInterval?: NodeJS.Timeout;
23
26
  private ociTokens?: { realm: string; service: string };
27
+ private upstream: OciUpstream | null = null;
28
+ private logger: Smartlog;
24
29
 
25
30
  constructor(
26
31
  storage: RegistryStorage,
27
32
  authManager: AuthManager,
28
33
  basePath: string = '/oci',
29
- ociTokens?: { realm: string; service: string }
34
+ ociTokens?: { realm: string; service: string },
35
+ upstreamConfig?: IProtocolUpstreamConfig
30
36
  ) {
31
37
  super();
32
38
  this.storage = storage;
33
39
  this.authManager = authManager;
34
40
  this.basePath = basePath;
35
41
  this.ociTokens = ociTokens;
42
+
43
+ // Initialize logger
44
+ this.logger = new Smartlog({
45
+ logContext: {
46
+ company: 'push.rocks',
47
+ companyunit: 'smartregistry',
48
+ containerName: 'oci-registry',
49
+ environment: (process.env.NODE_ENV as any) || 'development',
50
+ runtime: 'node',
51
+ zone: 'oci'
52
+ }
53
+ });
54
+ this.logger.enableConsole();
55
+
56
+ // Initialize upstream if configured
57
+ if (upstreamConfig?.enabled) {
58
+ this.upstream = new OciUpstream(upstreamConfig, basePath, this.logger);
59
+ this.logger.log('info', 'OCI upstream initialized', {
60
+ upstreams: upstreamConfig.upstreams.map(u => u.name),
61
+ });
62
+ }
36
63
  }
37
64
 
38
65
  public async init(): Promise<void> {
@@ -302,16 +329,50 @@ export class OciRegistry extends BaseRegistry {
302
329
  if (!reference.startsWith('sha256:')) {
303
330
  const tags = await this.getTagsData(repository);
304
331
  digest = tags[reference];
305
- if (!digest) {
306
- return {
307
- status: 404,
308
- headers: {},
309
- body: this.createError('MANIFEST_UNKNOWN', 'Manifest not found'),
310
- };
332
+ }
333
+
334
+ // Try local storage first (if we have a digest)
335
+ let manifestData: Buffer | null = null;
336
+ let contentType: string | null = null;
337
+
338
+ if (digest) {
339
+ manifestData = await this.storage.getOciManifest(repository, digest);
340
+ if (manifestData) {
341
+ contentType = await this.storage.getOciManifestContentType(repository, digest);
342
+ if (!contentType) {
343
+ contentType = this.detectManifestContentType(manifestData);
344
+ }
345
+ }
346
+ }
347
+
348
+ // If not found locally, try upstream
349
+ if (!manifestData && this.upstream) {
350
+ this.logger.log('debug', 'getManifest: fetching from upstream', { repository, reference });
351
+ const upstreamResult = await this.upstream.fetchManifest(repository, reference);
352
+ if (upstreamResult) {
353
+ manifestData = Buffer.from(JSON.stringify(upstreamResult.manifest), 'utf8');
354
+ contentType = upstreamResult.contentType;
355
+ digest = upstreamResult.digest;
356
+
357
+ // Cache the manifest locally
358
+ await this.storage.putOciManifest(repository, digest, manifestData, contentType);
359
+
360
+ // If reference is a tag, update tags mapping
361
+ if (!reference.startsWith('sha256:')) {
362
+ const tags = await this.getTagsData(repository);
363
+ tags[reference] = digest;
364
+ const tagsPath = `oci/tags/${repository}/tags.json`;
365
+ await this.storage.putObject(tagsPath, Buffer.from(JSON.stringify(tags), 'utf-8'));
366
+ }
367
+
368
+ this.logger.log('debug', 'getManifest: cached manifest locally', {
369
+ repository,
370
+ reference,
371
+ digest,
372
+ });
311
373
  }
312
374
  }
313
375
 
314
- const manifestData = await this.storage.getOciManifest(repository, digest);
315
376
  if (!manifestData) {
316
377
  return {
317
378
  status: 404,
@@ -320,17 +381,10 @@ export class OciRegistry extends BaseRegistry {
320
381
  };
321
382
  }
322
383
 
323
- // Get stored content type, falling back to detecting from manifest content
324
- let contentType = await this.storage.getOciManifestContentType(repository, digest);
325
- if (!contentType) {
326
- // Fallback: detect content type from manifest content
327
- contentType = this.detectManifestContentType(manifestData);
328
- }
329
-
330
384
  return {
331
385
  status: 200,
332
386
  headers: {
333
- 'Content-Type': contentType,
387
+ 'Content-Type': contentType || 'application/vnd.oci.image.manifest.v1+json',
334
388
  'Docker-Content-Digest': digest,
335
389
  },
336
390
  body: manifestData,
@@ -466,7 +520,25 @@ export class OciRegistry extends BaseRegistry {
466
520
  return this.createUnauthorizedResponse(repository, 'pull');
467
521
  }
468
522
 
469
- const data = await this.storage.getOciBlob(digest);
523
+ // Try local storage first
524
+ let data = await this.storage.getOciBlob(digest);
525
+
526
+ // If not found locally, try upstream
527
+ if (!data && this.upstream) {
528
+ this.logger.log('debug', 'getBlob: fetching from upstream', { repository, digest });
529
+ const upstreamBlob = await this.upstream.fetchBlob(repository, digest);
530
+ if (upstreamBlob) {
531
+ data = upstreamBlob;
532
+ // Cache the blob locally (blobs are content-addressable and immutable)
533
+ await this.storage.putOciBlob(digest, data);
534
+ this.logger.log('debug', 'getBlob: cached blob locally', {
535
+ repository,
536
+ digest,
537
+ size: data.length,
538
+ });
539
+ }
540
+ }
541
+
470
542
  if (!data) {
471
543
  return {
472
544
  status: 404,