4 Commits

Author SHA1 Message Date
29dea2e0e8 v1.1.1
Some checks failed
Default (tags) / security (push) Successful in 26s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-20 19:48:32 +00:00
52dc1c0549 fix(oci): Improve OCI manifest permission response and tag handling: include WWW-Authenticate header on unauthorized manifest GETs, accept optional headers in manifest lookup, and persist tags as a unified tags.json mapping when pushing manifests. 2025-11-20 19:48:32 +00:00
3d5b87ec05 v1.1.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-20 19:46:34 +00:00
1c63b74bb8 feat(oci): Support monolithic OCI blob uploads; add registry cleanup/destroy hooks; update tests and docs 2025-11-20 19:46:34 +00:00
10 changed files with 201 additions and 56 deletions

View File

@@ -1,5 +1,24 @@
# Changelog # Changelog
## 2025-11-20 - 1.1.1 - fix(oci)
Improve OCI manifest permission response and tag handling: include WWW-Authenticate header on unauthorized manifest GETs, accept optional headers in manifest lookup, and persist tags as a unified tags.json mapping when pushing manifests.
- getManifest now accepts an optional headers parameter for better request context handling.
- Unauthorized GET manifest responses now include a WWW-Authenticate header with realm/service/scope to comply with OCI auth expectations.
- PUT manifest logic no longer writes individual tag objects; it updates a consolidated oci/tags/{repository}/tags.json mapping using getTagsData and putObject.
- Simplified tag update flow when pushing a manifest: tags[reference] = digest and persist tags.json.
## 2025-11-20 - 1.1.0 - feat(oci)
Support monolithic OCI blob uploads; add registry cleanup/destroy hooks; update tests and docs
- OCI: Add monolithic upload handling in handleUploadInit — accept digest + body, verify digest, store blob and return 201 with Docker-Content-Digest and Location
- OCI: Include Docker-Distribution-API-Version header in /v2/ version check response
- Lifecycle: Persist upload session cleanup timer and provide destroy() to clear timers in OciRegistry
- Orchestrator: Add destroy() to SmartRegistry to propagate cleanup to protocol handlers
- Tests: Ensure test suites call registry.destroy() in postTask cleanup to prevent leaked timers/resources
- Package metadata: bump @git.zone/tstest dev dependency and add packageManager field
- Docs: Readme formatting and legal/trademark/company information updated
## 2025-11-20 - 1.0.2 - fix(scripts) ## 2025-11-20 - 1.0.2 - fix(scripts)
Increase tstest timeout from 30s to 240s in package.json test script Increase tstest timeout from 30s to 240s in package.json test script

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartregistry", "name": "@push.rocks/smartregistry",
"version": "1.0.2", "version": "1.1.1",
"private": false, "private": false,
"description": "a registry for npm modules and oci images", "description": "a registry for npm modules and oci images",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -17,7 +17,7 @@
"@git.zone/tsbuild": "^3.1.0", "@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsbundle": "^2.0.5", "@git.zone/tsbundle": "^2.0.5",
"@git.zone/tsrun": "^2.0.0", "@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^3.0.1", "@git.zone/tstest": "^3.1.0",
"@types/node": "^24.10.1" "@types/node": "^24.10.1"
}, },
"repository": { "repository": {
@@ -48,5 +48,6 @@
"@push.rocks/smartbucket": "^4.3.0", "@push.rocks/smartbucket": "^4.3.0",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpath": "^6.0.0" "@push.rocks/smartpath": "^6.0.0"
} },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
} }

10
pnpm-lock.yaml generated
View File

@@ -31,8 +31,8 @@ importers:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
'@git.zone/tstest': '@git.zone/tstest':
specifier: ^3.0.1 specifier: ^3.1.0
version: 3.0.1(socks@2.8.7)(typescript@5.9.3) version: 3.1.0(socks@2.8.7)(typescript@5.9.3)
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.1 version: 24.10.1
@@ -547,8 +547,8 @@ packages:
resolution: {integrity: sha512-yA6zCjL+kn7xfZe6sL/m4K+zYqgkznG/pF6++i/E17iwzpG6dHmW+VZmYldHe86sW4DcLMvqM6CxM+KlgaEpKw==} resolution: {integrity: sha512-yA6zCjL+kn7xfZe6sL/m4K+zYqgkznG/pF6++i/E17iwzpG6dHmW+VZmYldHe86sW4DcLMvqM6CxM+KlgaEpKw==}
hasBin: true hasBin: true
'@git.zone/tstest@3.0.1': '@git.zone/tstest@3.1.0':
resolution: {integrity: sha512-YjjLLWGj8fE8yYAfMrLSDgdZ+JJOS7I6iRshIyr6THH5dnTONOA3R076zBaryRw58qgPn+s/0jno7wlhYhv0iw==} resolution: {integrity: sha512-nshpkFvyIUUDvYcA/IOyqWBVEoxGm674ytIkA+XJ6DPO/hz2l3mMIjplc43d2U2eHkAZk8/ycr9GIo0xNhiLFg==}
hasBin: true hasBin: true
'@happy-dom/global-registrator@15.11.7': '@happy-dom/global-registrator@15.11.7':
@@ -4902,7 +4902,7 @@ snapshots:
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
tsx: 4.20.6 tsx: 4.20.6
'@git.zone/tstest@3.0.1(socks@2.8.7)(typescript@5.9.3)': '@git.zone/tstest@3.1.0(socks@2.8.7)(typescript@5.9.3)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.80 '@api.global/typedserver': 3.0.80
'@git.zone/tsbundle': 2.5.2 '@git.zone/tsbundle': 2.5.2

View File

@@ -1,26 +1,26 @@
# @push.rocks/smartregistry # @push.rocks/smartregistry
A composable TypeScript library implementing both OCI Distribution Specification v1.1 and NPM Registry API for building unified container and package registries. > 🚀 A composable TypeScript library implementing both **OCI Distribution Specification v1.1** and **NPM Registry API** for building unified container and package registries.
## Features ## Features
### Dual Protocol Support ### 🔄 Dual Protocol Support
- **OCI Distribution Spec v1.1**: Full container registry with manifest/blob operations - **OCI Distribution Spec v1.1**: Full container registry with manifest/blob operations
- **NPM Registry API**: Complete package registry with publish/install/search - **NPM Registry API**: Complete package registry with publish/install/search
### Unified Architecture ### 🏗️ Unified Architecture
- **Composable Design**: Core infrastructure with protocol plugins - **Composable Design**: Core infrastructure with protocol plugins
- **Shared Storage**: Cloud-agnostic S3-compatible backend (@push.rocks/smartbucket) - **Shared Storage**: Cloud-agnostic S3-compatible backend ([@push.rocks/smartbucket](https://www.npmjs.com/package/@push.rocks/smartbucket))
- **Unified Authentication**: Scope-based permissions across both protocols - **Unified Authentication**: Scope-based permissions across both protocols
- **Path-based Routing**: `/oci/*` for containers, `/npm/*` for packages - **Path-based Routing**: `/oci/*` for containers, `/npm/*` for packages
### Authentication & Authorization ### 🔐 Authentication & Authorization
- NPM UUID tokens for package operations - NPM UUID tokens for package operations
- OCI JWT tokens for container operations - OCI JWT tokens for container operations
- Unified scope system: `npm:package:foo:write`, `oci:repository:bar:push` - Unified scope system: `npm:package:foo:write`, `oci:repository:bar:push`
- Pluggable via async callbacks - Pluggable via async callbacks
### Comprehensive Feature Set ### 📦 Comprehensive Feature Set
**OCI Features:** **OCI Features:**
- ✅ Pull operations (manifests, blobs) - ✅ Pull operations (manifests, blobs)
@@ -35,15 +35,17 @@ A composable TypeScript library implementing both OCI Distribution Specification
- ✅ Dist-tag management - ✅ Dist-tag management
- ✅ Token management - ✅ Token management
## Installation ## 📥 Installation
```bash ```bash
# Using npm
npm install @push.rocks/smartregistry npm install @push.rocks/smartregistry
# or
# Using pnpm (recommended)
pnpm add @push.rocks/smartregistry pnpm add @push.rocks/smartregistry
``` ```
## Quick Start ## 🚀 Quick Start
```typescript ```typescript
import { SmartRegistry, IRegistryConfig } from '@push.rocks/smartregistry'; import { SmartRegistry, IRegistryConfig } from '@push.rocks/smartregistry';
@@ -90,7 +92,7 @@ const response = await registry.handleRequest({
}); });
``` ```
## Architecture ## 🏛️ Architecture
### Directory Structure ### Directory Structure
@@ -126,9 +128,9 @@ Path-based routing
S3-compatible backend S3-compatible backend
``` ```
## Usage Examples ## 💡 Usage Examples
### OCI Registry (Container Images) ### 🐳 OCI Registry (Container Images)
```typescript ```typescript
// Pull an image // Pull an image
@@ -160,7 +162,7 @@ await registry.handleRequest({
}); });
``` ```
### NPM Registry (Packages) ### 📦 NPM Registry (Packages)
```typescript ```typescript
// Install a package (get metadata) // Install a package (get metadata)
@@ -210,10 +212,10 @@ const searchResults = await registry.handleRequest({
}); });
``` ```
### Authentication ### 🔐 Authentication
```typescript ```typescript
// NPM Login // Get auth manager instance
const authManager = registry.getAuthManager(); const authManager = registry.getAuthManager();
// Authenticate user // Authenticate user
@@ -243,7 +245,7 @@ const canWrite = await authManager.authorize(
); );
``` ```
## Configuration ## ⚙️ Configuration
### Storage Configuration ### Storage Configuration
@@ -300,13 +302,13 @@ npm?: {
} }
``` ```
## API Reference ## 📚 API Reference
### Core Classes ### Core Classes
#### SmartRegistry #### SmartRegistry
Main orchestrator class. Main orchestrator class that routes requests to appropriate protocol handlers.
**Methods:** **Methods:**
- `init()` - Initialize the registry - `init()` - Initialize the registry
@@ -317,7 +319,7 @@ Main orchestrator class.
#### RegistryStorage #### RegistryStorage
Unified storage abstraction. Unified storage abstraction for both OCI and NPM content.
**OCI Methods:** **OCI Methods:**
- `getOciBlob(digest)` - Get blob - `getOciBlob(digest)` - Get blob
@@ -333,7 +335,7 @@ Unified storage abstraction.
#### AuthManager #### AuthManager
Unified authentication manager. Unified authentication manager supporting both NPM and OCI authentication schemes.
**Methods:** **Methods:**
- `authenticate(credentials)` - Validate user credentials - `authenticate(credentials)` - Validate user credentials
@@ -346,17 +348,22 @@ Unified authentication manager.
#### OciRegistry #### OciRegistry
OCI Distribution Specification v1.1 compliant registry.
**Endpoints:** **Endpoints:**
- `GET /v2/` - Version check - `GET /v2/` - Version check
- `GET /v2/{name}/manifests/{ref}` - Get manifest - `GET /v2/{name}/manifests/{ref}` - Get manifest
- `PUT /v2/{name}/manifests/{ref}` - Push manifest - `PUT /v2/{name}/manifests/{ref}` - Push manifest
- `GET /v2/{name}/blobs/{digest}` - Get blob - `GET /v2/{name}/blobs/{digest}` - Get blob
- `POST /v2/{name}/blobs/uploads/` - Initiate upload - `POST /v2/{name}/blobs/uploads/` - Initiate upload
- `PUT /v2/{name}/blobs/uploads/{uuid}` - Complete upload
- `GET /v2/{name}/tags/list` - List tags - `GET /v2/{name}/tags/list` - List tags
- `GET /v2/{name}/referrers/{digest}` - Get referrers - `GET /v2/{name}/referrers/{digest}` - Get referrers
#### NpmRegistry #### NpmRegistry
NPM registry API compliant implementation.
**Endpoints:** **Endpoints:**
- `GET /{package}` - Get package metadata - `GET /{package}` - Get package metadata
- `PUT /{package}` - Publish package - `PUT /{package}` - Publish package
@@ -367,7 +374,7 @@ Unified authentication manager.
- `POST /-/npm/v1/tokens` - Create token - `POST /-/npm/v1/tokens` - Create token
- `PUT /-/package/{pkg}/dist-tags/{tag}` - Update tag - `PUT /-/package/{pkg}/dist-tags/{tag}` - Update tag
## Storage Structure ## 🗄️ Storage Structure
``` ```
bucket/ bucket/
@@ -390,7 +397,7 @@ bucket/
└── {username}.json └── {username}.json
``` ```
## Scope Format ## 🎯 Scope Format
Unified scope format across protocols: Unified scope format across protocols:
@@ -400,13 +407,13 @@ Unified scope format across protocols:
Examples: Examples:
npm:package:express:read # Read express package npm:package:express:read # Read express package
npm:package:*:write # Write any package npm:package:*:write # Write any package
npm:*:* # Full NPM access npm:*:*:* # Full NPM access
oci:repository:nginx:pull # Pull nginx image oci:repository:nginx:pull # Pull nginx image
oci:repository:*:push # Push any image oci:repository:*:push # Push any image
oci:*:* # Full OCI access oci:*:*:* # Full OCI access
``` ```
## Integration Examples ## 🔌 Integration Examples
### Express Server ### Express Server
@@ -446,7 +453,7 @@ app.all('*', async (req, res) => {
app.listen(5000); app.listen(5000);
``` ```
## Development ## 🛠️ Development
```bash ```bash
# Install dependencies # Install dependencies
@@ -459,10 +466,21 @@ pnpm run build
pnpm test pnpm test
``` ```
## License ## License and Legal Information
MIT This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
## Contributing **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Contributions welcome! Please see the repository for guidelines. ### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -358,4 +358,10 @@ tap.test('NPM: should reject readonly token for write operations', async () => {
expect(response.status).toEqual(401); expect(response.status).toEqual(401);
}); });
tap.postTask('cleanup registry', async () => {
if (registry) {
registry.destroy();
}
});
export default tap.start(); export default tap.start();

View File

@@ -294,4 +294,10 @@ tap.test('OCI: should handle unauthorized requests', async () => {
expect(response.headers['WWW-Authenticate']).toInclude('Bearer'); expect(response.headers['WWW-Authenticate']).toInclude('Bearer');
}); });
tap.postTask('cleanup registry', async () => {
if (registry) {
registry.destroy();
}
});
export default tap.start(); export default tap.start();

View File

@@ -194,4 +194,10 @@ tap.test('Integration: should access storage backend', async () => {
expect(existsAfterDelete).toEqual(false); expect(existsAfterDelete).toEqual(false);
}); });
tap.postTask('cleanup registry', async () => {
if (registry) {
registry.destroy();
}
});
export default tap.start(); export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartregistry', name: '@push.rocks/smartregistry',
version: '1.0.2', version: '1.1.1',
description: 'a registry for npm modules and oci images' description: 'a registry for npm modules and oci images'
} }

View File

@@ -115,4 +115,15 @@ export class SmartRegistry {
public isInitialized(): boolean { public isInitialized(): boolean {
return this.initialized; return this.initialized;
} }
/**
* Clean up resources (timers, connections, etc.)
*/
public destroy(): void {
for (const registry of this.registries.values()) {
if (typeof (registry as any).destroy === 'function') {
(registry as any).destroy();
}
}
}
} }

View File

@@ -19,6 +19,7 @@ export class OciRegistry extends BaseRegistry {
private authManager: AuthManager; private authManager: AuthManager;
private uploadSessions: Map<string, IUploadSession> = new Map(); private uploadSessions: Map<string, IUploadSession> = new Map();
private basePath: string = '/oci'; private basePath: string = '/oci';
private cleanupInterval?: NodeJS.Timeout;
constructor(storage: RegistryStorage, authManager: AuthManager, basePath: string = '/oci') { constructor(storage: RegistryStorage, authManager: AuthManager, basePath: string = '/oci') {
super(); super();
@@ -54,7 +55,7 @@ export class OciRegistry extends BaseRegistry {
const manifestMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/manifests\/([^\/]+)$/); const manifestMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/manifests\/([^\/]+)$/);
if (manifestMatch) { if (manifestMatch) {
const [, name, reference] = manifestMatch; const [, name, reference] = manifestMatch;
return this.handleManifestRequest(context.method, name, reference, token); return this.handleManifestRequest(context.method, name, reference, token, context.body, context.headers);
} }
// Blob operations: /v2/{name}/blobs/{digest} // Blob operations: /v2/{name}/blobs/{digest}
@@ -68,7 +69,7 @@ export class OciRegistry extends BaseRegistry {
const uploadInitMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/blobs\/uploads\/?$/); const uploadInitMatch = path.match(/^\/v2\/([^\/]+(?:\/[^\/]+)*)\/blobs\/uploads\/?$/);
if (uploadInitMatch && context.method === 'POST') { if (uploadInitMatch && context.method === 'POST') {
const [, name] = uploadInitMatch; const [, name] = uploadInitMatch;
return this.handleUploadInit(name, token, context.query); return this.handleUploadInit(name, token, context.query, context.body);
} }
// Blob upload operations: /v2/{name}/blobs/uploads/{uuid} // Blob upload operations: /v2/{name}/blobs/uploads/{uuid}
@@ -115,7 +116,10 @@ export class OciRegistry extends BaseRegistry {
private handleVersionCheck(): IResponse { private handleVersionCheck(): IResponse {
return { return {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'Docker-Distribution-API-Version': 'registry/2.0',
},
body: {}, body: {},
}; };
} }
@@ -124,15 +128,17 @@ export class OciRegistry extends BaseRegistry {
method: string, method: string,
repository: string, repository: string,
reference: string, reference: string,
token: IAuthToken | null token: IAuthToken | null,
body?: Buffer | any,
headers?: Record<string, string>
): Promise<IResponse> { ): Promise<IResponse> {
switch (method) { switch (method) {
case 'GET': case 'GET':
return this.getManifest(repository, reference, token); return this.getManifest(repository, reference, token, headers);
case 'HEAD': case 'HEAD':
return this.headManifest(repository, reference, token); return this.headManifest(repository, reference, token);
case 'PUT': case 'PUT':
return this.putManifest(repository, reference, token); return this.putManifest(repository, reference, token, body, headers);
case 'DELETE': case 'DELETE':
return this.deleteManifest(repository, reference, token); return this.deleteManifest(repository, reference, token);
default: default:
@@ -170,7 +176,8 @@ export class OciRegistry extends BaseRegistry {
private async handleUploadInit( private async handleUploadInit(
repository: string, repository: string,
token: IAuthToken | null, token: IAuthToken | null,
query: Record<string, string> query: Record<string, string>,
body?: Buffer | any
): Promise<IResponse> { ): Promise<IResponse> {
if (!await this.checkPermission(token, repository, 'push')) { if (!await this.checkPermission(token, repository, 'push')) {
return { return {
@@ -180,6 +187,36 @@ export class OciRegistry extends BaseRegistry {
}; };
} }
// Check for monolithic upload (digest + body provided)
const digest = query.digest;
if (digest && body) {
// Monolithic upload: complete upload in single POST
const blobData = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
// Verify digest
const calculatedDigest = await this.calculateDigest(blobData);
if (calculatedDigest !== digest) {
return {
status: 400,
headers: {},
body: this.createError('DIGEST_INVALID', 'Provided digest does not match uploaded content'),
};
}
// Store the blob
await this.storage.putOciBlob(digest, blobData);
return {
status: 201,
headers: {
'Location': `${this.basePath}/v2/${repository}/blobs/${digest}`,
'Docker-Content-Digest': digest,
},
body: null,
};
}
// Standard chunked upload: create session
const uploadId = this.generateUploadId(); const uploadId = this.generateUploadId();
const session: IUploadSession = { const session: IUploadSession = {
uploadId, uploadId,
@@ -247,12 +284,15 @@ export class OciRegistry extends BaseRegistry {
private async getManifest( private async getManifest(
repository: string, repository: string,
reference: string, reference: string,
token: IAuthToken | null token: IAuthToken | null,
headers?: Record<string, string>
): Promise<IResponse> { ): Promise<IResponse> {
if (!await this.checkPermission(token, repository, 'pull')) { if (!await this.checkPermission(token, repository, 'pull')) {
return { return {
status: 401, status: 401,
headers: {}, headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:pull"`,
},
body: this.createError('DENIED', 'Insufficient permissions'), body: this.createError('DENIED', 'Insufficient permissions'),
}; };
} }
@@ -334,21 +374,52 @@ export class OciRegistry extends BaseRegistry {
private async putManifest( private async putManifest(
repository: string, repository: string,
reference: string, reference: string,
token: IAuthToken | null token: IAuthToken | null,
body?: Buffer | any,
headers?: Record<string, string>
): Promise<IResponse> { ): Promise<IResponse> {
if (!await this.checkPermission(token, repository, 'push')) { if (!await this.checkPermission(token, repository, 'push')) {
return { return {
status: 401, status: 401,
headers: {}, headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:push"`,
},
body: this.createError('DENIED', 'Insufficient permissions'), body: this.createError('DENIED', 'Insufficient permissions'),
}; };
} }
// Implementation continued in next file due to length... if (!body) {
return {
status: 400,
headers: {},
body: this.createError('MANIFEST_INVALID', 'Manifest body is required'),
};
}
const manifestData = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
const contentType = headers?.['content-type'] || headers?.['Content-Type'] || 'application/vnd.oci.image.manifest.v1+json';
// Calculate manifest digest
const digest = await this.calculateDigest(manifestData);
// Store manifest by digest
await this.storage.putOciManifest(repository, digest, manifestData, contentType);
// If reference is a tag (not a digest), update tags mapping
if (!reference.startsWith('sha256:')) {
const tags = await this.getTagsData(repository);
tags[reference] = digest;
const tagsPath = `oci/tags/${repository}/tags.json`;
await this.storage.putObject(tagsPath, Buffer.from(JSON.stringify(tags), 'utf-8'));
}
return { return {
status: 501, status: 201,
headers: {}, headers: {
body: this.createError('UNSUPPORTED', 'Not yet implemented'), 'Location': `${this.basePath}/v2/${repository}/manifests/${digest}`,
'Docker-Content-Digest': digest,
},
body: null,
}; };
} }
@@ -642,7 +713,7 @@ export class OciRegistry extends BaseRegistry {
} }
private startUploadSessionCleanup(): void { private startUploadSessionCleanup(): void {
setInterval(() => { this.cleanupInterval = setInterval(() => {
const now = new Date(); const now = new Date();
const maxAge = 60 * 60 * 1000; // 1 hour const maxAge = 60 * 60 * 1000; // 1 hour
@@ -653,4 +724,11 @@ export class OciRegistry extends BaseRegistry {
} }
}, 10 * 60 * 1000); }, 10 * 60 * 1000);
} }
public destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
}
} }