@push.rocks/smartregistry 2.2.0 → 2.2.2

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.
@@ -52,37 +52,40 @@ export class AuthManager {
52
52
  return token;
53
53
  }
54
54
 
55
- // ========================================================================
56
- // NPM AUTHENTICATION
57
- // ========================================================================
58
-
59
55
  /**
60
- * Create an NPM token
56
+ * Generic protocol token creation (internal helper)
61
57
  * @param userId - User ID
58
+ * @param protocol - Protocol type (npm, maven, composer, etc.)
62
59
  * @param readonly - Whether the token is readonly
63
- * @returns NPM UUID token
60
+ * @returns UUID token string
64
61
  */
65
- public async createNpmToken(userId: string, readonly: boolean = false): Promise<string> {
66
- if (!this.config.npmTokens.enabled) {
67
- throw new Error('NPM tokens are not enabled');
68
- }
69
-
70
- const scopes = readonly ? ['npm:*:*:read'] : ['npm:*:*:*'];
71
- return this.createUuidToken(userId, 'npm', scopes, readonly);
62
+ private async createProtocolToken(
63
+ userId: string,
64
+ protocol: TRegistryProtocol,
65
+ readonly: boolean
66
+ ): Promise<string> {
67
+ const scopes = readonly
68
+ ? [`${protocol}:*:*:read`]
69
+ : [`${protocol}:*:*:*`];
70
+ return this.createUuidToken(userId, protocol, scopes, readonly);
72
71
  }
73
72
 
74
73
  /**
75
- * Validate an NPM token
76
- * @param token - NPM UUID token
74
+ * Generic protocol token validation (internal helper)
75
+ * @param token - UUID token string
76
+ * @param protocol - Expected protocol type
77
77
  * @returns Auth token object or null
78
78
  */
79
- public async validateNpmToken(token: string): Promise<IAuthToken | null> {
79
+ private async validateProtocolToken(
80
+ token: string,
81
+ protocol: TRegistryProtocol
82
+ ): Promise<IAuthToken | null> {
80
83
  if (!this.isValidUuid(token)) {
81
84
  return null;
82
85
  }
83
86
 
84
87
  const authToken = this.tokenStore.get(token);
85
- if (!authToken || authToken.type !== 'npm') {
88
+ if (!authToken || authToken.type !== protocol) {
86
89
  return null;
87
90
  }
88
91
 
@@ -95,12 +98,46 @@ export class AuthManager {
95
98
  return authToken;
96
99
  }
97
100
 
101
+ /**
102
+ * Generic protocol token revocation (internal helper)
103
+ * @param token - UUID token string
104
+ */
105
+ private async revokeProtocolToken(token: string): Promise<void> {
106
+ this.tokenStore.delete(token);
107
+ }
108
+
109
+ // ========================================================================
110
+ // NPM AUTHENTICATION
111
+ // ========================================================================
112
+
113
+ /**
114
+ * Create an NPM token
115
+ * @param userId - User ID
116
+ * @param readonly - Whether the token is readonly
117
+ * @returns NPM UUID token
118
+ */
119
+ public async createNpmToken(userId: string, readonly: boolean = false): Promise<string> {
120
+ if (!this.config.npmTokens.enabled) {
121
+ throw new Error('NPM tokens are not enabled');
122
+ }
123
+ return this.createProtocolToken(userId, 'npm', readonly);
124
+ }
125
+
126
+ /**
127
+ * Validate an NPM token
128
+ * @param token - NPM UUID token
129
+ * @returns Auth token object or null
130
+ */
131
+ public async validateNpmToken(token: string): Promise<IAuthToken | null> {
132
+ return this.validateProtocolToken(token, 'npm');
133
+ }
134
+
98
135
  /**
99
136
  * Revoke an NPM token
100
137
  * @param token - NPM UUID token
101
138
  */
102
139
  public async revokeNpmToken(token: string): Promise<void> {
103
- this.tokenStore.delete(token);
140
+ return this.revokeProtocolToken(token);
104
141
  }
105
142
 
106
143
  /**
@@ -265,8 +302,7 @@ export class AuthManager {
265
302
  * @returns Maven UUID token
266
303
  */
267
304
  public async createMavenToken(userId: string, readonly: boolean = false): Promise<string> {
268
- const scopes = readonly ? ['maven:*:*:read'] : ['maven:*:*:*'];
269
- return this.createUuidToken(userId, 'maven', scopes, readonly);
305
+ return this.createProtocolToken(userId, 'maven', readonly);
270
306
  }
271
307
 
272
308
  /**
@@ -275,22 +311,7 @@ export class AuthManager {
275
311
  * @returns Auth token object or null
276
312
  */
277
313
  public async validateMavenToken(token: string): Promise<IAuthToken | null> {
278
- if (!this.isValidUuid(token)) {
279
- return null;
280
- }
281
-
282
- const authToken = this.tokenStore.get(token);
283
- if (!authToken || authToken.type !== 'maven') {
284
- return null;
285
- }
286
-
287
- // Check expiration if set
288
- if (authToken.expiresAt && authToken.expiresAt < new Date()) {
289
- this.tokenStore.delete(token);
290
- return null;
291
- }
292
-
293
- return authToken;
314
+ return this.validateProtocolToken(token, 'maven');
294
315
  }
295
316
 
296
317
  /**
@@ -298,7 +319,7 @@ export class AuthManager {
298
319
  * @param token - Maven UUID token
299
320
  */
300
321
  public async revokeMavenToken(token: string): Promise<void> {
301
- this.tokenStore.delete(token);
322
+ return this.revokeProtocolToken(token);
302
323
  }
303
324
 
304
325
  // ========================================================================
@@ -312,8 +333,7 @@ export class AuthManager {
312
333
  * @returns Composer UUID token
313
334
  */
314
335
  public async createComposerToken(userId: string, readonly: boolean = false): Promise<string> {
315
- const scopes = readonly ? ['composer:*:*:read'] : ['composer:*:*:*'];
316
- return this.createUuidToken(userId, 'composer', scopes, readonly);
336
+ return this.createProtocolToken(userId, 'composer', readonly);
317
337
  }
318
338
 
319
339
  /**
@@ -322,22 +342,7 @@ export class AuthManager {
322
342
  * @returns Auth token object or null
323
343
  */
324
344
  public async validateComposerToken(token: string): Promise<IAuthToken | null> {
325
- if (!this.isValidUuid(token)) {
326
- return null;
327
- }
328
-
329
- const authToken = this.tokenStore.get(token);
330
- if (!authToken || authToken.type !== 'composer') {
331
- return null;
332
- }
333
-
334
- // Check expiration if set
335
- if (authToken.expiresAt && authToken.expiresAt < new Date()) {
336
- this.tokenStore.delete(token);
337
- return null;
338
- }
339
-
340
- return authToken;
345
+ return this.validateProtocolToken(token, 'composer');
341
346
  }
342
347
 
343
348
  /**
@@ -345,7 +350,7 @@ export class AuthManager {
345
350
  * @param token - Composer UUID token
346
351
  */
347
352
  public async revokeComposerToken(token: string): Promise<void> {
348
- this.tokenStore.delete(token);
353
+ return this.revokeProtocolToken(token);
349
354
  }
350
355
 
351
356
  // ========================================================================
@@ -359,8 +364,7 @@ export class AuthManager {
359
364
  * @returns Cargo UUID token
360
365
  */
361
366
  public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> {
362
- const scopes = readonly ? ['cargo:*:*:read'] : ['cargo:*:*:*'];
363
- return this.createUuidToken(userId, 'cargo', scopes, readonly);
367
+ return this.createProtocolToken(userId, 'cargo', readonly);
364
368
  }
365
369
 
366
370
  /**
@@ -369,22 +373,7 @@ export class AuthManager {
369
373
  * @returns Auth token object or null
370
374
  */
371
375
  public async validateCargoToken(token: string): Promise<IAuthToken | null> {
372
- if (!this.isValidUuid(token)) {
373
- return null;
374
- }
375
-
376
- const authToken = this.tokenStore.get(token);
377
- if (!authToken || authToken.type !== 'cargo') {
378
- return null;
379
- }
380
-
381
- // Check expiration if set
382
- if (authToken.expiresAt && authToken.expiresAt < new Date()) {
383
- this.tokenStore.delete(token);
384
- return null;
385
- }
386
-
387
- return authToken;
376
+ return this.validateProtocolToken(token, 'cargo');
388
377
  }
389
378
 
390
379
  /**
@@ -392,7 +381,7 @@ export class AuthManager {
392
381
  * @param token - Cargo UUID token
393
382
  */
394
383
  public async revokeCargoToken(token: string): Promise<void> {
395
- this.tokenStore.delete(token);
384
+ return this.revokeProtocolToken(token);
396
385
  }
397
386
 
398
387
  // ========================================================================
@@ -406,8 +395,7 @@ export class AuthManager {
406
395
  * @returns PyPI UUID token
407
396
  */
408
397
  public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> {
409
- const scopes = readonly ? ['pypi:*:*:read'] : ['pypi:*:*:*'];
410
- return this.createUuidToken(userId, 'pypi', scopes, readonly);
398
+ return this.createProtocolToken(userId, 'pypi', readonly);
411
399
  }
412
400
 
413
401
  /**
@@ -416,22 +404,7 @@ export class AuthManager {
416
404
  * @returns Auth token object or null
417
405
  */
418
406
  public async validatePypiToken(token: string): Promise<IAuthToken | null> {
419
- if (!this.isValidUuid(token)) {
420
- return null;
421
- }
422
-
423
- const authToken = this.tokenStore.get(token);
424
- if (!authToken || authToken.type !== 'pypi') {
425
- return null;
426
- }
427
-
428
- // Check expiration if set
429
- if (authToken.expiresAt && authToken.expiresAt < new Date()) {
430
- this.tokenStore.delete(token);
431
- return null;
432
- }
433
-
434
- return authToken;
407
+ return this.validateProtocolToken(token, 'pypi');
435
408
  }
436
409
 
437
410
  /**
@@ -439,7 +412,7 @@ export class AuthManager {
439
412
  * @param token - PyPI UUID token
440
413
  */
441
414
  public async revokePypiToken(token: string): Promise<void> {
442
- this.tokenStore.delete(token);
415
+ return this.revokeProtocolToken(token);
443
416
  }
444
417
 
445
418
  // ========================================================================
@@ -453,8 +426,7 @@ export class AuthManager {
453
426
  * @returns RubyGems UUID token
454
427
  */
455
428
  public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> {
456
- const scopes = readonly ? ['rubygems:*:*:read'] : ['rubygems:*:*:*'];
457
- return this.createUuidToken(userId, 'rubygems', scopes, readonly);
429
+ return this.createProtocolToken(userId, 'rubygems', readonly);
458
430
  }
459
431
 
460
432
  /**
@@ -463,22 +435,7 @@ export class AuthManager {
463
435
  * @returns Auth token object or null
464
436
  */
465
437
  public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> {
466
- if (!this.isValidUuid(token)) {
467
- return null;
468
- }
469
-
470
- const authToken = this.tokenStore.get(token);
471
- if (!authToken || authToken.type !== 'rubygems') {
472
- return null;
473
- }
474
-
475
- // Check expiration if set
476
- if (authToken.expiresAt && authToken.expiresAt < new Date()) {
477
- this.tokenStore.delete(token);
478
- return null;
479
- }
480
-
481
- return authToken;
438
+ return this.validateProtocolToken(token, 'rubygems');
482
439
  }
483
440
 
484
441
  /**
@@ -486,7 +443,7 @@ export class AuthManager {
486
443
  * @param token - RubyGems UUID token
487
444
  */
488
445
  public async revokeRubyGemsToken(token: string): Promise<void> {
489
- this.tokenStore.delete(token);
446
+ return this.revokeProtocolToken(token);
490
447
  }
491
448
 
492
449
  // ========================================================================
@@ -495,57 +452,42 @@ export class AuthManager {
495
452
 
496
453
  /**
497
454
  * Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
455
+ * Optimized: O(1) lookup when protocol hint provided
498
456
  * @param tokenString - Token string (UUID or JWT)
499
- * @param protocol - Expected protocol type
457
+ * @param protocol - Expected protocol type (optional, improves performance)
500
458
  * @returns Auth token object or null
501
459
  */
502
460
  public async validateToken(
503
461
  tokenString: string,
504
462
  protocol?: TRegistryProtocol
505
463
  ): Promise<IAuthToken | null> {
506
- // Try UUID-based tokens (NPM, Maven, Composer, Cargo, PyPI, RubyGems)
507
- if (this.isValidUuid(tokenString)) {
508
- // Try NPM token
509
- const npmToken = await this.validateNpmToken(tokenString);
510
- if (npmToken && (!protocol || protocol === 'npm')) {
511
- return npmToken;
512
- }
513
-
514
- // Try Maven token
515
- const mavenToken = await this.validateMavenToken(tokenString);
516
- if (mavenToken && (!protocol || protocol === 'maven')) {
517
- return mavenToken;
518
- }
519
-
520
- // Try Composer token
521
- const composerToken = await this.validateComposerToken(tokenString);
522
- if (composerToken && (!protocol || protocol === 'composer')) {
523
- return composerToken;
524
- }
525
-
526
- // Try Cargo token
527
- const cargoToken = await this.validateCargoToken(tokenString);
528
- if (cargoToken && (!protocol || protocol === 'cargo')) {
529
- return cargoToken;
530
- }
531
-
532
- // Try PyPI token
533
- const pypiToken = await this.validatePypiToken(tokenString);
534
- if (pypiToken && (!protocol || protocol === 'pypi')) {
535
- return pypiToken;
464
+ // OCI uses JWT (contains dots), not UUID - check first if OCI is expected
465
+ if (protocol === 'oci' || tokenString.includes('.')) {
466
+ const ociToken = await this.validateOciToken(tokenString);
467
+ if (ociToken && (!protocol || protocol === 'oci')) {
468
+ return ociToken;
536
469
  }
537
-
538
- // Try RubyGems token
539
- const rubygemsToken = await this.validateRubyGemsToken(tokenString);
540
- if (rubygemsToken && (!protocol || protocol === 'rubygems')) {
541
- return rubygemsToken;
470
+ // If protocol was explicitly OCI but validation failed, return null
471
+ if (protocol === 'oci') {
472
+ return null;
542
473
  }
543
474
  }
544
475
 
545
- // Try OCI JWT
546
- const ociToken = await this.validateOciToken(tokenString);
547
- if (ociToken && (!protocol || protocol === 'oci')) {
548
- return ociToken;
476
+ // UUID-based tokens: single O(1) Map lookup
477
+ if (this.isValidUuid(tokenString)) {
478
+ const authToken = this.tokenStore.get(tokenString);
479
+ if (authToken) {
480
+ // If protocol specified, verify it matches
481
+ if (protocol && authToken.type !== protocol) {
482
+ return null;
483
+ }
484
+ // Check expiration
485
+ if (authToken.expiresAt && authToken.expiresAt < new Date()) {
486
+ this.tokenStore.delete(tokenString);
487
+ return null;
488
+ }
489
+ return authToken;
490
+ }
549
491
  }
550
492
 
551
493
  return null;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared buffer utilities for consistent binary data handling across all registry types.
3
+ *
4
+ * This module addresses the common issue where `Buffer.isBuffer(Uint8Array)` returns `false`,
5
+ * which can cause data handling bugs when binary data arrives as Uint8Array instead of Buffer.
6
+ */
7
+
8
+ /**
9
+ * Check if value is binary data (Buffer or Uint8Array)
10
+ */
11
+ export function isBinaryData(value: unknown): value is Buffer | Uint8Array {
12
+ return Buffer.isBuffer(value) || value instanceof Uint8Array;
13
+ }
14
+
15
+ /**
16
+ * Convert any binary-like data to Buffer.
17
+ * Handles Buffer, Uint8Array, string, and objects.
18
+ *
19
+ * @param data - The data to convert to Buffer
20
+ * @returns A Buffer containing the data
21
+ */
22
+ export function toBuffer(data: unknown): Buffer {
23
+ if (Buffer.isBuffer(data)) {
24
+ return data;
25
+ }
26
+ if (data instanceof Uint8Array) {
27
+ return Buffer.from(data);
28
+ }
29
+ if (typeof data === 'string') {
30
+ return Buffer.from(data, 'utf-8');
31
+ }
32
+ // Fallback: serialize object to JSON
33
+ return Buffer.from(JSON.stringify(data));
34
+ }
@@ -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 { toBuffer } from '../core/helpers.buffer.js';
10
11
  import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
11
12
  import {
12
13
  pathToGAV,
@@ -296,7 +297,7 @@ export class MavenRegistry extends BaseRegistry {
296
297
  coordinate: IMavenCoordinate,
297
298
  body: Buffer | any
298
299
  ): Promise<IResponse> {
299
- const data = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
300
+ const data = toBuffer(body);
300
301
 
301
302
  // Validate POM if uploading .pom file
302
303
  if (coordinate.extension === 'pom') {
@@ -113,7 +113,7 @@ export class NpmRegistry extends BaseRegistry {
113
113
  const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
114
114
  if (unpublishVersionMatch && context.method === 'DELETE') {
115
115
  const [, packageName, version] = unpublishVersionMatch;
116
- console.log(`[unpublishVersionMatch] packageName=${packageName}, version=${version}`);
116
+ this.logger.log('debug', 'unpublishVersionMatch', { packageName, version });
117
117
  return this.unpublishVersion(packageName, version, token);
118
118
  }
119
119
 
@@ -121,7 +121,7 @@ export class NpmRegistry extends BaseRegistry {
121
121
  const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
122
122
  if (unpublishPackageMatch && context.method === 'DELETE') {
123
123
  const [, packageName, rev] = unpublishPackageMatch;
124
- console.log(`[unpublishPackageMatch] packageName=${packageName}, rev=${rev}`);
124
+ this.logger.log('debug', 'unpublishPackageMatch', { packageName, rev });
125
125
  return this.unpublishPackage(packageName, token);
126
126
  }
127
127
 
@@ -129,7 +129,7 @@ export class NpmRegistry extends BaseRegistry {
129
129
  const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
130
130
  if (versionMatch) {
131
131
  const [, packageName, version] = versionMatch;
132
- console.log(`[versionMatch] matched! packageName=${packageName}, version=${version}`);
132
+ this.logger.log('debug', 'versionMatch', { packageName, version });
133
133
  return this.handlePackageVersion(packageName, version, token);
134
134
  }
135
135
 
@@ -137,7 +137,7 @@ export class NpmRegistry extends BaseRegistry {
137
137
  const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
138
138
  if (packageMatch) {
139
139
  const packageName = packageMatch[1];
140
- console.log(`[packageMatch] matched! packageName=${packageName}`);
140
+ this.logger.log('debug', 'packageMatch', { packageName });
141
141
  return this.handlePackage(context.method, packageName, context.body, context.query, token);
142
142
  }
143
143
 
@@ -254,11 +254,11 @@ export class NpmRegistry extends BaseRegistry {
254
254
  version: string,
255
255
  token: IAuthToken | null
256
256
  ): Promise<IResponse> {
257
- console.log(`[handlePackageVersion] packageName=${packageName}, version=${version}`);
257
+ this.logger.log('debug', 'handlePackageVersion', { packageName, version });
258
258
  const packument = await this.storage.getNpmPackument(packageName);
259
- console.log(`[handlePackageVersion] packument found:`, !!packument);
259
+ this.logger.log('debug', 'handlePackageVersion packument', { found: !!packument });
260
260
  if (packument) {
261
- console.log(`[handlePackageVersion] versions:`, Object.keys(packument.versions || {}));
261
+ this.logger.log('debug', 'handlePackageVersion versions', { versions: Object.keys(packument.versions || {}) });
262
262
  }
263
263
  if (!packument) {
264
264
  return {
@@ -621,7 +621,7 @@ export class NpmRegistry extends BaseRegistry {
621
621
  }
622
622
  }
623
623
  } catch (error) {
624
- console.error('[handleSearch] Error:', error);
624
+ this.logger.log('error', 'handleSearch failed', { error: (error as Error).message });
625
625
  }
626
626
 
627
627
  // Apply pagination
@@ -738,7 +738,7 @@ export class OciRegistry extends BaseRegistry {
738
738
  }
739
739
 
740
740
  private generateUploadId(): string {
741
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
741
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
742
742
  }
743
743
 
744
744
  private async calculateDigest(data: Buffer): Promise<string> {
@@ -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 { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
6
7
  import type {
7
8
  IPypiPackageMetadata,
8
9
  IPypiFile,
@@ -328,8 +329,9 @@ export class PypiRegistry extends BaseRegistry {
328
329
  const version = formData.version;
329
330
  // Support both: formData.content.filename (multipart parsed) and formData.filename (flat)
330
331
  const filename = formData.content?.filename || formData.filename;
331
- // Support both: formData.content.data (multipart parsed) and formData.content (Buffer directly)
332
- const fileData = (formData.content?.data || (Buffer.isBuffer(formData.content) ? formData.content : null)) as Buffer;
332
+ // Support both: formData.content.data (multipart parsed) and formData.content (Buffer/Uint8Array directly)
333
+ const rawContent = formData.content?.data || (isBinaryData(formData.content) ? formData.content : null);
334
+ const fileData = rawContent ? toBuffer(rawContent) : null;
333
335
  const filetype = formData.filetype; // 'bdist_wheel' or 'sdist'
334
336
  const pyversion = formData.pyversion;
335
337
 
@@ -455,9 +455,8 @@ export async function extractGemMetadata(gemData: Buffer): Promise<{
455
455
  }
456
456
 
457
457
  return null;
458
- } catch (error) {
459
- // Log error for debugging but return null gracefully
460
- console.error('Failed to extract gem metadata:', error);
458
+ } catch (_error) {
459
+ // Error handled gracefully - return null and let caller handle missing metadata
461
460
  return null;
462
461
  }
463
462
  }