feat(core): Add PyPI and RubyGems registries, integrate into SmartRegistry, extend storage and auth

This commit is contained in:
2025-11-21 17:13:06 +00:00
parent ac51a94c8b
commit 0d73230d5a
17 changed files with 3514 additions and 33 deletions

View File

@@ -0,0 +1,598 @@
import { Smartlog } from '@push.rocks/smartlog';
import { BaseRegistry } from '../core/classes.baseregistry.js';
import { RegistryStorage } from '../core/classes.registrystorage.js';
import { AuthManager } from '../core/classes.authmanager.js';
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
import type {
IRubyGemsMetadata,
IRubyGemsVersionMetadata,
IRubyGemsUploadResponse,
IRubyGemsYankResponse,
IRubyGemsError,
ICompactIndexInfoEntry,
} from './interfaces.rubygems.js';
import * as helpers from './helpers.rubygems.js';
/**
* RubyGems registry implementation
* Implements Compact Index API and RubyGems protocol
*/
export class RubyGemsRegistry extends BaseRegistry {
private storage: RegistryStorage;
private authManager: AuthManager;
private basePath: string = '/rubygems';
private registryUrl: string;
private logger: Smartlog;
constructor(
storage: RegistryStorage,
authManager: AuthManager,
basePath: string = '/rubygems',
registryUrl: string = 'http://localhost:5000/rubygems'
) {
super();
this.storage = storage;
this.authManager = authManager;
this.basePath = basePath;
this.registryUrl = registryUrl;
// Initialize logger
this.logger = new Smartlog({
logContext: {
company: 'push.rocks',
companyunit: 'smartregistry',
containerName: 'rubygems-registry',
environment: (process.env.NODE_ENV as any) || 'development',
runtime: 'node',
zone: 'rubygems'
}
});
this.logger.enableConsole();
}
public async init(): Promise<void> {
// Initialize Compact Index files if not exist
const existingVersions = await this.storage.getRubyGemsVersions();
if (!existingVersions) {
const versions = helpers.generateCompactIndexVersions([]);
await this.storage.putRubyGemsVersions(versions);
this.logger.log('info', 'Initialized RubyGems Compact Index');
}
const existingNames = await this.storage.getRubyGemsNames();
if (!existingNames) {
const names = helpers.generateNamesFile([]);
await this.storage.putRubyGemsNames(names);
this.logger.log('info', 'Initialized RubyGems names file');
}
}
public getBasePath(): string {
return this.basePath;
}
public async handleRequest(context: IRequestContext): Promise<IResponse> {
let path = context.path.replace(this.basePath, '');
// Extract token (Authorization header)
const token = await this.extractToken(context);
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
method: context.method,
path,
hasAuth: !!token
});
// Compact Index endpoints
if (path === '/versions' && context.method === 'GET') {
return this.handleVersionsFile();
}
if (path === '/names' && context.method === 'GET') {
return this.handleNamesFile();
}
// Info file: GET /info/{gem}
const infoMatch = path.match(/^\/info\/([^\/]+)$/);
if (infoMatch && context.method === 'GET') {
return this.handleInfoFile(infoMatch[1]);
}
// Gem download: GET /gems/{gem}-{version}[-{platform}].gem
const downloadMatch = path.match(/^\/gems\/(.+\.gem)$/);
if (downloadMatch && context.method === 'GET') {
return this.handleDownload(downloadMatch[1]);
}
// API v1 endpoints
if (path.startsWith('/api/v1/')) {
return this.handleApiRequest(path.substring(8), context, token);
}
return {
status: 404,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify({ message: 'Not Found' })),
};
}
/**
* Check if token has permission for resource
*/
protected async checkPermission(
token: IAuthToken | null,
resource: string,
action: string
): Promise<boolean> {
if (!token) return false;
return this.authManager.authorize(token, `rubygems:gem:${resource}`, action);
}
/**
* Extract authentication token from request
*/
private async extractToken(context: IRequestContext): Promise<IAuthToken | null> {
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
if (!authHeader) return null;
// RubyGems typically uses plain API key in Authorization header
return this.authManager.validateToken(authHeader, 'rubygems');
}
/**
* Handle /versions endpoint (Compact Index)
*/
private async handleVersionsFile(): Promise<IResponse> {
const content = await this.storage.getRubyGemsVersions();
if (!content) {
return this.errorResponse(500, 'Versions file not initialized');
}
return {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=60',
'ETag': `"${await helpers.calculateMD5(content)}"`
},
body: Buffer.from(content),
};
}
/**
* Handle /names endpoint (Compact Index)
*/
private async handleNamesFile(): Promise<IResponse> {
const content = await this.storage.getRubyGemsNames();
if (!content) {
return this.errorResponse(500, 'Names file not initialized');
}
return {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=300'
},
body: Buffer.from(content),
};
}
/**
* Handle /info/{gem} endpoint (Compact Index)
*/
private async handleInfoFile(gemName: string): Promise<IResponse> {
const content = await this.storage.getRubyGemsInfo(gemName);
if (!content) {
return {
status: 404,
headers: { 'Content-Type': 'text/plain' },
body: Buffer.from('Not Found'),
};
}
return {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=300',
'ETag': `"${await helpers.calculateMD5(content)}"`
},
body: Buffer.from(content),
};
}
/**
* Handle gem file download
*/
private async handleDownload(filename: string): Promise<IResponse> {
const parsed = helpers.parseGemFilename(filename);
if (!parsed) {
return this.errorResponse(400, 'Invalid gem filename');
}
const gemData = await this.storage.getRubyGemsGem(
parsed.name,
parsed.version,
parsed.platform
);
if (!gemData) {
return this.errorResponse(404, 'Gem not found');
}
return {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': gemData.length.toString()
},
body: gemData,
};
}
/**
* Handle API v1 requests
*/
private async handleApiRequest(
path: string,
context: IRequestContext,
token: IAuthToken | null
): Promise<IResponse> {
// Upload gem: POST /gems
if (path === '/gems' && context.method === 'POST') {
return this.handleUpload(context, token);
}
// Yank gem: DELETE /gems/yank
if (path === '/gems/yank' && context.method === 'DELETE') {
return this.handleYank(context, token);
}
// Unyank gem: PUT /gems/unyank
if (path === '/gems/unyank' && context.method === 'PUT') {
return this.handleUnyank(context, token);
}
// Version list: GET /versions/{gem}.json
const versionsMatch = path.match(/^\/versions\/([^\/]+)\.json$/);
if (versionsMatch && context.method === 'GET') {
return this.handleVersionsJson(versionsMatch[1]);
}
// Dependencies: GET /dependencies?gems={list}
if (path.startsWith('/dependencies') && context.method === 'GET') {
const gemsParam = context.query?.gems || '';
return this.handleDependencies(gemsParam);
}
return this.errorResponse(404, 'API endpoint not found');
}
/**
* Handle gem upload
* POST /api/v1/gems
*/
private async handleUpload(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
if (!token) {
return this.errorResponse(401, 'Authentication required');
}
try {
// Extract gem data from request body
const gemData = context.body as Buffer;
if (!gemData || gemData.length === 0) {
return this.errorResponse(400, 'No gem file provided');
}
// For now, we expect metadata in query params or headers
// Full implementation would parse .gem file (tar + gzip + Marshal)
const gemName = context.query?.name || context.headers['x-gem-name'];
const version = context.query?.version || context.headers['x-gem-version'];
const platform = context.query?.platform || context.headers['x-gem-platform'];
if (!gemName || !version) {
return this.errorResponse(400, 'Gem name and version required');
}
// Validate gem name
if (!helpers.isValidGemName(gemName)) {
return this.errorResponse(400, 'Invalid gem name');
}
// Check permission
if (!(await this.checkPermission(token, gemName, 'write'))) {
return this.errorResponse(403, 'Insufficient permissions');
}
// Calculate checksum
const checksum = await helpers.calculateSHA256(gemData);
// Store gem file
await this.storage.putRubyGemsGem(gemName, version, gemData, platform);
// Update metadata
let metadata: IRubyGemsMetadata = await this.storage.getRubyGemsMetadata(gemName) || {
name: gemName,
versions: {},
};
const versionKey = platform ? `${version}-${platform}` : version;
metadata.versions[versionKey] = {
version,
platform,
checksum,
size: gemData.length,
'upload-time': new Date().toISOString(),
'uploaded-by': token.userId,
dependencies: [], // Would extract from gem spec
requirements: [],
};
metadata['last-modified'] = new Date().toISOString();
await this.storage.putRubyGemsMetadata(gemName, metadata);
// Update Compact Index info file
await this.updateCompactIndexForGem(gemName, metadata);
// Update versions file
await this.updateVersionsFile(gemName, version, platform || 'ruby', false);
// Update names file
await this.updateNamesFile(gemName);
this.logger.log('info', `Gem uploaded: ${gemName} ${version}`, {
platform,
size: gemData.length
});
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify({
message: 'Gem uploaded successfully',
name: gemName,
version,
})),
};
} catch (error) {
this.logger.log('error', 'Upload failed', { error: (error as Error).message });
return this.errorResponse(500, 'Upload failed: ' + (error as Error).message);
}
}
/**
* Handle gem yanking
* DELETE /api/v1/gems/yank
*/
private async handleYank(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
if (!token) {
return this.errorResponse(401, 'Authentication required');
}
const gemName = context.query?.gem_name;
const version = context.query?.version;
const platform = context.query?.platform;
if (!gemName || !version) {
return this.errorResponse(400, 'Gem name and version required');
}
if (!(await this.checkPermission(token, gemName, 'yank'))) {
return this.errorResponse(403, 'Insufficient permissions');
}
// Update metadata to mark as yanked
const metadata = await this.storage.getRubyGemsMetadata(gemName);
if (!metadata) {
return this.errorResponse(404, 'Gem not found');
}
const versionKey = platform ? `${version}-${platform}` : version;
if (!metadata.versions[versionKey]) {
return this.errorResponse(404, 'Version not found');
}
metadata.versions[versionKey].yanked = true;
await this.storage.putRubyGemsMetadata(gemName, metadata);
// Update Compact Index
await this.updateCompactIndexForGem(gemName, metadata);
await this.updateVersionsFile(gemName, version, platform || 'ruby', true);
this.logger.log('info', `Gem yanked: ${gemName} ${version}`);
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify({
success: true,
message: 'Gem yanked successfully'
})),
};
}
/**
* Handle gem unyanking
* PUT /api/v1/gems/unyank
*/
private async handleUnyank(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
if (!token) {
return this.errorResponse(401, 'Authentication required');
}
const gemName = context.query?.gem_name;
const version = context.query?.version;
const platform = context.query?.platform;
if (!gemName || !version) {
return this.errorResponse(400, 'Gem name and version required');
}
if (!(await this.checkPermission(token, gemName, 'write'))) {
return this.errorResponse(403, 'Insufficient permissions');
}
const metadata = await this.storage.getRubyGemsMetadata(gemName);
if (!metadata) {
return this.errorResponse(404, 'Gem not found');
}
const versionKey = platform ? `${version}-${platform}` : version;
if (!metadata.versions[versionKey]) {
return this.errorResponse(404, 'Version not found');
}
metadata.versions[versionKey].yanked = false;
await this.storage.putRubyGemsMetadata(gemName, metadata);
// Update Compact Index
await this.updateCompactIndexForGem(gemName, metadata);
await this.updateVersionsFile(gemName, version, platform || 'ruby', false);
this.logger.log('info', `Gem unyanked: ${gemName} ${version}`);
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify({
success: true,
message: 'Gem unyanked successfully'
})),
};
}
/**
* Handle versions JSON API
*/
private async handleVersionsJson(gemName: string): Promise<IResponse> {
const metadata = await this.storage.getRubyGemsMetadata(gemName);
if (!metadata) {
return this.errorResponse(404, 'Gem not found');
}
const versions = Object.values(metadata.versions).map((v: any) => ({
version: v.version,
platform: v.platform,
uploadTime: v['upload-time'],
}));
const response = helpers.generateVersionsJson(gemName, versions);
return {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300'
},
body: Buffer.from(JSON.stringify(response)),
};
}
/**
* Handle dependencies query
*/
private async handleDependencies(gemsParam: string): Promise<IResponse> {
const gemNames = gemsParam.split(',').filter(n => n.trim());
const result = new Map();
for (const gemName of gemNames) {
const metadata = await this.storage.getRubyGemsMetadata(gemName);
if (metadata) {
const versions = Object.values(metadata.versions).map((v: any) => ({
version: v.version,
platform: v.platform,
dependencies: v.dependencies || [],
}));
result.set(gemName, versions);
}
}
const response = helpers.generateDependenciesJson(result);
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify(response)),
};
}
/**
* Update Compact Index info file for a gem
*/
private async updateCompactIndexForGem(
gemName: string,
metadata: IRubyGemsMetadata
): Promise<void> {
const entries: ICompactIndexInfoEntry[] = Object.values(metadata.versions)
.filter(v => !v.yanked) // Exclude yanked from info file
.map(v => ({
version: v.version,
platform: v.platform,
dependencies: v.dependencies || [],
requirements: v.requirements || [],
checksum: v.checksum,
}));
const content = helpers.generateCompactIndexInfo(entries);
await this.storage.putRubyGemsInfo(gemName, content);
}
/**
* Update versions file with new/updated gem
*/
private async updateVersionsFile(
gemName: string,
version: string,
platform: string,
yanked: boolean
): Promise<void> {
const existingVersions = await this.storage.getRubyGemsVersions();
if (!existingVersions) return;
// Calculate info file checksum
const infoContent = await this.storage.getRubyGemsInfo(gemName) || '';
const infoChecksum = await helpers.calculateMD5(infoContent);
const updated = helpers.updateCompactIndexVersions(
existingVersions,
gemName,
{ version, platform: platform !== 'ruby' ? platform : undefined, yanked },
infoChecksum
);
await this.storage.putRubyGemsVersions(updated);
}
/**
* Update names file with new gem
*/
private async updateNamesFile(gemName: string): Promise<void> {
const existingNames = await this.storage.getRubyGemsNames();
if (!existingNames) return;
const lines = existingNames.split('\n').filter(l => l !== '---');
if (!lines.includes(gemName)) {
lines.push(gemName);
lines.sort();
const updated = helpers.generateNamesFile(lines);
await this.storage.putRubyGemsNames(updated);
}
}
/**
* Helper: Create error response
*/
private errorResponse(status: number, message: string): IResponse {
const error: IRubyGemsError = { message, status };
return {
status,
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify(error)),
};
}
}

View File

@@ -0,0 +1,398 @@
/**
* Helper functions for RubyGems registry
* Compact Index generation, dependency formatting, etc.
*/
import type {
IRubyGemsVersion,
IRubyGemsDependency,
IRubyGemsRequirement,
ICompactIndexVersionsEntry,
ICompactIndexInfoEntry,
IRubyGemsMetadata,
} from './interfaces.rubygems.js';
/**
* Generate Compact Index versions file
* Format: GEMNAME [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
* @param entries - Version entries for all gems
* @returns Compact Index versions file content
*/
export function generateCompactIndexVersions(entries: ICompactIndexVersionsEntry[]): string {
const lines: string[] = [];
// Add metadata header
lines.push(`created_at: ${new Date().toISOString()}`);
lines.push('---');
// Add gem entries
for (const entry of entries) {
const versions = entry.versions
.map(v => {
const yanked = v.yanked ? '-' : '';
const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
return `${yanked}${v.version}${platform}`;
})
.join(',');
lines.push(`${entry.name} ${versions} ${entry.infoChecksum}`);
}
return lines.join('\n');
}
/**
* Generate Compact Index info file for a gem
* Format: VERSION[-PLATFORM] [DEP[,DEP,...]]|REQ[,REQ,...]
* @param entries - Info entries for gem versions
* @returns Compact Index info file content
*/
export function generateCompactIndexInfo(entries: ICompactIndexInfoEntry[]): string {
const lines: string[] = ['---']; // Info files start with ---
for (const entry of entries) {
// Build version string with optional platform
const versionStr = entry.platform && entry.platform !== 'ruby'
? `${entry.version}-${entry.platform}`
: entry.version;
// Build dependencies string
const depsStr = entry.dependencies.length > 0
? entry.dependencies.map(formatDependency).join(',')
: '';
// Build requirements string (checksum is always required)
const reqParts: string[] = [`checksum:${entry.checksum}`];
for (const req of entry.requirements) {
reqParts.push(`${req.type}:${req.requirement}`);
}
const reqStr = reqParts.join(',');
// Combine: VERSION[-PLATFORM] [DEPS]|REQS
const depPart = depsStr ? ` ${depsStr}` : '';
lines.push(`${versionStr}${depPart}|${reqStr}`);
}
return lines.join('\n');
}
/**
* Format a dependency for Compact Index
* Format: GEM:CONSTRAINT[&CONSTRAINT]
* @param dep - Dependency object
* @returns Formatted dependency string
*/
export function formatDependency(dep: IRubyGemsDependency): string {
return `${dep.name}:${dep.requirement}`;
}
/**
* Parse dependency string from Compact Index
* @param depStr - Dependency string
* @returns Dependency object
*/
export function parseDependency(depStr: string): IRubyGemsDependency {
const [name, ...reqParts] = depStr.split(':');
const requirement = reqParts.join(':'); // Handle :: in gem names
return { name, requirement };
}
/**
* Generate names file (newline-separated gem names)
* @param names - List of gem names
* @returns Names file content
*/
export function generateNamesFile(names: string[]): string {
return `---\n${names.sort().join('\n')}`;
}
/**
* Calculate MD5 hash for Compact Index checksum
* @param content - Content to hash
* @returns MD5 hash (hex)
*/
export async function calculateMD5(content: string): Promise<string> {
const crypto = await import('crypto');
return crypto.createHash('md5').update(content).digest('hex');
}
/**
* Calculate SHA256 hash for gem files
* @param data - Data to hash
* @returns SHA256 hash (hex)
*/
export async function calculateSHA256(data: Buffer): Promise<string> {
const crypto = await import('crypto');
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Parse gem filename to extract name, version, and platform
* @param filename - Gem filename (e.g., "rails-7.0.0-x86_64-linux.gem")
* @returns Parsed info or null
*/
export function parseGemFilename(filename: string): {
name: string;
version: string;
platform?: string;
} | null {
if (!filename.endsWith('.gem')) return null;
const withoutExt = filename.slice(0, -4); // Remove .gem
// Try to match: name-version-platform
// Platform can contain hyphens (e.g., x86_64-linux)
const parts = withoutExt.split('-');
if (parts.length < 2) return null;
// Find version (first part that starts with a digit)
let versionIndex = -1;
for (let i = 1; i < parts.length; i++) {
if (/^\d/.test(parts[i])) {
versionIndex = i;
break;
}
}
if (versionIndex === -1) return null;
const name = parts.slice(0, versionIndex).join('-');
const version = parts[versionIndex];
const platform = versionIndex + 1 < parts.length
? parts.slice(versionIndex + 1).join('-')
: undefined;
return {
name,
version,
platform: platform && platform !== 'ruby' ? platform : undefined,
};
}
/**
* Validate gem name
* Must contain only ASCII letters, numbers, _, and -
* @param name - Gem name
* @returns true if valid
*/
export function isValidGemName(name: string): boolean {
return /^[a-zA-Z0-9_-]+$/.test(name);
}
/**
* Validate version string
* Basic semantic versioning check
* @param version - Version string
* @returns true if valid
*/
export function isValidVersion(version: string): boolean {
// Allow semver and other common Ruby version formats
return /^[\d.a-zA-Z_-]+$/.test(version);
}
/**
* Build version list entry for Compact Index
* @param versions - Version info
* @returns Version list string
*/
export function buildVersionList(versions: Array<{
version: string;
platform?: string;
yanked: boolean;
}>): string {
return versions
.map(v => {
const yanked = v.yanked ? '-' : '';
const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
return `${yanked}${v.version}${platform}`;
})
.join(',');
}
/**
* Parse version list from Compact Index
* @param versionStr - Version list string
* @returns Parsed versions
*/
export function parseVersionList(versionStr: string): Array<{
version: string;
platform?: string;
yanked: boolean;
}> {
return versionStr.split(',').map(v => {
const yanked = v.startsWith('-');
const withoutYank = yanked ? v.substring(1) : v;
// Split on _ to separate version from platform
const [version, ...platformParts] = withoutYank.split('_');
const platform = platformParts.length > 0 ? platformParts.join('_') : undefined;
return {
version,
platform: platform && platform !== 'ruby' ? platform : undefined,
yanked,
};
});
}
/**
* Generate JSON response for /api/v1/versions/{gem}.json
* @param gemName - Gem name
* @param versions - Version list
* @returns JSON response object
*/
export function generateVersionsJson(
gemName: string,
versions: Array<{
version: string;
platform?: string;
uploadTime?: string;
}>
): any {
return {
name: gemName,
versions: versions.map(v => ({
number: v.version,
platform: v.platform || 'ruby',
built_at: v.uploadTime,
})),
};
}
/**
* Generate JSON response for /api/v1/dependencies
* @param gems - Map of gem names to version dependencies
* @returns JSON response array
*/
export function generateDependenciesJson(gems: Map<string, Array<{
version: string;
platform?: string;
dependencies: IRubyGemsDependency[];
}>>): any {
const result: any[] = [];
for (const [name, versions] of gems) {
for (const v of versions) {
result.push({
name,
number: v.version,
platform: v.platform || 'ruby',
dependencies: v.dependencies.map(d => ({
name: d.name,
requirements: d.requirement,
})),
});
}
}
return result;
}
/**
* Update Compact Index versions file with new gem version
* Handles append-only semantics for the current month
* @param existingContent - Current versions file content
* @param gemName - Gem name
* @param newVersion - New version info
* @param infoChecksum - MD5 of info file
* @returns Updated versions file content
*/
export function updateCompactIndexVersions(
existingContent: string,
gemName: string,
newVersion: { version: string; platform?: string; yanked: boolean },
infoChecksum: string
): string {
const lines = existingContent.split('\n');
const headerEndIndex = lines.findIndex(l => l === '---');
if (headerEndIndex === -1) {
throw new Error('Invalid Compact Index versions file');
}
const header = lines.slice(0, headerEndIndex + 1);
const entries = lines.slice(headerEndIndex + 1).filter(l => l.trim());
// Find existing entry for gem
const gemLineIndex = entries.findIndex(l => l.startsWith(`${gemName} `));
const versionStr = buildVersionList([newVersion]);
if (gemLineIndex >= 0) {
// Append to existing entry
const parts = entries[gemLineIndex].split(' ');
const existingVersions = parts[1];
const updatedVersions = `${existingVersions},${versionStr}`;
entries[gemLineIndex] = `${gemName} ${updatedVersions} ${infoChecksum}`;
} else {
// Add new entry
entries.push(`${gemName} ${versionStr} ${infoChecksum}`);
entries.sort(); // Keep alphabetical
}
return [...header, ...entries].join('\n');
}
/**
* Update Compact Index info file with new version
* @param existingContent - Current info file content
* @param newEntry - New version entry
* @returns Updated info file content
*/
export function updateCompactIndexInfo(
existingContent: string,
newEntry: ICompactIndexInfoEntry
): string {
const lines = existingContent ? existingContent.split('\n').filter(l => l !== '---') : [];
// Build version string
const versionStr = newEntry.platform && newEntry.platform !== 'ruby'
? `${newEntry.version}-${newEntry.platform}`
: newEntry.version;
// Build dependencies string
const depsStr = newEntry.dependencies.length > 0
? newEntry.dependencies.map(formatDependency).join(',')
: '';
// Build requirements string
const reqParts: string[] = [`checksum:${newEntry.checksum}`];
for (const req of newEntry.requirements) {
reqParts.push(`${req.type}:${req.requirement}`);
}
const reqStr = reqParts.join(',');
// Combine
const depPart = depsStr ? ` ${depsStr}` : '';
const newLine = `${versionStr}${depPart}|${reqStr}`;
lines.push(newLine);
return `---\n${lines.join('\n')}`;
}
/**
* Extract gem specification from .gem file
* Note: This is a simplified version. Full implementation would use tar + gzip + Marshal
* @param gemData - Gem file data
* @returns Extracted spec or null
*/
export async function extractGemSpec(gemData: Buffer): Promise<any | null> {
try {
// .gem files are gzipped tar archives
// They contain metadata.gz which has Marshal-encoded spec
// This is a placeholder - full implementation would need:
// 1. Unzip outer gzip
// 2. Untar to find metadata.gz
// 3. Unzip metadata.gz
// 4. Parse Ruby Marshal format
// For now, return null and expect metadata to be provided
return null;
} catch (error) {
return null;
}
}

8
ts/rubygems/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* RubyGems Registry Module
* RubyGems/Bundler Compact Index implementation
*/
export * from './interfaces.rubygems.js';
export * from './classes.rubygemsregistry.js';
export * as rubygemsHelpers from './helpers.rubygems.js';

View File

@@ -0,0 +1,251 @@
/**
* RubyGems Registry Type Definitions
* Compliant with Compact Index API and RubyGems protocol
*/
/**
* Gem version entry in compact index
*/
export interface IRubyGemsVersion {
/** Version number */
version: string;
/** Platform (e.g., ruby, x86_64-linux) */
platform?: string;
/** Dependencies */
dependencies?: IRubyGemsDependency[];
/** Requirements */
requirements?: IRubyGemsRequirement[];
/** Whether this version is yanked */
yanked?: boolean;
/** SHA256 checksum of .gem file */
checksum?: string;
}
/**
* Gem dependency specification
*/
export interface IRubyGemsDependency {
/** Gem name */
name: string;
/** Version requirement (e.g., ">= 1.0", "~> 2.0") */
requirement: string;
}
/**
* Gem requirements (ruby version, rubygems version, etc.)
*/
export interface IRubyGemsRequirement {
/** Requirement type (ruby, rubygems) */
type: 'ruby' | 'rubygems';
/** Version requirement */
requirement: string;
}
/**
* Complete gem metadata
*/
export interface IRubyGemsMetadata {
/** Gem name */
name: string;
/** All versions */
versions: Record<string, IRubyGemsVersionMetadata>;
/** Last modified timestamp */
'last-modified'?: string;
}
/**
* Version-specific metadata
*/
export interface IRubyGemsVersionMetadata {
/** Version number */
version: string;
/** Platform */
platform?: string;
/** Authors */
authors?: string[];
/** Description */
description?: string;
/** Summary */
summary?: string;
/** Homepage */
homepage?: string;
/** License */
license?: string;
/** Dependencies */
dependencies?: IRubyGemsDependency[];
/** Requirements */
requirements?: IRubyGemsRequirement[];
/** SHA256 checksum */
checksum: string;
/** File size */
size: number;
/** Upload timestamp */
'upload-time': string;
/** Uploader */
'uploaded-by': string;
/** Yanked status */
yanked?: boolean;
/** Yank reason */
'yank-reason'?: string;
}
/**
* Compact index versions file entry
* Format: GEMNAME [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
*/
export interface ICompactIndexVersionsEntry {
/** Gem name */
name: string;
/** Versions (with optional platform and yank flag) */
versions: Array<{
version: string;
platform?: string;
yanked: boolean;
}>;
/** MD5 checksum of info file */
infoChecksum: string;
}
/**
* Compact index info file entry
* Format: VERSION[-PLATFORM] [DEP[,DEP,...]]|REQ[,REQ,...]
*/
export interface ICompactIndexInfoEntry {
/** Version number */
version: string;
/** Platform (optional) */
platform?: string;
/** Dependencies */
dependencies: IRubyGemsDependency[];
/** Requirements */
requirements: IRubyGemsRequirement[];
/** SHA256 checksum */
checksum: string;
}
/**
* Gem upload request
*/
export interface IRubyGemsUploadRequest {
/** Gem file data */
gemData: Buffer;
/** Gem filename */
filename: string;
}
/**
* Gem upload response
*/
export interface IRubyGemsUploadResponse {
/** Success message */
message?: string;
/** Gem name */
name?: string;
/** Version */
version?: string;
}
/**
* Yank request
*/
export interface IRubyGemsYankRequest {
/** Gem name */
gem_name: string;
/** Version to yank */
version: string;
/** Platform (optional) */
platform?: string;
}
/**
* Yank response
*/
export interface IRubyGemsYankResponse {
/** Success indicator */
success: boolean;
/** Message */
message?: string;
}
/**
* Version info response (JSON)
*/
export interface IRubyGemsVersionInfo {
/** Gem name */
name: string;
/** Versions list */
versions: Array<{
/** Version number */
number: string;
/** Platform */
platform?: string;
/** Build date */
built_at?: string;
/** Download count */
downloads_count?: number;
}>;
}
/**
* Dependencies query response
*/
export interface IRubyGemsDependenciesResponse {
/** Dependencies for requested gems */
dependencies: Array<{
/** Gem name */
name: string;
/** Version */
number: string;
/** Platform */
platform?: string;
/** Dependencies */
dependencies: Array<{
name: string;
requirements: string;
}>;
}>;
}
/**
* Error response structure
*/
export interface IRubyGemsError {
/** Error message */
message: string;
/** HTTP status code */
status?: number;
}
/**
* Gem specification (extracted from .gem file)
*/
export interface IRubyGemsSpec {
/** Gem name */
name: string;
/** Version */
version: string;
/** Platform */
platform?: string;
/** Authors */
authors?: string[];
/** Email */
email?: string;
/** Homepage */
homepage?: string;
/** Summary */
summary?: string;
/** Description */
description?: string;
/** License */
license?: string;
/** Dependencies */
dependencies?: IRubyGemsDependency[];
/** Required Ruby version */
required_ruby_version?: string;
/** Required RubyGems version */
required_rubygems_version?: string;
/** Files */
files?: string[];
/** Requirements */
requirements?: string[];
}