feat(core): Add Cargo and Composer registries with storage, auth and helpers

This commit is contained in:
2025-11-21 09:13:02 +00:00
parent 92d27d8b15
commit 8d48627301
19 changed files with 1869 additions and 56 deletions

View File

@@ -270,6 +270,53 @@ export class AuthManager {
this.tokenStore.delete(token);
}
// ========================================================================
// COMPOSER TOKEN MANAGEMENT
// ========================================================================
/**
* Create a Composer token
* @param userId - User ID
* @param readonly - Whether the token is readonly
* @returns Composer UUID token
*/
public async createComposerToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['composer:*:*:read'] : ['composer:*:*:*'];
return this.createUuidToken(userId, 'composer', scopes, readonly);
}
/**
* Validate a Composer token
* @param token - Composer UUID token
* @returns Auth token object or null
*/
public async validateComposerToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) {
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'composer') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
}
/**
* Revoke a Composer token
* @param token - Composer UUID token
*/
public async revokeComposerToken(token: string): Promise<void> {
this.tokenStore.delete(token);
}
// ========================================================================
// UNIFIED AUTHENTICATION
// ========================================================================
@@ -284,7 +331,7 @@ export class AuthManager {
tokenString: string,
protocol?: TRegistryProtocol
): Promise<IAuthToken | null> {
// Try UUID-based tokens (NPM, Maven)
// Try UUID-based tokens (NPM, Maven, Composer)
if (this.isValidUuid(tokenString)) {
// Try NPM token
const npmToken = await this.validateNpmToken(tokenString);
@@ -297,6 +344,12 @@ export class AuthManager {
if (mavenToken && (!protocol || protocol === 'maven')) {
return mavenToken;
}
// Try Composer token
const composerToken = await this.validateComposerToken(tokenString);
if (composerToken && (!protocol || protocol === 'composer')) {
return composerToken;
}
}
// Try OCI JWT

View File

@@ -392,4 +392,202 @@ export class RegistryStorage implements IStorageBackend {
const groupPath = groupId.replace(/\./g, '/');
return `maven/metadata/${groupPath}/${artifactId}/maven-metadata.xml`;
}
// ========================================================================
// CARGO-SPECIFIC HELPERS
// ========================================================================
/**
* Get Cargo config.json
*/
public async getCargoConfig(): Promise<any | null> {
const data = await this.getObject('cargo/config.json');
return data ? JSON.parse(data.toString('utf-8')) : null;
}
/**
* Store Cargo config.json
*/
public async putCargoConfig(config: any): Promise<void> {
const data = Buffer.from(JSON.stringify(config, null, 2), 'utf-8');
return this.putObject('cargo/config.json', data, { 'Content-Type': 'application/json' });
}
/**
* Get Cargo index file (newline-delimited JSON)
*/
public async getCargoIndex(crateName: string): Promise<any[] | null> {
const path = this.getCargoIndexPath(crateName);
const data = await this.getObject(path);
if (!data) return null;
// Parse newline-delimited JSON
const lines = data.toString('utf-8').split('\n').filter(line => line.trim());
return lines.map(line => JSON.parse(line));
}
/**
* Store Cargo index file
*/
public async putCargoIndex(crateName: string, entries: any[]): Promise<void> {
const path = this.getCargoIndexPath(crateName);
// Convert to newline-delimited JSON
const data = Buffer.from(entries.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
return this.putObject(path, data, { 'Content-Type': 'text/plain' });
}
/**
* Get Cargo .crate file
*/
public async getCargoCrate(crateName: string, version: string): Promise<Buffer | null> {
const path = this.getCargoCratePath(crateName, version);
return this.getObject(path);
}
/**
* Store Cargo .crate file
*/
public async putCargoCrate(
crateName: string,
version: string,
crateFile: Buffer
): Promise<void> {
const path = this.getCargoCratePath(crateName, version);
return this.putObject(path, crateFile, { 'Content-Type': 'application/gzip' });
}
/**
* Check if Cargo crate exists
*/
public async cargoCrateExists(crateName: string, version: string): Promise<boolean> {
const path = this.getCargoCratePath(crateName, version);
return this.objectExists(path);
}
/**
* Delete Cargo crate (for cleanup, not for unpublishing)
*/
public async deleteCargoCrate(crateName: string, version: string): Promise<void> {
const path = this.getCargoCratePath(crateName, version);
return this.deleteObject(path);
}
// ========================================================================
// CARGO PATH HELPERS
// ========================================================================
private getCargoIndexPath(crateName: string): string {
const lower = crateName.toLowerCase();
const len = lower.length;
if (len === 1) {
return `cargo/index/1/${lower}`;
} else if (len === 2) {
return `cargo/index/2/${lower}`;
} else if (len === 3) {
return `cargo/index/3/${lower.charAt(0)}/${lower}`;
} else {
// 4+ characters: {first-two}/{second-two}/{name}
const prefix1 = lower.substring(0, 2);
const prefix2 = lower.substring(2, 4);
return `cargo/index/${prefix1}/${prefix2}/${lower}`;
}
}
private getCargoCratePath(crateName: string, version: string): string {
return `cargo/crates/${crateName}/${crateName}-${version}.crate`;
}
// ========================================================================
// COMPOSER-SPECIFIC HELPERS
// ========================================================================
/**
* Get Composer package metadata
*/
public async getComposerPackageMetadata(vendorPackage: string): Promise<any | null> {
const path = this.getComposerMetadataPath(vendorPackage);
const data = await this.getObject(path);
return data ? JSON.parse(data.toString('utf-8')) : null;
}
/**
* Store Composer package metadata
*/
public async putComposerPackageMetadata(vendorPackage: string, metadata: any): Promise<void> {
const path = this.getComposerMetadataPath(vendorPackage);
const data = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
return this.putObject(path, data, { 'Content-Type': 'application/json' });
}
/**
* Get Composer package ZIP
*/
public async getComposerPackageZip(vendorPackage: string, reference: string): Promise<Buffer | null> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.getObject(path);
}
/**
* Store Composer package ZIP
*/
public async putComposerPackageZip(vendorPackage: string, reference: string, zipData: Buffer): Promise<void> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.putObject(path, zipData, { 'Content-Type': 'application/zip' });
}
/**
* Check if Composer package metadata exists
*/
public async composerPackageMetadataExists(vendorPackage: string): Promise<boolean> {
const path = this.getComposerMetadataPath(vendorPackage);
return this.objectExists(path);
}
/**
* Delete Composer package metadata
*/
public async deleteComposerPackageMetadata(vendorPackage: string): Promise<void> {
const path = this.getComposerMetadataPath(vendorPackage);
return this.deleteObject(path);
}
/**
* Delete Composer package ZIP
*/
public async deleteComposerPackageZip(vendorPackage: string, reference: string): Promise<void> {
const path = this.getComposerZipPath(vendorPackage, reference);
return this.deleteObject(path);
}
/**
* List all Composer packages
*/
public async listComposerPackages(): Promise<string[]> {
const prefix = 'composer/packages/';
const objects = await this.listObjects(prefix);
const packages = new Set<string>();
// Extract vendor/package from paths like: composer/packages/vendor/package/metadata.json
for (const obj of objects) {
const match = obj.match(/^composer\/packages\/([^\/]+\/[^\/]+)\/metadata\.json$/);
if (match) {
packages.add(match[1]);
}
}
return Array.from(packages).sort();
}
// ========================================================================
// COMPOSER PATH HELPERS
// ========================================================================
private getComposerMetadataPath(vendorPackage: string): string {
return `composer/packages/${vendorPackage}/metadata.json`;
}
private getComposerZipPath(vendorPackage: string, reference: string): string {
return `composer/packages/${vendorPackage}/${reference}.zip`;
}
}

View File

@@ -5,7 +5,7 @@
/**
* Registry protocol types
*/
export type TRegistryProtocol = 'oci' | 'npm' | 'maven';
export type TRegistryProtocol = 'oci' | 'npm' | 'maven' | 'cargo' | 'composer';
/**
* Unified action types across protocols
@@ -90,6 +90,8 @@ export interface IRegistryConfig {
oci?: IProtocolConfig;
npm?: IProtocolConfig;
maven?: IProtocolConfig;
cargo?: IProtocolConfig;
composer?: IProtocolConfig;
}
/**