@push.rocks/smartregistry 2.1.2 → 2.2.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.
@@ -198,7 +198,7 @@ export class OciRegistry extends BaseRegistry {
198
198
  const digest = query.digest;
199
199
  if (digest && body) {
200
200
  // Monolithic upload: complete upload in single POST
201
- const blobData = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
201
+ const blobData = this.toBuffer(body);
202
202
 
203
203
  // Verify digest
204
204
  const calculatedDigest = await this.calculateDigest(blobData);
@@ -320,10 +320,17 @@ export class OciRegistry extends BaseRegistry {
320
320
  };
321
321
  }
322
322
 
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
+
323
330
  return {
324
331
  status: 200,
325
332
  headers: {
326
- 'Content-Type': 'application/vnd.oci.image.manifest.v1+json',
333
+ 'Content-Type': contentType,
327
334
  'Docker-Content-Digest': digest,
328
335
  },
329
336
  body: manifestData,
@@ -356,10 +363,18 @@ export class OciRegistry extends BaseRegistry {
356
363
 
357
364
  const manifestData = await this.storage.getOciManifest(repository, digest);
358
365
 
366
+ // Get stored content type, falling back to detecting from manifest content
367
+ let contentType = await this.storage.getOciManifestContentType(repository, digest);
368
+ if (!contentType && manifestData) {
369
+ // Fallback: detect content type from manifest content
370
+ contentType = this.detectManifestContentType(manifestData);
371
+ }
372
+ contentType = contentType || 'application/vnd.oci.image.manifest.v1+json';
373
+
359
374
  return {
360
375
  status: 200,
361
376
  headers: {
362
- 'Content-Type': 'application/vnd.oci.image.manifest.v1+json',
377
+ 'Content-Type': contentType,
363
378
  'Docker-Content-Digest': digest,
364
379
  'Content-Length': manifestData ? manifestData.length.toString() : '0',
365
380
  },
@@ -388,16 +403,7 @@ export class OciRegistry extends BaseRegistry {
388
403
 
389
404
  // Preserve raw bytes for accurate digest calculation
390
405
  // Per OCI spec, digest must match the exact bytes sent by client
391
- let manifestData: Buffer;
392
- if (Buffer.isBuffer(body)) {
393
- manifestData = body;
394
- } else if (typeof body === 'string') {
395
- // String body - convert directly without JSON transformation
396
- manifestData = Buffer.from(body, 'utf-8');
397
- } else {
398
- // Body was already parsed as JSON object - re-serialize as fallback
399
- manifestData = Buffer.from(JSON.stringify(body));
400
- }
406
+ const manifestData = this.toBuffer(body);
401
407
  const contentType = headers?.['content-type'] || headers?.['Content-Type'] || 'application/vnd.oci.image.manifest.v1+json';
402
408
 
403
409
  // Calculate manifest digest
@@ -525,7 +531,7 @@ export class OciRegistry extends BaseRegistry {
525
531
 
526
532
  private async uploadChunk(
527
533
  uploadId: string,
528
- data: Buffer,
534
+ data: Buffer | Uint8Array | unknown,
529
535
  contentRange: string
530
536
  ): Promise<IResponse> {
531
537
  const session = this.uploadSessions.get(uploadId);
@@ -537,8 +543,9 @@ export class OciRegistry extends BaseRegistry {
537
543
  };
538
544
  }
539
545
 
540
- session.chunks.push(data);
541
- session.totalSize += data.length;
546
+ const chunkData = this.toBuffer(data);
547
+ session.chunks.push(chunkData);
548
+ session.totalSize += chunkData.length;
542
549
  session.lastActivity = new Date();
543
550
 
544
551
  return {
@@ -555,7 +562,7 @@ export class OciRegistry extends BaseRegistry {
555
562
  private async completeUpload(
556
563
  uploadId: string,
557
564
  digest: string,
558
- finalData?: Buffer
565
+ finalData?: Buffer | Uint8Array | unknown
559
566
  ): Promise<IResponse> {
560
567
  const session = this.uploadSessions.get(uploadId);
561
568
  if (!session) {
@@ -567,7 +574,7 @@ export class OciRegistry extends BaseRegistry {
567
574
  }
568
575
 
569
576
  const chunks = [...session.chunks];
570
- if (finalData) chunks.push(finalData);
577
+ if (finalData) chunks.push(this.toBuffer(finalData));
571
578
  const blobData = Buffer.concat(chunks);
572
579
 
573
580
  // Verify digest
@@ -665,6 +672,59 @@ export class OciRegistry extends BaseRegistry {
665
672
  // HELPER METHODS
666
673
  // ========================================================================
667
674
 
675
+ /**
676
+ * Detect manifest content type from manifest content.
677
+ * OCI Image Index has "manifests" array, OCI Image Manifest has "config" object.
678
+ * Also checks the mediaType field if present.
679
+ */
680
+ private detectManifestContentType(manifestData: Buffer): string {
681
+ try {
682
+ const manifest = JSON.parse(manifestData.toString('utf-8'));
683
+
684
+ // First check if manifest has explicit mediaType field
685
+ if (manifest.mediaType) {
686
+ return manifest.mediaType;
687
+ }
688
+
689
+ // Otherwise detect from structure
690
+ if (Array.isArray(manifest.manifests)) {
691
+ // OCI Image Index (multi-arch manifest list)
692
+ return 'application/vnd.oci.image.index.v1+json';
693
+ } else if (manifest.config) {
694
+ // OCI Image Manifest
695
+ return 'application/vnd.oci.image.manifest.v1+json';
696
+ }
697
+
698
+ // Fallback to standard manifest type
699
+ return 'application/vnd.oci.image.manifest.v1+json';
700
+ } catch (e) {
701
+ // If parsing fails, return default
702
+ return 'application/vnd.oci.image.manifest.v1+json';
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Convert any binary-like data to Buffer.
708
+ * Handles Buffer, Uint8Array (modern cross-platform), string, and objects.
709
+ *
710
+ * Note: Buffer.isBuffer(Uint8Array) returns false even though Buffer extends Uint8Array.
711
+ * This is because Uint8Array is the modern, cross-platform standard while Buffer is Node.js-specific.
712
+ * Many HTTP frameworks pass request bodies as Uint8Array for better compatibility.
713
+ */
714
+ private toBuffer(data: unknown): Buffer {
715
+ if (Buffer.isBuffer(data)) {
716
+ return data;
717
+ }
718
+ if (data instanceof Uint8Array) {
719
+ return Buffer.from(data);
720
+ }
721
+ if (typeof data === 'string') {
722
+ return Buffer.from(data, 'utf-8');
723
+ }
724
+ // Fallback: serialize object to JSON (may cause digest mismatch for manifests)
725
+ return Buffer.from(JSON.stringify(data));
726
+ }
727
+
668
728
  private async getTagsData(repository: string): Promise<Record<string, string>> {
669
729
  const path = `oci/tags/${repository}/tags.json`;
670
730
  const data = await this.storage.getObject(path);
@@ -678,7 +738,7 @@ export class OciRegistry extends BaseRegistry {
678
738
  }
679
739
 
680
740
  private generateUploadId(): string {
681
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
741
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
682
742
  }
683
743
 
684
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