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

@@ -6,7 +6,7 @@ import type { IRegistryConfig } from '../../ts/core/interfaces.core.js';
const testQenv = new qenv.Qenv('./', './.nogit');
/**
* Create a test SmartRegistry instance with OCI, NPM, Maven, and Composer enabled
* Create a test SmartRegistry instance with all protocols enabled
*/
export async function createTestRegistry(): Promise<SmartRegistry> {
// Read S3 config from env.json
@@ -36,6 +36,12 @@ export async function createTestRegistry(): Promise<SmartRegistry> {
realm: 'https://auth.example.com/token',
service: 'test-registry',
},
pypiTokens: {
enabled: true,
},
rubygemsTokens: {
enabled: true,
},
},
oci: {
enabled: true,
@@ -57,6 +63,14 @@ export async function createTestRegistry(): Promise<SmartRegistry> {
enabled: true,
basePath: '/cargo',
},
pypi: {
enabled: true,
basePath: '/pypi',
},
rubygems: {
enabled: true,
basePath: '/rubygems',
},
};
const registry = new SmartRegistry(config);
@@ -100,7 +114,13 @@ export async function createTestTokens(registry: SmartRegistry) {
// Create Cargo token with full access
const cargoToken = await authManager.createCargoToken(userId, false);
return { npmToken, ociToken, mavenToken, composerToken, cargoToken, userId };
// Create PyPI token with full access
const pypiToken = await authManager.createPypiToken(userId, false);
// Create RubyGems token with full access
const rubygemsToken = await authManager.createRubyGemsToken(userId, false);
return { npmToken, ociToken, mavenToken, composerToken, cargoToken, pypiToken, rubygemsToken, userId };
}
/**
@@ -277,3 +297,257 @@ class TestClass
return zip.toBuffer();
}
/**
* Helper to create a test Python wheel file (minimal ZIP structure)
*/
export async function createPythonWheel(
packageName: string,
version: string,
pyVersion: string = 'py3'
): Promise<Buffer> {
const AdmZip = (await import('adm-zip')).default;
const zip = new AdmZip();
const normalizedName = packageName.replace(/-/g, '_');
const distInfoDir = `${normalizedName}-${version}.dist-info`;
// Create METADATA file
const metadata = `Metadata-Version: 2.1
Name: ${packageName}
Version: ${version}
Summary: Test Python package
Home-page: https://example.com
Author: Test Author
Author-email: test@example.com
License: MIT
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.7
Description-Content-Type: text/markdown
# ${packageName}
Test package for SmartRegistry
`;
zip.addFile(`${distInfoDir}/METADATA`, Buffer.from(metadata, 'utf-8'));
// Create WHEEL file
const wheelContent = `Wheel-Version: 1.0
Generator: test 1.0.0
Root-Is-Purelib: true
Tag: ${pyVersion}-none-any
`;
zip.addFile(`${distInfoDir}/WHEEL`, Buffer.from(wheelContent, 'utf-8'));
// Create RECORD file (empty for test)
zip.addFile(`${distInfoDir}/RECORD`, Buffer.from('', 'utf-8'));
// Create top_level.txt
zip.addFile(`${distInfoDir}/top_level.txt`, Buffer.from(normalizedName, 'utf-8'));
// Create a simple Python module
const moduleContent = `"""${packageName} module"""
__version__ = "${version}"
def hello():
return "Hello from ${packageName}!"
`;
zip.addFile(`${normalizedName}/__init__.py`, Buffer.from(moduleContent, 'utf-8'));
return zip.toBuffer();
}
/**
* Helper to create a test Python source distribution (sdist)
*/
export async function createPythonSdist(
packageName: string,
version: string
): Promise<Buffer> {
const tar = await import('tar-stream');
const zlib = await import('zlib');
const { Readable } = await import('stream');
const normalizedName = packageName.replace(/-/g, '_');
const dirPrefix = `${packageName}-${version}`;
const pack = tar.pack();
// PKG-INFO
const pkgInfo = `Metadata-Version: 2.1
Name: ${packageName}
Version: ${version}
Summary: Test Python package
Home-page: https://example.com
Author: Test Author
Author-email: test@example.com
License: MIT
`;
pack.entry({ name: `${dirPrefix}/PKG-INFO` }, pkgInfo);
// setup.py
const setupPy = `from setuptools import setup, find_packages
setup(
name="${packageName}",
version="${version}",
packages=find_packages(),
python_requires=">=3.7",
)
`;
pack.entry({ name: `${dirPrefix}/setup.py` }, setupPy);
// Module file
const moduleContent = `"""${packageName} module"""
__version__ = "${version}"
def hello():
return "Hello from ${packageName}!"
`;
pack.entry({ name: `${dirPrefix}/${normalizedName}/__init__.py` }, moduleContent);
pack.finalize();
// Convert to gzipped tar
const chunks: Buffer[] = [];
const gzip = zlib.createGzip();
return new Promise((resolve, reject) => {
pack.pipe(gzip);
gzip.on('data', (chunk) => chunks.push(chunk));
gzip.on('end', () => resolve(Buffer.concat(chunks)));
gzip.on('error', reject);
});
}
/**
* Helper to calculate PyPI file hashes
*/
export function calculatePypiHashes(data: Buffer) {
return {
md5: crypto.createHash('md5').update(data).digest('hex'),
sha256: crypto.createHash('sha256').update(data).digest('hex'),
blake2b: crypto.createHash('blake2b512').update(data).digest('hex'),
};
}
/**
* Helper to create a test RubyGem file (minimal tar.gz structure)
*/
export async function createRubyGem(
gemName: string,
version: string,
platform: string = 'ruby'
): Promise<Buffer> {
const tar = await import('tar-stream');
const zlib = await import('zlib');
const pack = tar.pack();
// Create metadata.gz (simplified)
const metadataYaml = `--- !ruby/object:Gem::Specification
name: ${gemName}
version: !ruby/object:Gem::Version
version: ${version}
platform: ${platform}
authors:
- Test Author
autorequire:
bindir: bin
cert_chain: []
date: ${new Date().toISOString().split('T')[0]}
dependencies: []
description: Test RubyGem
email: test@example.com
executables: []
extensions: []
extra_rdoc_files: []
files:
- lib/${gemName}.rb
homepage: https://example.com
licenses:
- MIT
metadata: {}
post_install_message:
rdoc_options: []
require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: '2.7'
required_rubygems_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: '0'
requirements: []
rubygems_version: 3.0.0
signing_key:
specification_version: 4
summary: Test gem for SmartRegistry
test_files: []
`;
pack.entry({ name: 'metadata.gz' }, zlib.gzipSync(Buffer.from(metadataYaml, 'utf-8')));
// Create data.tar.gz (simplified)
const dataPack = tar.pack();
const libContent = `# ${gemName}
module ${gemName.charAt(0).toUpperCase() + gemName.slice(1).replace(/-/g, '')}
VERSION = "${version}"
def self.hello
"Hello from #{gemName}!"
end
end
`;
dataPack.entry({ name: `lib/${gemName}.rb` }, libContent);
dataPack.finalize();
const dataChunks: Buffer[] = [];
const dataGzip = zlib.createGzip();
dataPack.pipe(dataGzip);
await new Promise((resolve) => {
dataGzip.on('data', (chunk) => dataChunks.push(chunk));
dataGzip.on('end', resolve);
});
pack.entry({ name: 'data.tar.gz' }, Buffer.concat(dataChunks));
pack.finalize();
// Convert to gzipped tar
const chunks: Buffer[] = [];
const gzip = zlib.createGzip();
return new Promise((resolve, reject) => {
pack.pipe(gzip);
gzip.on('data', (chunk) => chunks.push(chunk));
gzip.on('end', () => resolve(Buffer.concat(chunks)));
gzip.on('error', reject);
});
}
/**
* Helper to calculate RubyGems checksums
*/
export function calculateRubyGemsChecksums(data: Buffer) {
return {
md5: crypto.createHash('md5').update(data).digest('hex'),
sha256: crypto.createHash('sha256').update(data).digest('hex'),
};
}