From a2f7f4302781ff435792df2ddc4a33c2d2eaf02d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 25 Nov 2025 17:15:47 +0000 Subject: [PATCH] fix(oci): Prefer raw request body for content-addressable OCI operations and expose rawBody on request context --- changelog.md | 7 +++++++ ts/00_commitinfo_data.ts | 2 +- ts/core/interfaces.core.ts | 6 ++++++ ts/oci/classes.ociregistry.ts | 15 +++++++++++---- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index b65db92..7fc6220 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-11-25 - 2.1.2 - fix(oci) +Prefer raw request body for content-addressable OCI operations and expose rawBody on request context + +- Add rawBody?: Buffer to IRequestContext to allow callers to provide the exact raw request bytes for digest calculation (falls back to body if absent). +- OCI registry handlers now prefer context.rawBody over context.body for content-addressable operations (manifests, blobs, and blob uploads) to preserve exact bytes and ensure digest calculation matches client expectations. +- Upload flow updates: upload init, PATCH (upload chunk) and PUT (complete upload) now pass rawBody when available. + ## 2025-11-25 - 2.1.1 - fix(oci) Preserve raw manifest bytes for digest calculation and handle string/JSON manifest bodies in OCI registry diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8eaeef5..29cf398 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartregistry', - version: '2.1.1', + version: '2.1.2', description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries' } diff --git a/ts/core/interfaces.core.ts b/ts/core/interfaces.core.ts index 019fb0e..62c1c28 100644 --- a/ts/core/interfaces.core.ts +++ b/ts/core/interfaces.core.ts @@ -158,6 +158,12 @@ export interface IRequestContext { headers: Record; query: Record; body?: any; + /** + * Raw request body as bytes. MUST be provided for content-addressable operations + * (OCI manifests, blobs) to ensure digest calculation matches client expectations. + * If not provided, falls back to 'body' field. + */ + rawBody?: Buffer; token?: string; } diff --git a/ts/oci/classes.ociregistry.ts b/ts/oci/classes.ociregistry.ts index 07241f4..1a49634 100644 --- a/ts/oci/classes.ociregistry.ts +++ b/ts/oci/classes.ociregistry.ts @@ -62,7 +62,9 @@ export class OciRegistry extends BaseRegistry { const manifestMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/manifests\/([^\/]+)$/); if (manifestMatch) { const [, name, reference] = manifestMatch; - return this.handleManifestRequest(context.method, name, reference, token, context.body, context.headers); + // Prefer rawBody for content-addressable operations to preserve exact bytes + const bodyData = context.rawBody || context.body; + return this.handleManifestRequest(context.method, name, reference, token, bodyData, context.headers); } // Blob operations: /v2/{name}/blobs/{digest} @@ -76,7 +78,9 @@ export class OciRegistry extends BaseRegistry { const uploadInitMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/blobs\/uploads\/?$/); if (uploadInitMatch && context.method === 'POST') { const [, name] = uploadInitMatch; - return this.handleUploadInit(name, token, context.query, context.body); + // Prefer rawBody for content-addressable operations to preserve exact bytes + const bodyData = context.rawBody || context.body; + return this.handleUploadInit(name, token, context.query, bodyData); } // Blob upload operations: /v2/{name}/blobs/uploads/{uuid} @@ -261,11 +265,14 @@ export class OciRegistry extends BaseRegistry { return this.createUnauthorizedResponse(session.repository, 'push'); } + // Prefer rawBody for content-addressable operations to preserve exact bytes + const bodyData = context.rawBody || context.body; + switch (method) { case 'PATCH': - return this.uploadChunk(uploadId, context.body, context.headers['content-range']); + return this.uploadChunk(uploadId, bodyData, context.headers['content-range']); case 'PUT': - return this.completeUpload(uploadId, context.query['digest'], context.body); + return this.completeUpload(uploadId, context.query['digest'], bodyData); case 'GET': return this.getUploadStatus(uploadId); default: