feat(registry): add declarative protocol routing and request-scoped storage hook context across registries
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as smartarchive from '@push.rocks/smartarchive';
|
||||
|
||||
/**
|
||||
* Helper to calculate SHA-256 digest in OCI format
|
||||
*/
|
||||
export function calculateDigest(data: Buffer): string {
|
||||
const hash = crypto.createHash('sha256').update(data).digest('hex');
|
||||
return `sha256:${hash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a minimal valid OCI manifest
|
||||
*/
|
||||
export function createTestManifest(configDigest: string, layerDigest: string) {
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
||||
config: {
|
||||
mediaType: 'application/vnd.oci.image.config.v1+json',
|
||||
size: 123,
|
||||
digest: configDigest,
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
mediaType: 'application/vnd.oci.image.layer.v1.tar+gzip',
|
||||
size: 456,
|
||||
digest: layerDigest,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a minimal valid NPM packument
|
||||
*/
|
||||
export function createTestPackument(packageName: string, version: string, tarballData: Buffer) {
|
||||
const shasum = crypto.createHash('sha1').update(tarballData).digest('hex');
|
||||
const integrity = `sha512-${crypto.createHash('sha512').update(tarballData).digest('base64')}`;
|
||||
|
||||
return {
|
||||
name: packageName,
|
||||
versions: {
|
||||
[version]: {
|
||||
name: packageName,
|
||||
version: version,
|
||||
description: 'Test package',
|
||||
main: 'index.js',
|
||||
scripts: {},
|
||||
dist: {
|
||||
shasum: shasum,
|
||||
integrity: integrity,
|
||||
tarball: `http://localhost:5000/npm/${packageName}/-/${packageName}-${version}.tgz`,
|
||||
},
|
||||
},
|
||||
},
|
||||
'dist-tags': {
|
||||
latest: version,
|
||||
},
|
||||
_attachments: {
|
||||
[`${packageName}-${version}.tgz`]: {
|
||||
content_type: 'application/octet-stream',
|
||||
data: tarballData.toString('base64'),
|
||||
length: tarballData.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a minimal valid Maven POM file
|
||||
*/
|
||||
export function createTestPom(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
packaging: string = 'jar'
|
||||
): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>${groupId}</groupId>
|
||||
<artifactId>${artifactId}</artifactId>
|
||||
<version>${version}</version>
|
||||
<packaging>${packaging}</packaging>
|
||||
<name>${artifactId}</name>
|
||||
<description>Test Maven artifact</description>
|
||||
</project>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a test JAR file (minimal ZIP with manifest)
|
||||
*/
|
||||
export function createTestJar(): Buffer {
|
||||
const manifestContent = `Manifest-Version: 1.0
|
||||
Created-By: SmartRegistry Test
|
||||
`;
|
||||
|
||||
return Buffer.from(manifestContent, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to calculate Maven checksums
|
||||
*/
|
||||
export function calculateMavenChecksums(data: Buffer) {
|
||||
return {
|
||||
md5: crypto.createHash('md5').update(data).digest('hex'),
|
||||
sha1: crypto.createHash('sha1').update(data).digest('hex'),
|
||||
sha256: crypto.createHash('sha256').update(data).digest('hex'),
|
||||
sha512: crypto.createHash('sha512').update(data).digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a Composer package ZIP using smartarchive
|
||||
*/
|
||||
export async function createComposerZip(
|
||||
vendorPackage: string,
|
||||
version: string,
|
||||
options?: {
|
||||
description?: string;
|
||||
license?: string[];
|
||||
authors?: Array<{ name: string; email?: string }>;
|
||||
}
|
||||
): Promise<Buffer> {
|
||||
const zipTools = new smartarchive.ZipTools();
|
||||
|
||||
const composerJson = {
|
||||
name: vendorPackage,
|
||||
version: version,
|
||||
type: 'library',
|
||||
description: options?.description || 'Test Composer package',
|
||||
license: options?.license || ['MIT'],
|
||||
authors: options?.authors || [{ name: 'Test Author', email: 'test@example.com' }],
|
||||
require: {
|
||||
php: '>=7.4',
|
||||
},
|
||||
autoload: {
|
||||
'psr-4': {
|
||||
'Vendor\\TestPackage\\': 'src/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [vendor, pkg] = vendorPackage.split('/');
|
||||
const namespace = `${vendor.charAt(0).toUpperCase() + vendor.slice(1)}\\${pkg.charAt(0).toUpperCase() + pkg.slice(1).replace(/-/g, '')}`;
|
||||
const testPhpContent = `<?php
|
||||
namespace ${namespace};
|
||||
|
||||
class TestClass
|
||||
{
|
||||
public function greet(): string
|
||||
{
|
||||
return "Hello from ${vendorPackage}!";
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const entries: smartarchive.IArchiveEntry[] = [
|
||||
{
|
||||
archivePath: 'composer.json',
|
||||
content: Buffer.from(JSON.stringify(composerJson, null, 2), 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: 'src/TestClass.php',
|
||||
content: Buffer.from(testPhpContent, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: 'README.md',
|
||||
content: Buffer.from(`# ${vendorPackage}\n\nTest package`, 'utf-8'),
|
||||
},
|
||||
];
|
||||
|
||||
return Buffer.from(await zipTools.createZip(entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a test Python wheel file (minimal ZIP structure) using smartarchive
|
||||
*/
|
||||
export async function createPythonWheel(
|
||||
packageName: string,
|
||||
version: string,
|
||||
pyVersion: string = 'py3'
|
||||
): Promise<Buffer> {
|
||||
const zipTools = new smartarchive.ZipTools();
|
||||
|
||||
const normalizedName = packageName.replace(/-/g, '_');
|
||||
const distInfoDir = `${normalizedName}-${version}.dist-info`;
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
const wheelContent = `Wheel-Version: 1.0
|
||||
Generator: test 1.0.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: ${pyVersion}-none-any
|
||||
`;
|
||||
|
||||
const moduleContent = `"""${packageName} module"""
|
||||
|
||||
__version__ = "${version}"
|
||||
|
||||
def hello():
|
||||
return "Hello from ${packageName}!"
|
||||
`;
|
||||
|
||||
const entries: smartarchive.IArchiveEntry[] = [
|
||||
{
|
||||
archivePath: `${distInfoDir}/METADATA`,
|
||||
content: Buffer.from(metadata, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${distInfoDir}/WHEEL`,
|
||||
content: Buffer.from(wheelContent, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${distInfoDir}/RECORD`,
|
||||
content: Buffer.from('', 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${distInfoDir}/top_level.txt`,
|
||||
content: Buffer.from(normalizedName, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${normalizedName}/__init__.py`,
|
||||
content: Buffer.from(moduleContent, 'utf-8'),
|
||||
},
|
||||
];
|
||||
|
||||
return Buffer.from(await zipTools.createZip(entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a test Python source distribution (sdist) using smartarchive
|
||||
*/
|
||||
export async function createPythonSdist(
|
||||
packageName: string,
|
||||
version: string
|
||||
): Promise<Buffer> {
|
||||
const tarTools = new smartarchive.TarTools();
|
||||
|
||||
const normalizedName = packageName.replace(/-/g, '_');
|
||||
const dirPrefix = `${packageName}-${version}`;
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
const setupPy = `from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="${packageName}",
|
||||
version="${version}",
|
||||
packages=find_packages(),
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
`;
|
||||
|
||||
const moduleContent = `"""${packageName} module"""
|
||||
|
||||
__version__ = "${version}"
|
||||
|
||||
def hello():
|
||||
return "Hello from ${packageName}!"
|
||||
`;
|
||||
|
||||
const entries: smartarchive.IArchiveEntry[] = [
|
||||
{
|
||||
archivePath: `${dirPrefix}/PKG-INFO`,
|
||||
content: Buffer.from(pkgInfo, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${dirPrefix}/setup.py`,
|
||||
content: Buffer.from(setupPy, 'utf-8'),
|
||||
},
|
||||
{
|
||||
archivePath: `${dirPrefix}/${normalizedName}/__init__.py`,
|
||||
content: Buffer.from(moduleContent, 'utf-8'),
|
||||
},
|
||||
];
|
||||
|
||||
return Buffer.from(await tarTools.packFilesToTarGz(entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) using smartarchive
|
||||
*/
|
||||
export async function createRubyGem(
|
||||
gemName: string,
|
||||
version: string,
|
||||
platform: string = 'ruby'
|
||||
): Promise<Buffer> {
|
||||
const tarTools = new smartarchive.TarTools();
|
||||
const gzipTools = new smartarchive.GzipTools();
|
||||
|
||||
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: []
|
||||
`;
|
||||
|
||||
const metadataGz = Buffer.from(await gzipTools.compress(Buffer.from(metadataYaml, 'utf-8')));
|
||||
|
||||
const libContent = `# ${gemName}
|
||||
|
||||
module ${gemName.charAt(0).toUpperCase() + gemName.slice(1).replace(/-/g, '')}
|
||||
VERSION = "${version}"
|
||||
|
||||
def self.hello
|
||||
"Hello from #{gemName}!"
|
||||
end
|
||||
end
|
||||
`;
|
||||
|
||||
const dataEntries: smartarchive.IArchiveEntry[] = [
|
||||
{
|
||||
archivePath: `lib/${gemName}.rb`,
|
||||
content: Buffer.from(libContent, 'utf-8'),
|
||||
},
|
||||
];
|
||||
|
||||
const dataTarGz = Buffer.from(await tarTools.packFilesToTarGz(dataEntries));
|
||||
|
||||
const gemEntries: smartarchive.IArchiveEntry[] = [
|
||||
{
|
||||
archivePath: 'metadata.gz',
|
||||
content: metadataGz,
|
||||
},
|
||||
{
|
||||
archivePath: 'data.tar.gz',
|
||||
content: dataTarGz,
|
||||
},
|
||||
];
|
||||
|
||||
return Buffer.from(await tarTools.packFiles(gemEntries));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user