multi registry support v3
This commit is contained in:
516
readme.md
516
readme.md
@@ -1,17 +1,39 @@
|
||||
# @push.rocks/smartregistry
|
||||
|
||||
A TypeScript library implementing the OCI Distribution Specification v1.1 for building container and artifact registries.
|
||||
A composable TypeScript library implementing both OCI Distribution Specification v1.1 and NPM Registry API for building unified container and package registries.
|
||||
|
||||
## Features
|
||||
|
||||
- **OCI Distribution Spec v1.1 Compliant**: Implements all required and optional endpoints
|
||||
- **Cloud-Agnostic Storage**: Uses @push.rocks/smartbucket for S3-compatible object storage
|
||||
- **Pluggable Authentication**: Async callbacks for login and authorization
|
||||
- **Bearer Token Auth**: JWT-based authentication following Docker Registry Token Authentication spec
|
||||
- **Programmatic API**: Use as a library in any Node.js/TypeScript application
|
||||
- **Full CRUD Operations**: Push, pull, list, and delete manifests and blobs
|
||||
- **Content Discovery**: Tag listing and referrers API for artifact relationships
|
||||
- **Chunked Uploads**: Support for large blob uploads with resumable sessions
|
||||
### Dual Protocol Support
|
||||
- **OCI Distribution Spec v1.1**: Full container registry with manifest/blob operations
|
||||
- **NPM Registry API**: Complete package registry with publish/install/search
|
||||
|
||||
### Unified Architecture
|
||||
- **Composable Design**: Core infrastructure with protocol plugins
|
||||
- **Shared Storage**: Cloud-agnostic S3-compatible backend (@push.rocks/smartbucket)
|
||||
- **Unified Authentication**: Scope-based permissions across both protocols
|
||||
- **Path-based Routing**: `/oci/*` for containers, `/npm/*` for packages
|
||||
|
||||
### Authentication & Authorization
|
||||
- NPM UUID tokens for package operations
|
||||
- OCI JWT tokens for container operations
|
||||
- Unified scope system: `npm:package:foo:write`, `oci:repository:bar:push`
|
||||
- Pluggable via async callbacks
|
||||
|
||||
### Comprehensive Feature Set
|
||||
|
||||
**OCI Features:**
|
||||
- ✅ Pull operations (manifests, blobs)
|
||||
- ✅ Push operations (chunked uploads)
|
||||
- ✅ Content discovery (tags, referrers API)
|
||||
- ✅ Content management (deletion)
|
||||
|
||||
**NPM Features:**
|
||||
- ✅ Package publish/unpublish
|
||||
- ✅ Package download (tarballs)
|
||||
- ✅ Metadata & search
|
||||
- ✅ Dist-tag management
|
||||
- ✅ Token management
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -21,31 +43,14 @@ npm install @push.rocks/smartregistry
|
||||
pnpm add @push.rocks/smartregistry
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { SmartRegistry, IRegistryConfig, TLoginCallback, TAuthCallback } from '@push.rocks/smartregistry';
|
||||
import { SmartRegistry, IRegistryConfig } from '@push.rocks/smartregistry';
|
||||
|
||||
// Implement login callback
|
||||
const loginCallback: TLoginCallback = async (credentials) => {
|
||||
// Validate credentials and return JWT token
|
||||
// This should create a proper JWT with required claims
|
||||
return generateJWT(credentials.username);
|
||||
};
|
||||
|
||||
// Implement authorization callback
|
||||
const authCallback: TAuthCallback = async (token, repository, action) => {
|
||||
// Validate token and check permissions
|
||||
const claims = verifyJWT(token);
|
||||
return hasPermission(claims, repository, action);
|
||||
};
|
||||
|
||||
// Configure registry
|
||||
const config: IRegistryConfig = {
|
||||
storage: {
|
||||
accessKey: 'your-s3-access-key',
|
||||
accessKey: 'your-s3-key',
|
||||
accessSecret: 'your-s3-secret',
|
||||
endpoint: 's3.amazonaws.com',
|
||||
port: 443,
|
||||
@@ -53,117 +58,406 @@ const config: IRegistryConfig = {
|
||||
region: 'us-east-1',
|
||||
bucketName: 'my-registry',
|
||||
},
|
||||
serviceName: 'my-registry',
|
||||
tokenRealm: 'https://auth.example.com/token',
|
||||
loginCallback,
|
||||
authCallback,
|
||||
auth: {
|
||||
jwtSecret: 'your-secret-key',
|
||||
tokenStore: 'memory',
|
||||
npmTokens: { enabled: true },
|
||||
ociTokens: {
|
||||
enabled: true,
|
||||
realm: 'https://auth.example.com/token',
|
||||
service: 'my-registry',
|
||||
},
|
||||
},
|
||||
oci: {
|
||||
enabled: true,
|
||||
basePath: '/oci',
|
||||
},
|
||||
npm: {
|
||||
enabled: true,
|
||||
basePath: '/npm',
|
||||
},
|
||||
};
|
||||
|
||||
// Create and initialize registry
|
||||
const registry = new SmartRegistry(config);
|
||||
await registry.init();
|
||||
|
||||
// Handle requests
|
||||
const response = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/npm/express',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
```
|
||||
|
||||
### Integration with HTTP Server
|
||||
## Architecture
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
### Directory Structure
|
||||
|
||||
const app = express();
|
||||
|
||||
// OCI Distribution API endpoints
|
||||
app.get('/v2/', (req, res) => {
|
||||
res.status(200).json({});
|
||||
});
|
||||
|
||||
app.get('/v2/:name(*)/manifests/:reference', async (req, res) => {
|
||||
const { name, reference } = req.params;
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
const result = await registry.getManifest(name, reference, token);
|
||||
|
||||
if ('errors' in result) {
|
||||
return res.status(404).json(result);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', result.contentType);
|
||||
res.setHeader('Docker-Content-Digest', result.digest);
|
||||
res.send(result.data);
|
||||
});
|
||||
|
||||
app.get('/v2/:name(*)/blobs/:digest', async (req, res) => {
|
||||
const { name, digest } = req.params;
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
const result = await registry.getBlob(name, digest, token);
|
||||
|
||||
if ('errors' in result) {
|
||||
return res.status(404).json(result);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.send(result.data);
|
||||
});
|
||||
|
||||
// ... implement other endpoints
|
||||
|
||||
app.listen(5000);
|
||||
```
|
||||
ts/
|
||||
├── core/ # Shared infrastructure
|
||||
│ ├── classes.baseregistry.ts
|
||||
│ ├── classes.registrystorage.ts
|
||||
│ ├── classes.authmanager.ts
|
||||
│ └── interfaces.core.ts
|
||||
├── oci/ # OCI implementation
|
||||
│ ├── classes.ociregistry.ts
|
||||
│ └── interfaces.oci.ts
|
||||
├── npm/ # NPM implementation
|
||||
│ ├── classes.npmregistry.ts
|
||||
│ └── interfaces.npm.ts
|
||||
└── classes.smartregistry.ts # Main orchestrator
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
↓
|
||||
SmartRegistry (orchestrator)
|
||||
↓
|
||||
Path-based routing
|
||||
├─→ /oci/* → OciRegistry
|
||||
└─→ /npm/* → NpmRegistry
|
||||
↓
|
||||
Shared Storage & Auth
|
||||
↓
|
||||
S3-compatible backend
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### OCI Registry (Container Images)
|
||||
|
||||
```typescript
|
||||
// Client requests without token
|
||||
const challenge = registry.getAuthChallenge('library/nginx', ['pull', 'push']);
|
||||
// Returns: Bearer realm="https://auth.example.com/token",service="my-registry",scope="repository:library/nginx:pull,push"
|
||||
// Pull an image
|
||||
const response = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/oci/v2/library/nginx/manifests/latest',
|
||||
headers: {
|
||||
'Authorization': 'Bearer <token>',
|
||||
},
|
||||
query: {},
|
||||
});
|
||||
|
||||
// Client authenticates
|
||||
const token = await registry.login({ username: 'user', password: 'pass' });
|
||||
// Push a blob
|
||||
const uploadInit = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/oci/v2/myapp/blobs/uploads/',
|
||||
headers: { 'Authorization': 'Bearer <token>' },
|
||||
query: {},
|
||||
});
|
||||
|
||||
// Client uses token for subsequent requests
|
||||
const manifest = await registry.getManifest('library/nginx', 'latest', token);
|
||||
const uploadId = uploadInit.headers['Docker-Upload-UUID'];
|
||||
|
||||
await registry.handleRequest({
|
||||
method: 'PUT',
|
||||
path: `/oci/v2/myapp/blobs/uploads/${uploadId}`,
|
||||
headers: { 'Authorization': 'Bearer <token>' },
|
||||
query: { digest: 'sha256:abc123...' },
|
||||
body: blobData,
|
||||
});
|
||||
```
|
||||
|
||||
### NPM Registry (Packages)
|
||||
|
||||
```typescript
|
||||
// Install a package (get metadata)
|
||||
const metadata = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/npm/express',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
// Download tarball
|
||||
const tarball = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/npm/express/-/express-4.18.0.tgz',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
// Publish a package
|
||||
const publishResponse = await registry.handleRequest({
|
||||
method: 'PUT',
|
||||
path: '/npm/my-package',
|
||||
headers: { 'Authorization': 'Bearer <npm-token>' },
|
||||
query: {},
|
||||
body: {
|
||||
name: 'my-package',
|
||||
versions: {
|
||||
'1.0.0': { /* version metadata */ },
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
_attachments: {
|
||||
'my-package-1.0.0.tgz': {
|
||||
content_type: 'application/octet-stream',
|
||||
data: '<base64-tarball>',
|
||||
length: 12345,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Search packages
|
||||
const searchResults = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/npm/-/v1/search',
|
||||
headers: {},
|
||||
query: { text: 'express', size: '20' },
|
||||
});
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
```typescript
|
||||
// NPM Login
|
||||
const authManager = registry.getAuthManager();
|
||||
|
||||
// Authenticate user
|
||||
const userId = await authManager.authenticate({
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
});
|
||||
|
||||
// Create NPM token
|
||||
const npmToken = await authManager.createNpmToken(userId, false);
|
||||
|
||||
// Create OCI token with scopes
|
||||
const ociToken = await authManager.createOciToken(
|
||||
userId,
|
||||
['oci:repository:myapp:push', 'oci:repository:myapp:pull'],
|
||||
3600
|
||||
);
|
||||
|
||||
// Validate any token
|
||||
const token = await authManager.validateToken(npmToken, 'npm');
|
||||
|
||||
// Check permissions
|
||||
const canWrite = await authManager.authorize(
|
||||
token,
|
||||
'npm:package:my-package',
|
||||
'write'
|
||||
);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
```typescript
|
||||
storage: {
|
||||
accessKey: string; // S3 access key
|
||||
accessSecret: string; // S3 secret key
|
||||
endpoint: string; // S3 endpoint
|
||||
port?: number; // Default: 443
|
||||
useSsl?: boolean; // Default: true
|
||||
region?: string; // Default: 'us-east-1'
|
||||
bucketName: string; // Bucket name
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Configuration
|
||||
|
||||
```typescript
|
||||
auth: {
|
||||
jwtSecret: string; // Secret for signing JWTs
|
||||
tokenStore: 'memory' | 'redis' | 'database';
|
||||
npmTokens: {
|
||||
enabled: boolean;
|
||||
defaultReadonly?: boolean;
|
||||
};
|
||||
ociTokens: {
|
||||
enabled: boolean;
|
||||
realm: string; // Auth server URL
|
||||
service: string; // Service name
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Protocol Configuration
|
||||
|
||||
```typescript
|
||||
oci?: {
|
||||
enabled: boolean;
|
||||
basePath: string; // Default: '/oci'
|
||||
features?: {
|
||||
referrers?: boolean;
|
||||
deletion?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
npm?: {
|
||||
enabled: boolean;
|
||||
basePath: string; // Default: '/npm'
|
||||
features?: {
|
||||
publish?: boolean;
|
||||
unpublish?: boolean;
|
||||
search?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Pull Operations (Required)
|
||||
### Core Classes
|
||||
|
||||
- `getManifest(repository, reference, token?)` - Download a manifest
|
||||
- `headManifest(repository, reference, token?)` - Check manifest existence
|
||||
- `getBlob(repository, digest, token?, range?)` - Download a blob
|
||||
- `headBlob(repository, digest, token?)` - Check blob existence
|
||||
#### SmartRegistry
|
||||
|
||||
### Push Operations
|
||||
Main orchestrator class.
|
||||
|
||||
- `initiateUpload(repository, token, mountDigest?, fromRepository?)` - Start blob upload
|
||||
- `uploadChunk(uploadId, data, contentRange, token)` - Upload blob chunk
|
||||
- `completeUpload(uploadId, digest, token, finalData?)` - Finalize blob upload
|
||||
- `putManifest(repository, reference, manifest, contentType, token)` - Upload manifest
|
||||
**Methods:**
|
||||
- `init()` - Initialize the registry
|
||||
- `handleRequest(context)` - Handle HTTP request
|
||||
- `getStorage()` - Get storage instance
|
||||
- `getAuthManager()` - Get auth manager
|
||||
- `getRegistry(protocol)` - Get protocol handler
|
||||
|
||||
### Content Discovery
|
||||
#### RegistryStorage
|
||||
|
||||
- `listTags(repository, token?, pagination?)` - List all tags
|
||||
- `getReferrers(repository, digest, token?, artifactType?)` - Get referencing artifacts
|
||||
Unified storage abstraction.
|
||||
|
||||
### Content Management
|
||||
**OCI Methods:**
|
||||
- `getOciBlob(digest)` - Get blob
|
||||
- `putOciBlob(digest, data)` - Store blob
|
||||
- `getOciManifest(repo, digest)` - Get manifest
|
||||
- `putOciManifest(repo, digest, data, type)` - Store manifest
|
||||
|
||||
- `deleteManifest(repository, digest, token)` - Delete manifest
|
||||
- `deleteBlob(repository, digest, token)` - Delete blob
|
||||
- `deleteTag(repository, tag, token)` - Delete tag
|
||||
**NPM Methods:**
|
||||
- `getNpmPackument(name)` - Get package metadata
|
||||
- `putNpmPackument(name, data)` - Store package metadata
|
||||
- `getNpmTarball(name, version)` - Get tarball
|
||||
- `putNpmTarball(name, version, data)` - Store tarball
|
||||
|
||||
### Authentication
|
||||
#### AuthManager
|
||||
|
||||
- `login(credentials)` - Get authentication token
|
||||
- `getAuthChallenge(repository, actions)` - Generate WWW-Authenticate header
|
||||
Unified authentication manager.
|
||||
|
||||
## OCI Specification Compliance
|
||||
**Methods:**
|
||||
- `authenticate(credentials)` - Validate user credentials
|
||||
- `createNpmToken(userId, readonly)` - Create NPM token
|
||||
- `createOciToken(userId, scopes, expiresIn)` - Create OCI JWT
|
||||
- `validateToken(token, protocol)` - Validate any token
|
||||
- `authorize(token, resource, action)` - Check permissions
|
||||
|
||||
This library implements:
|
||||
### Protocol Handlers
|
||||
|
||||
- **Pull Category** (required): All manifest and blob retrieval operations
|
||||
- **Push Category**: Complete blob upload workflow with chunked and monolithic modes
|
||||
- **Content Discovery**: Tag listing and referrers API
|
||||
- **Content Management**: Deletion operations for manifests, blobs, and tags
|
||||
#### OciRegistry
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /v2/` - Version check
|
||||
- `GET /v2/{name}/manifests/{ref}` - Get manifest
|
||||
- `PUT /v2/{name}/manifests/{ref}` - Push manifest
|
||||
- `GET /v2/{name}/blobs/{digest}` - Get blob
|
||||
- `POST /v2/{name}/blobs/uploads/` - Initiate upload
|
||||
- `GET /v2/{name}/tags/list` - List tags
|
||||
- `GET /v2/{name}/referrers/{digest}` - Get referrers
|
||||
|
||||
#### NpmRegistry
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /{package}` - Get package metadata
|
||||
- `PUT /{package}` - Publish package
|
||||
- `GET /{package}/-/{tarball}` - Download tarball
|
||||
- `GET /-/v1/search` - Search packages
|
||||
- `PUT /-/user/org.couchdb.user:{user}` - Login
|
||||
- `GET /-/npm/v1/tokens` - List tokens
|
||||
- `POST /-/npm/v1/tokens` - Create token
|
||||
- `PUT /-/package/{pkg}/dist-tags/{tag}` - Update tag
|
||||
|
||||
## Storage Structure
|
||||
|
||||
```
|
||||
bucket/
|
||||
├── oci/
|
||||
│ ├── blobs/
|
||||
│ │ └── sha256/{hash}
|
||||
│ ├── manifests/
|
||||
│ │ └── {repository}/{digest}
|
||||
│ └── tags/
|
||||
│ └── {repository}/tags.json
|
||||
└── npm/
|
||||
├── packages/
|
||||
│ ├── {name}/
|
||||
│ │ ├── index.json # Packument
|
||||
│ │ └── {name}-{ver}.tgz # Tarball
|
||||
│ └── @{scope}/{name}/
|
||||
│ ├── index.json
|
||||
│ └── {name}-{ver}.tgz
|
||||
└── users/
|
||||
└── {username}.json
|
||||
```
|
||||
|
||||
## Scope Format
|
||||
|
||||
Unified scope format across protocols:
|
||||
|
||||
```
|
||||
{protocol}:{type}:{name}:{action}
|
||||
|
||||
Examples:
|
||||
npm:package:express:read # Read express package
|
||||
npm:package:*:write # Write any package
|
||||
npm:*:* # Full NPM access
|
||||
oci:repository:nginx:pull # Pull nginx image
|
||||
oci:repository:*:push # Push any image
|
||||
oci:*:* # Full OCI access
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express Server
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { SmartRegistry } from '@push.rocks/smartregistry';
|
||||
|
||||
const app = express();
|
||||
const registry = new SmartRegistry(config);
|
||||
await registry.init();
|
||||
|
||||
app.all('*', async (req, res) => {
|
||||
const response = await registry.handleRequest({
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
headers: req.headers as Record<string, string>,
|
||||
query: req.query as Record<string, string>,
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
res.status(response.status);
|
||||
Object.entries(response.headers).forEach(([key, value]) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
|
||||
if (response.body) {
|
||||
if (Buffer.isBuffer(response.body)) {
|
||||
res.send(response.body);
|
||||
} else {
|
||||
res.json(response.body);
|
||||
}
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(5000);
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build
|
||||
pnpm run build
|
||||
|
||||
# Test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -171,4 +465,4 @@ MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
See the main repository for contribution guidelines.
|
||||
Contributions welcome! Please see the repository for guidelines.
|
||||
|
||||
Reference in New Issue
Block a user