@push.rocks/smartregistry 2.6.0 → 2.8.1

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 (52) hide show
  1. package/.smartconfig.json +24 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/cargo/classes.cargoregistry.d.ts +8 -3
  4. package/dist_ts/cargo/classes.cargoregistry.js +71 -33
  5. package/dist_ts/classes.smartregistry.js +48 -36
  6. package/dist_ts/composer/classes.composerregistry.d.ts +14 -3
  7. package/dist_ts/composer/classes.composerregistry.js +64 -28
  8. package/dist_ts/core/classes.registrystorage.d.ts +45 -0
  9. package/dist_ts/core/classes.registrystorage.js +116 -1
  10. package/dist_ts/core/helpers.stream.d.ts +20 -0
  11. package/dist_ts/core/helpers.stream.js +59 -0
  12. package/dist_ts/core/index.d.ts +1 -0
  13. package/dist_ts/core/index.js +3 -1
  14. package/dist_ts/core/interfaces.core.d.ts +28 -5
  15. package/dist_ts/maven/classes.mavenregistry.d.ts +14 -3
  16. package/dist_ts/maven/classes.mavenregistry.js +78 -27
  17. package/dist_ts/npm/classes.npmregistry.d.ts +14 -3
  18. package/dist_ts/npm/classes.npmregistry.js +121 -48
  19. package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
  20. package/dist_ts/oci/classes.ociregistry.js +187 -73
  21. package/dist_ts/oci/classes.ociupstream.d.ts +5 -2
  22. package/dist_ts/oci/classes.ociupstream.js +17 -10
  23. package/dist_ts/oci/interfaces.oci.d.ts +4 -0
  24. package/dist_ts/pypi/classes.pypiregistry.d.ts +8 -3
  25. package/dist_ts/pypi/classes.pypiregistry.js +88 -50
  26. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +8 -3
  27. package/dist_ts/rubygems/classes.rubygemsregistry.js +61 -23
  28. package/dist_ts/rubygems/helpers.rubygems.js +9 -11
  29. package/dist_ts/upstream/classes.upstreamcache.js +2 -2
  30. package/dist_ts/upstream/interfaces.upstream.d.ts +72 -1
  31. package/dist_ts/upstream/interfaces.upstream.js +24 -1
  32. package/package.json +24 -20
  33. package/readme.md +354 -812
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/cargo/classes.cargoregistry.ts +84 -37
  36. package/ts/classes.smartregistry.ts +49 -35
  37. package/ts/composer/classes.composerregistry.ts +74 -30
  38. package/ts/core/classes.registrystorage.ts +133 -2
  39. package/ts/core/helpers.stream.ts +63 -0
  40. package/ts/core/index.ts +3 -0
  41. package/ts/core/interfaces.core.ts +29 -5
  42. package/ts/maven/classes.mavenregistry.ts +89 -28
  43. package/ts/npm/classes.npmregistry.ts +134 -49
  44. package/ts/oci/classes.ociregistry.ts +206 -77
  45. package/ts/oci/classes.ociupstream.ts +18 -8
  46. package/ts/oci/interfaces.oci.ts +4 -0
  47. package/ts/pypi/classes.pypiregistry.ts +100 -54
  48. package/ts/rubygems/classes.rubygemsregistry.ts +69 -24
  49. package/ts/rubygems/helpers.rubygems.ts +8 -10
  50. package/ts/upstream/classes.upstreamcache.ts +1 -1
  51. package/ts/upstream/interfaces.upstream.ts +82 -1
  52. package/npmextra.json +0 -18
@@ -2,8 +2,8 @@ import { Smartlog } from '@push.rocks/smartlog';
2
2
  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
- import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
6
- import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
5
+ import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
6
+ import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
7
7
  import { NpmUpstream } from './classes.npmupstream.js';
8
8
  import type {
9
9
  IPackument,
@@ -27,20 +27,21 @@ export class NpmRegistry extends BaseRegistry {
27
27
  private basePath: string = '/npm';
28
28
  private registryUrl: string;
29
29
  private logger: Smartlog;
30
- private upstream: NpmUpstream | null = null;
30
+ private upstreamProvider: IUpstreamProvider | null = null;
31
31
 
32
32
  constructor(
33
33
  storage: RegistryStorage,
34
34
  authManager: AuthManager,
35
35
  basePath: string = '/npm',
36
36
  registryUrl: string = 'http://localhost:5000/npm',
37
- upstreamConfig?: IProtocolUpstreamConfig
37
+ upstreamProvider?: IUpstreamProvider
38
38
  ) {
39
39
  super();
40
40
  this.storage = storage;
41
41
  this.authManager = authManager;
42
42
  this.basePath = basePath;
43
43
  this.registryUrl = registryUrl;
44
+ this.upstreamProvider = upstreamProvider || null;
44
45
 
45
46
  // Initialize logger
46
47
  this.logger = new Smartlog({
@@ -55,13 +56,49 @@ export class NpmRegistry extends BaseRegistry {
55
56
  });
56
57
  this.logger.enableConsole();
57
58
 
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
- });
59
+ if (upstreamProvider) {
60
+ this.logger.log('info', 'NPM upstream provider configured');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Extract scope from npm package name.
66
+ * @example "@company/utils" -> "company"
67
+ * @example "lodash" -> null
68
+ */
69
+ private extractScope(packageName: string): string | null {
70
+ if (packageName.startsWith('@')) {
71
+ const slashIndex = packageName.indexOf('/');
72
+ if (slashIndex > 1) {
73
+ return packageName.substring(1, slashIndex);
74
+ }
64
75
  }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Get upstream for a specific request.
81
+ * Calls the provider to resolve upstream config dynamically.
82
+ */
83
+ private async getUpstreamForRequest(
84
+ resource: string,
85
+ resourceType: string,
86
+ method: string,
87
+ actor?: IRequestActor
88
+ ): Promise<NpmUpstream | null> {
89
+ if (!this.upstreamProvider) return null;
90
+
91
+ const config = await this.upstreamProvider.resolveUpstreamConfig({
92
+ protocol: 'npm',
93
+ resource,
94
+ scope: this.extractScope(resource),
95
+ actor,
96
+ method,
97
+ resourceType,
98
+ });
99
+
100
+ if (!config?.enabled) return null;
101
+ return new NpmUpstream(config, this.registryUrl, this.logger);
65
102
  }
66
103
 
67
104
  public async init(): Promise<void> {
@@ -80,6 +117,14 @@ export class NpmRegistry extends BaseRegistry {
80
117
  const tokenString = authHeader?.replace(/^Bearer\s+/i, '');
81
118
  const token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
82
119
 
120
+ // Build actor context for upstream resolution
121
+ const actor: IRequestActor = {
122
+ userId: token?.userId,
123
+ ip: context.headers['x-forwarded-for'] || context.headers['x-real-ip'],
124
+ userAgent: context.headers['user-agent'],
125
+ ...context.actor, // Include any pre-populated actor info
126
+ };
127
+
83
128
  this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
84
129
  method: context.method,
85
130
  path,
@@ -110,47 +155,47 @@ export class NpmRegistry extends BaseRegistry {
110
155
  // Dist-tags: /-/package/{package}/dist-tags
111
156
  const distTagsMatch = path.match(/^\/-\/package\/(@?[^\/]+(?:\/[^\/]+)?)\/dist-tags(?:\/(.+))?$/);
112
157
  if (distTagsMatch) {
113
- const [, packageName, tag] = distTagsMatch;
114
- return this.handleDistTags(context.method, packageName, tag, context.body, token);
158
+ const [, rawPkgName, tag] = distTagsMatch;
159
+ return this.handleDistTags(context.method, decodeURIComponent(rawPkgName), tag, context.body, token);
115
160
  }
116
161
 
117
162
  // Tarball download: /{package}/-/{filename}.tgz
118
163
  const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
119
164
  if (tarballMatch) {
120
- const [, packageName, filename] = tarballMatch;
121
- return this.handleTarballDownload(packageName, filename, token);
165
+ const [, rawPkgName, filename] = tarballMatch;
166
+ return this.handleTarballDownload(decodeURIComponent(rawPkgName), filename, token, actor);
122
167
  }
123
168
 
124
169
  // Unpublish specific version: DELETE /{package}/-/{version}
125
170
  const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
126
171
  if (unpublishVersionMatch && context.method === 'DELETE') {
127
- const [, packageName, version] = unpublishVersionMatch;
128
- this.logger.log('debug', 'unpublishVersionMatch', { packageName, version });
129
- return this.unpublishVersion(packageName, version, token);
172
+ const [, rawPkgName, version] = unpublishVersionMatch;
173
+ this.logger.log('debug', 'unpublishVersionMatch', { packageName: decodeURIComponent(rawPkgName), version });
174
+ return this.unpublishVersion(decodeURIComponent(rawPkgName), version, token);
130
175
  }
131
176
 
132
177
  // Unpublish entire package: DELETE /{package}/-rev/{rev}
133
178
  const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
134
179
  if (unpublishPackageMatch && context.method === 'DELETE') {
135
- const [, packageName, rev] = unpublishPackageMatch;
136
- this.logger.log('debug', 'unpublishPackageMatch', { packageName, rev });
137
- return this.unpublishPackage(packageName, token);
180
+ const [, rawPkgName, rev] = unpublishPackageMatch;
181
+ this.logger.log('debug', 'unpublishPackageMatch', { packageName: decodeURIComponent(rawPkgName), rev });
182
+ return this.unpublishPackage(decodeURIComponent(rawPkgName), token);
138
183
  }
139
184
 
140
185
  // Package version: /{package}/{version}
141
186
  const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
142
187
  if (versionMatch) {
143
- const [, packageName, version] = versionMatch;
144
- this.logger.log('debug', 'versionMatch', { packageName, version });
145
- return this.handlePackageVersion(packageName, version, token);
188
+ const [, rawPkgName, version] = versionMatch;
189
+ this.logger.log('debug', 'versionMatch', { packageName: decodeURIComponent(rawPkgName), version });
190
+ return this.handlePackageVersion(decodeURIComponent(rawPkgName), version, token, actor);
146
191
  }
147
192
 
148
193
  // Package operations: /{package}
149
194
  const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
150
195
  if (packageMatch) {
151
- const packageName = packageMatch[1];
196
+ const packageName = decodeURIComponent(packageMatch[1]);
152
197
  this.logger.log('debug', 'packageMatch', { packageName });
153
- return this.handlePackage(context.method, packageName, context.body, context.query, token);
198
+ return this.handlePackage(context.method, packageName, context.body, context.query, token, actor);
154
199
  }
155
200
 
156
201
  return {
@@ -198,11 +243,12 @@ export class NpmRegistry extends BaseRegistry {
198
243
  packageName: string,
199
244
  body: any,
200
245
  query: Record<string, string>,
201
- token: IAuthToken | null
246
+ token: IAuthToken | null,
247
+ actor?: IRequestActor
202
248
  ): Promise<IResponse> {
203
249
  switch (method) {
204
250
  case 'GET':
205
- return this.getPackument(packageName, token, query);
251
+ return this.getPackument(packageName, token, query, actor);
206
252
  case 'PUT':
207
253
  return this.publishPackage(packageName, body, token);
208
254
  case 'DELETE':
@@ -219,7 +265,8 @@ export class NpmRegistry extends BaseRegistry {
219
265
  private async getPackument(
220
266
  packageName: string,
221
267
  token: IAuthToken | null,
222
- query: Record<string, string>
268
+ query: Record<string, string>,
269
+ actor?: IRequestActor
223
270
  ): Promise<IResponse> {
224
271
  let packument = await this.storage.getNpmPackument(packageName);
225
272
  this.logger.log('debug', `getPackument: ${packageName}`, {
@@ -229,17 +276,20 @@ export class NpmRegistry extends BaseRegistry {
229
276
  });
230
277
 
231
278
  // 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
279
+ if (!packument) {
280
+ const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
281
+ if (upstream) {
282
+ this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
283
+ const upstreamPackument = await upstream.fetchPackument(packageName);
284
+ if (upstreamPackument) {
285
+ this.logger.log('debug', `getPackument: found in upstream`, {
286
+ packageName,
287
+ versions: Object.keys(upstreamPackument.versions || {}).length
288
+ });
289
+ packument = upstreamPackument;
290
+ // Optionally cache the packument locally (without tarballs)
291
+ // We don't store tarballs here - they'll be fetched on demand
292
+ }
243
293
  }
244
294
  }
245
295
 
@@ -279,7 +329,8 @@ export class NpmRegistry extends BaseRegistry {
279
329
  private async handlePackageVersion(
280
330
  packageName: string,
281
331
  version: string,
282
- token: IAuthToken | null
332
+ token: IAuthToken | null,
333
+ actor?: IRequestActor
283
334
  ): Promise<IResponse> {
284
335
  this.logger.log('debug', 'handlePackageVersion', { packageName, version });
285
336
  let packument = await this.storage.getNpmPackument(packageName);
@@ -289,11 +340,14 @@ export class NpmRegistry extends BaseRegistry {
289
340
  }
290
341
 
291
342
  // 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;
343
+ if (!packument) {
344
+ const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
345
+ if (upstream) {
346
+ this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
347
+ const upstreamPackument = await upstream.fetchPackument(packageName);
348
+ if (upstreamPackument) {
349
+ packument = upstreamPackument;
350
+ }
297
351
  }
298
352
  }
299
353
 
@@ -563,7 +617,8 @@ export class NpmRegistry extends BaseRegistry {
563
617
  private async handleTarballDownload(
564
618
  packageName: string,
565
619
  filename: string,
566
- token: IAuthToken | null
620
+ token: IAuthToken | null,
621
+ actor?: IRequestActor
567
622
  ): Promise<IResponse> {
568
623
  // Extract version from filename: package-name-1.0.0.tgz
569
624
  const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/i);
@@ -576,15 +631,29 @@ export class NpmRegistry extends BaseRegistry {
576
631
  }
577
632
 
578
633
  const version = versionMatch[1];
579
- let tarball = await this.storage.getNpmTarball(packageName, version);
634
+
635
+ // Try local storage first (streaming)
636
+ const streamResult = await this.storage.getNpmTarballStream(packageName, version);
637
+ if (streamResult) {
638
+ return {
639
+ status: 200,
640
+ headers: {
641
+ 'Content-Type': 'application/octet-stream',
642
+ 'Content-Length': streamResult.size.toString(),
643
+ },
644
+ body: streamResult.stream,
645
+ };
646
+ }
580
647
 
581
648
  // If not found locally, try upstream
582
- if (!tarball && this.upstream) {
649
+ let tarball: Buffer | null = null;
650
+ const upstream = await this.getUpstreamForRequest(packageName, 'tarball', 'GET', actor);
651
+ if (upstream) {
583
652
  this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
584
653
  packageName,
585
654
  version,
586
655
  });
587
- const upstreamTarball = await this.upstream.fetchTarball(packageName, version);
656
+ const upstreamTarball = await upstream.fetchTarball(packageName, version);
588
657
  if (upstreamTarball) {
589
658
  tarball = upstreamTarball;
590
659
  // Cache the tarball locally for future requests
@@ -680,6 +749,22 @@ export class NpmRegistry extends BaseRegistry {
680
749
  this.logger.log('error', 'handleSearch failed', { error: (error as Error).message });
681
750
  }
682
751
 
752
+ // Sort results by relevance: exact match first, then prefix match, then substring match
753
+ if (text) {
754
+ const lowerText = text.toLowerCase();
755
+ results.sort((a, b) => {
756
+ const aName = a.package.name.toLowerCase();
757
+ const bName = b.package.name.toLowerCase();
758
+ const aExact = aName === lowerText ? 0 : 1;
759
+ const bExact = bName === lowerText ? 0 : 1;
760
+ if (aExact !== bExact) return aExact - bExact;
761
+ const aPrefix = aName.startsWith(lowerText) ? 0 : 1;
762
+ const bPrefix = bName.startsWith(lowerText) ? 0 : 1;
763
+ if (aPrefix !== bPrefix) return aPrefix - bPrefix;
764
+ return aName.localeCompare(bName);
765
+ });
766
+ }
767
+
683
768
  // Apply pagination
684
769
  const paginatedResults = results.slice(from, from + size);
685
770