multi registry support v2
This commit is contained in:
388
ts/core/classes.authmanager.ts
Normal file
388
ts/core/classes.authmanager.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
|
||||
|
||||
/**
|
||||
* Unified authentication manager for all registry protocols
|
||||
* Handles both NPM UUID tokens and OCI JWT tokens
|
||||
*/
|
||||
export class AuthManager {
|
||||
private tokenStore: Map<string, IAuthToken> = new Map();
|
||||
private userCredentials: Map<string, string> = new Map(); // username -> password hash (mock)
|
||||
|
||||
constructor(private config: IAuthConfig) {}
|
||||
|
||||
/**
|
||||
* Initialize the auth manager
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
// Initialize token store (in-memory for now)
|
||||
// In production, this could be Redis or a database
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// NPM AUTHENTICATION
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Create an NPM token
|
||||
* @param userId - User ID
|
||||
* @param readonly - Whether the token is readonly
|
||||
* @returns NPM UUID token
|
||||
*/
|
||||
public async createNpmToken(userId: string, readonly: boolean = false): Promise<string> {
|
||||
if (!this.config.npmTokens.enabled) {
|
||||
throw new Error('NPM tokens are not enabled');
|
||||
}
|
||||
|
||||
const token = this.generateUuid();
|
||||
const authToken: IAuthToken = {
|
||||
type: 'npm',
|
||||
userId,
|
||||
scopes: readonly ? ['npm:*:read'] : ['npm:*:*'],
|
||||
readonly,
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
this.tokenStore.set(token, authToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an NPM token
|
||||
* @param token - NPM UUID token
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateNpmToken(token: string): Promise<IAuthToken | null> {
|
||||
if (!this.isValidUuid(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authToken = this.tokenStore.get(token);
|
||||
if (!authToken || authToken.type !== 'npm') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check expiration if set
|
||||
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
||||
this.tokenStore.delete(token);
|
||||
return null;
|
||||
}
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an NPM token
|
||||
* @param token - NPM UUID token
|
||||
*/
|
||||
public async revokeNpmToken(token: string): Promise<void> {
|
||||
this.tokenStore.delete(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all tokens for a user
|
||||
* @param userId - User ID
|
||||
* @returns List of token info (without actual token values)
|
||||
*/
|
||||
public async listUserTokens(userId: string): Promise<Array<{
|
||||
key: string;
|
||||
readonly: boolean;
|
||||
created: string;
|
||||
}>> {
|
||||
const tokens: Array<{key: string; readonly: boolean; created: string}> = [];
|
||||
|
||||
for (const [token, authToken] of this.tokenStore.entries()) {
|
||||
if (authToken.userId === userId) {
|
||||
tokens.push({
|
||||
key: this.hashToken(token),
|
||||
readonly: authToken.readonly || false,
|
||||
created: authToken.metadata?.created || 'unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OCI AUTHENTICATION (JWT)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Create an OCI JWT token
|
||||
* @param userId - User ID
|
||||
* @param scopes - Permission scopes
|
||||
* @param expiresIn - Expiration time in seconds
|
||||
* @returns JWT token string
|
||||
*/
|
||||
public async createOciToken(
|
||||
userId: string,
|
||||
scopes: string[],
|
||||
expiresIn: number = 3600
|
||||
): Promise<string> {
|
||||
if (!this.config.ociTokens.enabled) {
|
||||
throw new Error('OCI tokens are not enabled');
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = {
|
||||
iss: this.config.ociTokens.realm,
|
||||
sub: userId,
|
||||
aud: this.config.ociTokens.service,
|
||||
exp: now + expiresIn,
|
||||
nbf: now,
|
||||
iat: now,
|
||||
access: this.scopesToOciAccess(scopes),
|
||||
};
|
||||
|
||||
// In production, use proper JWT library with signing
|
||||
// For now, return JSON string (mock JWT)
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an OCI JWT token
|
||||
* @param jwt - JWT token string
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateOciToken(jwt: string): Promise<IAuthToken | null> {
|
||||
try {
|
||||
// In production, verify JWT signature
|
||||
const payload = JSON.parse(jwt);
|
||||
|
||||
// Check expiration
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp && payload.exp < now) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to unified token format
|
||||
const scopes = this.ociAccessToScopes(payload.access || []);
|
||||
|
||||
return {
|
||||
type: 'oci',
|
||||
userId: payload.sub,
|
||||
scopes,
|
||||
expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,
|
||||
metadata: {
|
||||
iss: payload.iss,
|
||||
aud: payload.aud,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// UNIFIED AUTHENTICATION
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Authenticate user credentials
|
||||
* @param credentials - Username and password
|
||||
* @returns User ID or null
|
||||
*/
|
||||
public async authenticate(credentials: ICredentials): Promise<string | null> {
|
||||
// Mock authentication - in production, verify against database
|
||||
const storedPassword = this.userCredentials.get(credentials.username);
|
||||
|
||||
if (!storedPassword) {
|
||||
// Auto-register for testing (remove in production)
|
||||
this.userCredentials.set(credentials.username, credentials.password);
|
||||
return credentials.username;
|
||||
}
|
||||
|
||||
if (storedPassword === credentials.password) {
|
||||
return credentials.username;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate any token (NPM or OCI)
|
||||
* @param tokenString - Token string (UUID or JWT)
|
||||
* @param protocol - Expected protocol type
|
||||
* @returns Auth token object or null
|
||||
*/
|
||||
public async validateToken(
|
||||
tokenString: string,
|
||||
protocol?: TRegistryProtocol
|
||||
): Promise<IAuthToken | null> {
|
||||
// Try NPM token first (UUID format)
|
||||
if (this.isValidUuid(tokenString)) {
|
||||
const npmToken = await this.validateNpmToken(tokenString);
|
||||
if (npmToken && (!protocol || protocol === 'npm')) {
|
||||
return npmToken;
|
||||
}
|
||||
}
|
||||
|
||||
// Try OCI JWT
|
||||
const ociToken = await this.validateOciToken(tokenString);
|
||||
if (ociToken && (!protocol || protocol === 'oci')) {
|
||||
return ociToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has permission for an action
|
||||
* @param token - Auth token
|
||||
* @param resource - Resource being accessed (e.g., "package:foo" or "repository:bar")
|
||||
* @param action - Action being performed (read, write, push, pull, delete)
|
||||
* @returns true if authorized
|
||||
*/
|
||||
public async authorize(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check readonly flag
|
||||
if (token.readonly && ['write', 'push', 'delete'].includes(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check scopes
|
||||
for (const scope of token.scopes) {
|
||||
if (this.matchesScope(scope, resource, action)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// HELPER METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if a scope matches a resource and action
|
||||
* Scope format: "{protocol}:{type}:{name}:{action}"
|
||||
* Examples:
|
||||
* - "npm:*:*" - All NPM access
|
||||
* - "npm:package:foo:*" - All actions on package foo
|
||||
* - "npm:package:foo:read" - Read-only on package foo
|
||||
* - "oci:repository:*:pull" - Pull from any OCI repo
|
||||
*/
|
||||
private matchesScope(scope: string, resource: string, action: string): boolean {
|
||||
const scopeParts = scope.split(':');
|
||||
const resourceParts = resource.split(':');
|
||||
|
||||
// Scope must have at least protocol:type:name:action
|
||||
if (scopeParts.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [scopeProtocol, scopeType, scopeName, scopeAction] = scopeParts;
|
||||
const [resourceProtocol, resourceType, resourceName] = resourceParts;
|
||||
|
||||
// Check protocol
|
||||
if (scopeProtocol !== '*' && scopeProtocol !== resourceProtocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check type
|
||||
if (scopeType !== '*' && scopeType !== resourceType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check name
|
||||
if (scopeName !== '*' && scopeName !== resourceName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check action
|
||||
if (scopeAction !== '*' && scopeAction !== action) {
|
||||
// Map action aliases
|
||||
const actionAliases: Record<string, string[]> = {
|
||||
read: ['pull', 'get'],
|
||||
write: ['push', 'put', 'post'],
|
||||
};
|
||||
|
||||
const aliases = actionAliases[scopeAction] || [];
|
||||
if (!aliases.includes(action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert unified scopes to OCI access array
|
||||
*/
|
||||
private scopesToOciAccess(scopes: string[]): Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
actions: string[];
|
||||
}> {
|
||||
const access: Array<{type: string; name: string; actions: string[]}> = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
const parts = scope.split(':');
|
||||
if (parts.length >= 4 && parts[0] === 'oci') {
|
||||
access.push({
|
||||
type: parts[1],
|
||||
name: parts[2],
|
||||
actions: [parts[3]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OCI access array to unified scopes
|
||||
*/
|
||||
private ociAccessToScopes(access: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
actions: string[];
|
||||
}>): string[] {
|
||||
const scopes: string[] = [];
|
||||
|
||||
for (const item of access) {
|
||||
for (const action of item.actions) {
|
||||
scopes.push(`oci:${item.type}:${item.name}:${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUID for NPM tokens
|
||||
*/
|
||||
private generateUuid(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is a valid UUID
|
||||
*/
|
||||
private isValidUuid(str: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a token for identification (SHA-512 mock)
|
||||
*/
|
||||
private hashToken(token: string): string {
|
||||
// In production, use actual SHA-512
|
||||
return `sha512-${token.substring(0, 16)}...`;
|
||||
}
|
||||
}
|
||||
36
ts/core/classes.baseregistry.ts
Normal file
36
ts/core/classes.baseregistry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IRequestContext, IResponse, IAuthToken } from './interfaces.core.js';
|
||||
|
||||
/**
|
||||
* Abstract base class for all registry protocol implementations
|
||||
*/
|
||||
export abstract class BaseRegistry {
|
||||
/**
|
||||
* Initialize the registry
|
||||
*/
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Handle an incoming HTTP request
|
||||
* @param context - Request context
|
||||
* @returns Response object
|
||||
*/
|
||||
abstract handleRequest(context: IRequestContext): Promise<IResponse>;
|
||||
|
||||
/**
|
||||
* Get the base path for this registry protocol
|
||||
*/
|
||||
abstract getBasePath(): string;
|
||||
|
||||
/**
|
||||
* Validate that a token has the required permissions
|
||||
* @param token - Authentication token
|
||||
* @param resource - Resource being accessed
|
||||
* @param action - Action being performed
|
||||
* @returns true if authorized
|
||||
*/
|
||||
protected abstract checkPermission(
|
||||
token: IAuthToken | null,
|
||||
resource: string,
|
||||
action: string
|
||||
): Promise<boolean>;
|
||||
}
|
||||
270
ts/core/classes.registrystorage.ts
Normal file
270
ts/core/classes.registrystorage.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { IStorageConfig, IStorageBackend } from './interfaces.core.js';
|
||||
|
||||
/**
|
||||
* Storage abstraction layer for registry
|
||||
* Provides a unified interface over SmartBucket
|
||||
*/
|
||||
export class RegistryStorage implements IStorageBackend {
|
||||
private smartBucket: plugins.smartbucket.SmartBucket;
|
||||
private bucket: plugins.smartbucket.Bucket;
|
||||
private bucketName: string;
|
||||
|
||||
constructor(private config: IStorageConfig) {
|
||||
this.bucketName = config.bucketName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the storage backend
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
this.smartBucket = new plugins.smartbucket.SmartBucket({
|
||||
accessKey: this.config.accessKey,
|
||||
accessSecret: this.config.accessSecret,
|
||||
endpoint: this.config.endpoint,
|
||||
port: this.config.port || 443,
|
||||
useSsl: this.config.useSsl !== false,
|
||||
region: this.config.region || 'us-east-1',
|
||||
});
|
||||
|
||||
// Ensure bucket exists
|
||||
await this.smartBucket.createBucket(this.bucketName).catch(() => {
|
||||
// Bucket may already exist
|
||||
});
|
||||
|
||||
this.bucket = await this.smartBucket.getBucketByName(this.bucketName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object from storage
|
||||
*/
|
||||
public async getObject(key: string): Promise<Buffer | null> {
|
||||
try {
|
||||
return await this.bucket.fastGet({ path: key });
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an object
|
||||
*/
|
||||
public async putObject(
|
||||
key: string,
|
||||
data: Buffer,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<void> {
|
||||
await this.bucket.fastPut({
|
||||
path: key,
|
||||
contents: data,
|
||||
meta: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
*/
|
||||
public async deleteObject(key: string): Promise<void> {
|
||||
await this.bucket.fastRemove({ path: key });
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects with a prefix
|
||||
*/
|
||||
public async listObjects(prefix: string): Promise<string[]> {
|
||||
const baseDir = await this.bucket.getBaseDirectory();
|
||||
const dir = prefix ? await baseDir.getSubDirectoryByName(prefix) : baseDir;
|
||||
if (!dir) return [];
|
||||
|
||||
const files = await dir.listFiles();
|
||||
return files.map(f => f.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object exists
|
||||
*/
|
||||
public async objectExists(key: string): Promise<boolean> {
|
||||
return await this.bucket.fastExists({ path: key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object metadata
|
||||
* Note: SmartBucket may not support metadata retrieval, returning empty object
|
||||
*/
|
||||
public async getMetadata(key: string): Promise<Record<string, string> | null> {
|
||||
// SmartBucket doesn't expose metadata retrieval directly
|
||||
// This is a limitation we'll document
|
||||
const exists = await this.objectExists(key);
|
||||
return exists ? {} : null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OCI-SPECIFIC HELPERS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get OCI blob by digest
|
||||
*/
|
||||
public async getOciBlob(digest: string): Promise<Buffer | null> {
|
||||
const path = this.getOciBlobPath(digest);
|
||||
return this.getObject(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store OCI blob
|
||||
*/
|
||||
public async putOciBlob(digest: string, data: Buffer): Promise<void> {
|
||||
const path = this.getOciBlobPath(digest);
|
||||
return this.putObject(path, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OCI blob exists
|
||||
*/
|
||||
public async ociBlobExists(digest: string): Promise<boolean> {
|
||||
const path = this.getOciBlobPath(digest);
|
||||
return this.objectExists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete OCI blob
|
||||
*/
|
||||
public async deleteOciBlob(digest: string): Promise<void> {
|
||||
const path = this.getOciBlobPath(digest);
|
||||
return this.deleteObject(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OCI manifest
|
||||
*/
|
||||
public async getOciManifest(repository: string, digest: string): Promise<Buffer | null> {
|
||||
const path = this.getOciManifestPath(repository, digest);
|
||||
return this.getObject(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store OCI manifest
|
||||
*/
|
||||
public async putOciManifest(
|
||||
repository: string,
|
||||
digest: string,
|
||||
data: Buffer,
|
||||
contentType: string
|
||||
): Promise<void> {
|
||||
const path = this.getOciManifestPath(repository, digest);
|
||||
return this.putObject(path, data, { 'Content-Type': contentType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OCI manifest exists
|
||||
*/
|
||||
public async ociManifestExists(repository: string, digest: string): Promise<boolean> {
|
||||
const path = this.getOciManifestPath(repository, digest);
|
||||
return this.objectExists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete OCI manifest
|
||||
*/
|
||||
public async deleteOciManifest(repository: string, digest: string): Promise<void> {
|
||||
const path = this.getOciManifestPath(repository, digest);
|
||||
return this.deleteObject(path);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// NPM-SPECIFIC HELPERS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get NPM packument (package document)
|
||||
*/
|
||||
public async getNpmPackument(packageName: string): Promise<any | null> {
|
||||
const path = this.getNpmPackumentPath(packageName);
|
||||
const data = await this.getObject(path);
|
||||
return data ? JSON.parse(data.toString('utf-8')) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store NPM packument
|
||||
*/
|
||||
public async putNpmPackument(packageName: string, packument: any): Promise<void> {
|
||||
const path = this.getNpmPackumentPath(packageName);
|
||||
const data = Buffer.from(JSON.stringify(packument, null, 2), 'utf-8');
|
||||
return this.putObject(path, data, { 'Content-Type': 'application/json' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPM packument exists
|
||||
*/
|
||||
public async npmPackumentExists(packageName: string): Promise<boolean> {
|
||||
const path = this.getNpmPackumentPath(packageName);
|
||||
return this.objectExists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete NPM packument
|
||||
*/
|
||||
public async deleteNpmPackument(packageName: string): Promise<void> {
|
||||
const path = this.getNpmPackumentPath(packageName);
|
||||
return this.deleteObject(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPM tarball
|
||||
*/
|
||||
public async getNpmTarball(packageName: string, version: string): Promise<Buffer | null> {
|
||||
const path = this.getNpmTarballPath(packageName, version);
|
||||
return this.getObject(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store NPM tarball
|
||||
*/
|
||||
public async putNpmTarball(
|
||||
packageName: string,
|
||||
version: string,
|
||||
tarball: Buffer
|
||||
): Promise<void> {
|
||||
const path = this.getNpmTarballPath(packageName, version);
|
||||
return this.putObject(path, tarball, { 'Content-Type': 'application/octet-stream' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPM tarball exists
|
||||
*/
|
||||
public async npmTarballExists(packageName: string, version: string): Promise<boolean> {
|
||||
const path = this.getNpmTarballPath(packageName, version);
|
||||
return this.objectExists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete NPM tarball
|
||||
*/
|
||||
public async deleteNpmTarball(packageName: string, version: string): Promise<void> {
|
||||
const path = this.getNpmTarballPath(packageName, version);
|
||||
return this.deleteObject(path);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PATH HELPERS
|
||||
// ========================================================================
|
||||
|
||||
private getOciBlobPath(digest: string): string {
|
||||
const hash = digest.split(':')[1];
|
||||
return `oci/blobs/sha256/${hash}`;
|
||||
}
|
||||
|
||||
private getOciManifestPath(repository: string, digest: string): string {
|
||||
const hash = digest.split(':')[1];
|
||||
return `oci/manifests/${repository}/${hash}`;
|
||||
}
|
||||
|
||||
private getNpmPackumentPath(packageName: string): string {
|
||||
return `npm/packages/${packageName}/index.json`;
|
||||
}
|
||||
|
||||
private getNpmTarballPath(packageName: string, version: string): string {
|
||||
const safeName = packageName.replace('@', '').replace('/', '-');
|
||||
return `npm/packages/${packageName}/${safeName}-${version}.tgz`;
|
||||
}
|
||||
}
|
||||
11
ts/core/index.ts
Normal file
11
ts/core/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Core registry infrastructure exports
|
||||
*/
|
||||
|
||||
// Interfaces
|
||||
export * from './interfaces.core.js';
|
||||
|
||||
// Classes
|
||||
export { BaseRegistry } from './classes.baseregistry.js';
|
||||
export { RegistryStorage } from './classes.registrystorage.js';
|
||||
export { AuthManager } from './classes.authmanager.js';
|
||||
159
ts/core/interfaces.core.ts
Normal file
159
ts/core/interfaces.core.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Core interfaces for the composable registry system
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registry protocol types
|
||||
*/
|
||||
export type TRegistryProtocol = 'oci' | 'npm';
|
||||
|
||||
/**
|
||||
* Unified action types across protocols
|
||||
*/
|
||||
export type TRegistryAction = 'pull' | 'push' | 'delete' | 'read' | 'write' | '*';
|
||||
|
||||
/**
|
||||
* Unified authentication token
|
||||
*/
|
||||
export interface IAuthToken {
|
||||
/** Token type/protocol */
|
||||
type: TRegistryProtocol;
|
||||
/** User ID */
|
||||
userId: string;
|
||||
/** Permission scopes (e.g., "npm:package:foo:write", "oci:repository:bar:push") */
|
||||
scopes: string[];
|
||||
/** Token expiration */
|
||||
expiresAt?: Date;
|
||||
/** Read-only flag */
|
||||
readonly?: boolean;
|
||||
/** Additional metadata */
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credentials for authentication
|
||||
*/
|
||||
export interface ICredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage backend configuration
|
||||
*/
|
||||
export interface IStorageConfig {
|
||||
accessKey: string;
|
||||
accessSecret: string;
|
||||
endpoint: string;
|
||||
port?: number;
|
||||
useSsl?: boolean;
|
||||
region?: string;
|
||||
bucketName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication configuration
|
||||
*/
|
||||
export interface IAuthConfig {
|
||||
/** JWT secret for OCI tokens */
|
||||
jwtSecret: string;
|
||||
/** Token storage type */
|
||||
tokenStore: 'memory' | 'redis' | 'database';
|
||||
/** NPM token settings */
|
||||
npmTokens: {
|
||||
enabled: boolean;
|
||||
defaultReadonly?: boolean;
|
||||
};
|
||||
/** OCI token settings */
|
||||
ociTokens: {
|
||||
enabled: boolean;
|
||||
realm: string;
|
||||
service: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Protocol-specific configuration
|
||||
*/
|
||||
export interface IProtocolConfig {
|
||||
enabled: boolean;
|
||||
basePath: string;
|
||||
features?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main registry configuration
|
||||
*/
|
||||
export interface IRegistryConfig {
|
||||
storage: IStorageConfig;
|
||||
auth: IAuthConfig;
|
||||
oci?: IProtocolConfig;
|
||||
npm?: IProtocolConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage backend interface
|
||||
*/
|
||||
export interface IStorageBackend {
|
||||
/**
|
||||
* Get an object from storage
|
||||
*/
|
||||
getObject(key: string): Promise<Buffer | null>;
|
||||
|
||||
/**
|
||||
* Store an object
|
||||
*/
|
||||
putObject(key: string, data: Buffer, metadata?: Record<string, string>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
*/
|
||||
deleteObject(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* List objects with a prefix
|
||||
*/
|
||||
listObjects(prefix: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Check if an object exists
|
||||
*/
|
||||
objectExists(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get object metadata
|
||||
*/
|
||||
getMetadata(key: string): Promise<Record<string, string> | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response structure
|
||||
*/
|
||||
export interface IRegistryError {
|
||||
errors: Array<{
|
||||
code: string;
|
||||
message: string;
|
||||
detail?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base request context
|
||||
*/
|
||||
export interface IRequestContext {
|
||||
method: string;
|
||||
path: string;
|
||||
headers: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
body?: any;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base response structure
|
||||
*/
|
||||
export interface IResponse {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
Reference in New Issue
Block a user