4 Commits

Author SHA1 Message Date
67188a4e9f v2.1.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-25 17:15:47 +00:00
a2f7f43027 fix(oci): Prefer raw request body for content-addressable OCI operations and expose rawBody on request context 2025-11-25 17:15:47 +00:00
37a89239d9 v2.1.1
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-25 16:59:37 +00:00
93fee289e7 fix(oci): Preserve raw manifest bytes for digest calculation and handle string/JSON manifest bodies in OCI registry 2025-11-25 16:59:37 +00:00
5 changed files with 45 additions and 7 deletions

View File

@@ -1,5 +1,19 @@
# 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
- Preserve the exact bytes of the manifest payload when computing the sha256 digest to comply with the OCI spec and avoid mismatches caused by re-serialization.
- Accept string request bodies (converted using UTF-8) and treat already-parsed JSON objects by re-serializing as a fallback.
- Keep existing content-type fallback logic while ensuring accurate digest calculation prior to storing manifests.
## 2025-11-25 - 2.1.0 - feat(oci)
Support configurable OCI token realm/service and centralize unauthorized responses

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartregistry",
"version": "2.1.0",
"version": "2.1.2",
"private": false,
"description": "A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries",
"main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartregistry',
version: '2.1.0',
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'
}

View File

@@ -158,6 +158,12 @@ export interface IRequestContext {
headers: Record<string, string>;
query: Record<string, string>;
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;
}

View File

@@ -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:
@@ -379,7 +386,18 @@ export class OciRegistry extends BaseRegistry {
};
}
const manifestData = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
// Preserve raw bytes for accurate digest calculation
// Per OCI spec, digest must match the exact bytes sent by client
let manifestData: Buffer;
if (Buffer.isBuffer(body)) {
manifestData = body;
} else if (typeof body === 'string') {
// String body - convert directly without JSON transformation
manifestData = Buffer.from(body, 'utf-8');
} else {
// Body was already parsed as JSON object - re-serialize as fallback
manifestData = Buffer.from(JSON.stringify(body));
}
const contentType = headers?.['content-type'] || headers?.['Content-Type'] || 'application/vnd.oci.image.manifest.v1+json';
// Calculate manifest digest