feat(maven): Add Maven registry protocol support (storage, auth, routing, interfaces, and exports)

This commit is contained in:
2025-11-21 08:58:29 +00:00
parent 29dea2e0e8
commit 0b31219b7d
16 changed files with 2533 additions and 22 deletions

View File

@@ -0,0 +1,596 @@
/**
* Maven Registry Implementation
* Implements Maven repository protocol for Java artifacts
*/
import { BaseRegistry } from '../core/classes.baseregistry.js';
import type { RegistryStorage } from '../core/classes.registrystorage.js';
import type { AuthManager } from '../core/classes.authmanager.js';
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
import {
pathToGAV,
buildFilename,
calculateChecksums,
generateMetadataXml,
parseMetadataXml,
formatMavenTimestamp,
isSnapshot,
validatePom,
extractGAVFromPom,
gavToPath,
} from './helpers.maven.js';
/**
* Maven Registry class
* Handles Maven repository HTTP protocol
*/
export class MavenRegistry extends BaseRegistry {
private storage: RegistryStorage;
private authManager: AuthManager;
private basePath: string = '/maven';
private registryUrl: string;
constructor(
storage: RegistryStorage,
authManager: AuthManager,
basePath: string,
registryUrl: string
) {
super();
this.storage = storage;
this.authManager = authManager;
this.basePath = basePath;
this.registryUrl = registryUrl;
}
public async init(): Promise<void> {
// No special initialization needed for Maven
}
public getBasePath(): string {
return this.basePath;
}
public async handleRequest(context: IRequestContext): Promise<IResponse> {
// Remove base path from URL
const path = context.path.replace(this.basePath, '');
// Extract token from Authorization header
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
let token: IAuthToken | null = null;
if (authHeader) {
const tokenString = authHeader.replace(/^(Bearer|Basic)\s+/i, '');
// For now, try to validate as Maven token (reuse npm token type)
token = await this.authManager.validateToken(tokenString, 'maven');
}
// Parse path to determine request type
const coordinate = pathToGAV(path);
if (!coordinate) {
// Not a valid artifact path, could be metadata or root
if (path.endsWith('/maven-metadata.xml')) {
return this.handleMetadataRequest(context.method, path, token);
}
return {
status: 404,
headers: { 'Content-Type': 'application/json' },
body: { error: 'NOT_FOUND', message: 'Invalid Maven path' },
};
}
// Check if it's a checksum file
if (coordinate.extension === 'md5' || coordinate.extension === 'sha1' ||
coordinate.extension === 'sha256' || coordinate.extension === 'sha512') {
return this.handleChecksumRequest(context.method, coordinate, token);
}
// Handle artifact requests (JAR, POM, WAR, etc.)
return this.handleArtifactRequest(context.method, coordinate, token, context.body);
}
protected async checkPermission(
token: IAuthToken | null,
resource: string,
action: string
): Promise<boolean> {
if (!token) return false;
return this.authManager.authorize(token, `maven:artifact:${resource}`, action);
}
// ========================================================================
// REQUEST HANDLERS
// ========================================================================
private async handleArtifactRequest(
method: string,
coordinate: IMavenCoordinate,
token: IAuthToken | null,
body?: Buffer | any
): Promise<IResponse> {
const { groupId, artifactId, version } = coordinate;
const filename = buildFilename(coordinate);
const resource = `${groupId}:${artifactId}`;
switch (method) {
case 'GET':
case 'HEAD':
// Read permission required
if (!await this.checkPermission(token, resource, 'read')) {
return {
status: 401,
headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
},
body: { error: 'UNAUTHORIZED', message: 'Authentication required' },
};
}
return method === 'GET'
? this.getArtifact(groupId, artifactId, version, filename)
: this.headArtifact(groupId, artifactId, version, filename);
case 'PUT':
// Write permission required
if (!await this.checkPermission(token, resource, 'write')) {
return {
status: 401,
headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
},
body: { error: 'UNAUTHORIZED', message: 'Write permission required' },
};
}
if (!body) {
return {
status: 400,
headers: {},
body: { error: 'BAD_REQUEST', message: 'Request body required' },
};
}
return this.putArtifact(groupId, artifactId, version, filename, coordinate, body);
case 'DELETE':
// Delete permission required
if (!await this.checkPermission(token, resource, 'delete')) {
return {
status: 401,
headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
},
body: { error: 'UNAUTHORIZED', message: 'Delete permission required' },
};
}
return this.deleteArtifact(groupId, artifactId, version, filename);
default:
return {
status: 405,
headers: { 'Allow': 'GET, HEAD, PUT, DELETE' },
body: { error: 'METHOD_NOT_ALLOWED', message: 'Method not allowed' },
};
}
}
private async handleChecksumRequest(
method: string,
coordinate: IMavenCoordinate,
token: IAuthToken | null
): Promise<IResponse> {
const { groupId, artifactId, version, extension } = coordinate;
const resource = `${groupId}:${artifactId}`;
// Checksums follow the same permissions as their artifacts
if (method === 'GET' || method === 'HEAD') {
if (!await this.checkPermission(token, resource, 'read')) {
return {
status: 401,
headers: {
'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
},
body: { error: 'UNAUTHORIZED', message: 'Authentication required' },
};
}
return this.getChecksum(groupId, artifactId, version, coordinate);
}
return {
status: 405,
headers: { 'Allow': 'GET, HEAD' },
body: { error: 'METHOD_NOT_ALLOWED', message: 'Checksums are auto-generated' },
};
}
private async handleMetadataRequest(
method: string,
path: string,
token: IAuthToken | null
): Promise<IResponse> {
// Parse path to extract groupId and artifactId
// Path format: /com/example/my-lib/maven-metadata.xml
const parts = path.split('/').filter(p => p && p !== 'maven-metadata.xml');
if (parts.length < 2) {
return {
status: 400,
headers: {},
body: { error: 'BAD_REQUEST', message: 'Invalid metadata path' },
};
}
const artifactId = parts[parts.length - 1];
const groupId = parts.slice(0, -1).join('.');
const resource = `${groupId}:${artifactId}`;
if (method === 'GET') {
// Metadata is usually public (read permission optional)
// Some registries allow anonymous metadata access
return this.getMetadata(groupId, artifactId);
}
return {
status: 405,
headers: { 'Allow': 'GET' },
body: { error: 'METHOD_NOT_ALLOWED', message: 'Metadata is auto-generated' },
};
}
// ========================================================================
// ARTIFACT OPERATIONS
// ========================================================================
private async getArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<IResponse> {
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
if (!data) {
return {
status: 404,
headers: {},
body: { error: 'NOT_FOUND', message: 'Artifact not found' },
};
}
// Determine content type based on extension
const extension = filename.split('.').pop() || '';
const contentType = this.getContentType(extension);
return {
status: 200,
headers: {
'Content-Type': contentType,
'Content-Length': data.length.toString(),
},
body: data,
};
}
private async headArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<IResponse> {
const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
if (!exists) {
return {
status: 404,
headers: {},
body: null,
};
}
// Get file size for Content-Length header
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
const extension = filename.split('.').pop() || '';
const contentType = this.getContentType(extension);
return {
status: 200,
headers: {
'Content-Type': contentType,
'Content-Length': data ? data.length.toString() : '0',
},
body: null,
};
}
private async putArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string,
coordinate: IMavenCoordinate,
body: Buffer | any
): Promise<IResponse> {
const data = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
// Validate POM if uploading .pom file
if (coordinate.extension === 'pom') {
const pomValid = validatePom(data.toString('utf-8'));
if (!pomValid) {
return {
status: 400,
headers: {},
body: { error: 'INVALID_POM', message: 'Invalid POM file' },
};
}
// Verify GAV matches path
const pomGAV = extractGAVFromPom(data.toString('utf-8'));
if (pomGAV && (pomGAV.groupId !== groupId || pomGAV.artifactId !== artifactId || pomGAV.version !== version)) {
return {
status: 400,
headers: {},
body: { error: 'GAV_MISMATCH', message: 'POM coordinates do not match upload path' },
};
}
}
// Store the artifact
await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
// Generate and store checksums
const checksums = await calculateChecksums(data);
await this.storeChecksums(groupId, artifactId, version, filename, checksums);
// Update maven-metadata.xml if this is a primary artifact (jar, pom, war)
if (['jar', 'pom', 'war', 'ear', 'aar'].includes(coordinate.extension)) {
await this.updateMetadata(groupId, artifactId, version);
}
return {
status: 201,
headers: {
'Location': `${this.registryUrl}/${gavToPath(groupId, artifactId, version)}/${filename}`,
},
body: { success: true, message: 'Artifact uploaded successfully' },
};
}
private async deleteArtifact(
groupId: string,
artifactId: string,
version: string,
filename: string
): Promise<IResponse> {
const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
if (!exists) {
return {
status: 404,
headers: {},
body: { error: 'NOT_FOUND', message: 'Artifact not found' },
};
}
await this.storage.deleteMavenArtifact(groupId, artifactId, version, filename);
// Also delete checksums
for (const ext of ['md5', 'sha1', 'sha256', 'sha512']) {
const checksumFile = `${filename}.${ext}`;
const checksumExists = await this.storage.mavenArtifactExists(groupId, artifactId, version, checksumFile);
if (checksumExists) {
await this.storage.deleteMavenArtifact(groupId, artifactId, version, checksumFile);
}
}
return {
status: 204,
headers: {},
body: null,
};
}
// ========================================================================
// CHECKSUM OPERATIONS
// ========================================================================
private async getChecksum(
groupId: string,
artifactId: string,
version: string,
coordinate: IMavenCoordinate
): Promise<IResponse> {
const checksumFilename = buildFilename(coordinate);
const data = await this.storage.getMavenArtifact(groupId, artifactId, version, checksumFilename);
if (!data) {
return {
status: 404,
headers: {},
body: { error: 'NOT_FOUND', message: 'Checksum not found' },
};
}
return {
status: 200,
headers: {
'Content-Type': 'text/plain',
'Content-Length': data.length.toString(),
},
body: data,
};
}
private async storeChecksums(
groupId: string,
artifactId: string,
version: string,
filename: string,
checksums: IChecksums
): Promise<void> {
// Store each checksum as a separate file
await this.storage.putMavenArtifact(
groupId,
artifactId,
version,
`${filename}.md5`,
Buffer.from(checksums.md5, 'utf-8')
);
await this.storage.putMavenArtifact(
groupId,
artifactId,
version,
`${filename}.sha1`,
Buffer.from(checksums.sha1, 'utf-8')
);
if (checksums.sha256) {
await this.storage.putMavenArtifact(
groupId,
artifactId,
version,
`${filename}.sha256`,
Buffer.from(checksums.sha256, 'utf-8')
);
}
if (checksums.sha512) {
await this.storage.putMavenArtifact(
groupId,
artifactId,
version,
`${filename}.sha512`,
Buffer.from(checksums.sha512, 'utf-8')
);
}
}
// ========================================================================
// METADATA OPERATIONS
// ========================================================================
private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
const metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
if (!metadataBuffer) {
// Generate empty metadata if none exists
const emptyMetadata: IMavenMetadata = {
groupId,
artifactId,
versioning: {
versions: [],
lastUpdated: formatMavenTimestamp(new Date()),
},
};
const xml = generateMetadataXml(emptyMetadata);
return {
status: 200,
headers: {
'Content-Type': 'application/xml',
'Content-Length': xml.length.toString(),
},
body: Buffer.from(xml, 'utf-8'),
};
}
return {
status: 200,
headers: {
'Content-Type': 'application/xml',
'Content-Length': metadataBuffer.length.toString(),
},
body: metadataBuffer,
};
}
private async updateMetadata(
groupId: string,
artifactId: string,
newVersion: string
): Promise<void> {
// Get existing metadata or create new
const existingBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
let metadata: IMavenMetadata;
if (existingBuffer) {
const parsed = parseMetadataXml(existingBuffer.toString('utf-8'));
if (parsed) {
metadata = parsed;
} else {
// Create new if parsing failed
metadata = {
groupId,
artifactId,
versioning: {
versions: [],
lastUpdated: formatMavenTimestamp(new Date()),
},
};
}
} else {
metadata = {
groupId,
artifactId,
versioning: {
versions: [],
lastUpdated: formatMavenTimestamp(new Date()),
},
};
}
// Add new version if not already present
if (!metadata.versioning.versions.includes(newVersion)) {
metadata.versioning.versions.push(newVersion);
metadata.versioning.versions.sort(); // Sort versions
}
// Update latest and release
const versions = metadata.versioning.versions;
metadata.versioning.latest = versions[versions.length - 1];
// Release is the latest non-SNAPSHOT version
const releaseVersions = versions.filter(v => !isSnapshot(v));
if (releaseVersions.length > 0) {
metadata.versioning.release = releaseVersions[releaseVersions.length - 1];
}
// Update timestamp
metadata.versioning.lastUpdated = formatMavenTimestamp(new Date());
// Generate and store XML
const xml = generateMetadataXml(metadata);
await this.storage.putMavenMetadata(groupId, artifactId, Buffer.from(xml, 'utf-8'));
// Also store checksums for metadata
const checksums = await calculateChecksums(Buffer.from(xml, 'utf-8'));
const metadataFilename = 'maven-metadata.xml';
await this.storeChecksums(groupId, artifactId, '', metadataFilename, checksums);
}
// ========================================================================
// UTILITY METHODS
// ========================================================================
private getContentType(extension: string): string {
const contentTypes: Record<string, string> = {
'jar': 'application/java-archive',
'war': 'application/java-archive',
'ear': 'application/java-archive',
'aar': 'application/java-archive',
'pom': 'application/xml',
'xml': 'application/xml',
'md5': 'text/plain',
'sha1': 'text/plain',
'sha256': 'text/plain',
'sha512': 'text/plain',
};
return contentTypes[extension] || 'application/octet-stream';
}
}

333
ts/maven/helpers.maven.ts Normal file
View File

@@ -0,0 +1,333 @@
/**
* Maven helper utilities
* Path conversion, XML generation, checksum calculation
*/
import * as plugins from '../plugins.js';
import type {
IMavenCoordinate,
IMavenMetadata,
IChecksums,
IMavenPom,
} from './interfaces.maven.js';
/**
* Convert Maven GAV coordinates to storage path
* Example: com.example:my-lib:1.0.0 → com/example/my-lib/1.0.0
*/
export function gavToPath(
groupId: string,
artifactId: string,
version?: string
): string {
const groupPath = groupId.replace(/\./g, '/');
if (version) {
return `${groupPath}/${artifactId}/${version}`;
}
return `${groupPath}/${artifactId}`;
}
/**
* Parse Maven path to GAV coordinates
* Example: com/example/my-lib/1.0.0/my-lib-1.0.0.jar → {groupId, artifactId, version, ...}
*/
export function pathToGAV(path: string): IMavenCoordinate | null {
// Remove leading slash if present
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
// Split path into parts
const parts = cleanPath.split('/');
if (parts.length < 4) {
return null; // Not a valid artifact path
}
// Last part is filename
const filename = parts[parts.length - 1];
const version = parts[parts.length - 2];
const artifactId = parts[parts.length - 3];
const groupId = parts.slice(0, -3).join('.');
// Parse filename to extract classifier and extension
const parsed = parseFilename(filename, artifactId, version);
if (!parsed) {
return null;
}
return {
groupId,
artifactId,
version,
classifier: parsed.classifier,
extension: parsed.extension,
};
}
/**
* Parse Maven artifact filename
* Example: my-lib-1.0.0-sources.jar → {classifier: 'sources', extension: 'jar'}
*/
export function parseFilename(
filename: string,
artifactId: string,
version: string
): { classifier?: string; extension: string } | null {
// Expected format: {artifactId}-{version}[-{classifier}].{extension}
const prefix = `${artifactId}-${version}`;
if (!filename.startsWith(prefix)) {
return null;
}
const remainder = filename.substring(prefix.length);
// Check for classifier
const dotIndex = remainder.lastIndexOf('.');
if (dotIndex === -1) {
return null; // No extension
}
const extension = remainder.substring(dotIndex + 1);
const classifierPart = remainder.substring(0, dotIndex);
if (classifierPart.length === 0) {
// No classifier
return { extension };
}
if (classifierPart.startsWith('-')) {
// Has classifier
const classifier = classifierPart.substring(1);
return { classifier, extension };
}
return null; // Invalid format
}
/**
* Build Maven artifact filename
* Example: {artifactId: 'my-lib', version: '1.0.0', classifier: 'sources', extension: 'jar'}
* → 'my-lib-1.0.0-sources.jar'
*/
export function buildFilename(coordinate: IMavenCoordinate): string {
const { artifactId, version, classifier, extension } = coordinate;
let filename = `${artifactId}-${version}`;
if (classifier) {
filename += `-${classifier}`;
}
filename += `.${extension}`;
return filename;
}
/**
* Calculate checksums for Maven artifact
* Returns MD5, SHA-1, SHA-256, SHA-512
*/
export async function calculateChecksums(data: Buffer): Promise<IChecksums> {
const crypto = await import('crypto');
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'),
};
}
/**
* Generate maven-metadata.xml from metadata object
*/
export function generateMetadataXml(metadata: IMavenMetadata): string {
const { groupId, artifactId, versioning } = metadata;
const { latest, release, versions, lastUpdated, snapshot, snapshotVersions } = versioning;
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<metadata>\n';
xml += ` <groupId>${escapeXml(groupId)}</groupId>\n`;
xml += ` <artifactId>${escapeXml(artifactId)}</artifactId>\n`;
// Add version if SNAPSHOT
if (snapshot) {
const snapshotVersion = versions[versions.length - 1]; // Assume last version is the SNAPSHOT
xml += ` <version>${escapeXml(snapshotVersion)}</version>\n`;
}
xml += ' <versioning>\n';
if (latest) {
xml += ` <latest>${escapeXml(latest)}</latest>\n`;
}
if (release) {
xml += ` <release>${escapeXml(release)}</release>\n`;
}
xml += ' <versions>\n';
for (const version of versions) {
xml += ` <version>${escapeXml(version)}</version>\n`;
}
xml += ' </versions>\n';
xml += ` <lastUpdated>${lastUpdated}</lastUpdated>\n`;
// Add SNAPSHOT info if present
if (snapshot) {
xml += ' <snapshot>\n';
xml += ` <timestamp>${escapeXml(snapshot.timestamp)}</timestamp>\n`;
xml += ` <buildNumber>${snapshot.buildNumber}</buildNumber>\n`;
xml += ' </snapshot>\n';
}
// Add SNAPSHOT versions if present
if (snapshotVersions && snapshotVersions.length > 0) {
xml += ' <snapshotVersions>\n';
for (const sv of snapshotVersions) {
xml += ' <snapshotVersion>\n';
if (sv.classifier) {
xml += ` <classifier>${escapeXml(sv.classifier)}</classifier>\n`;
}
xml += ` <extension>${escapeXml(sv.extension)}</extension>\n`;
xml += ` <value>${escapeXml(sv.value)}</value>\n`;
xml += ` <updated>${sv.updated}</updated>\n`;
xml += ' </snapshotVersion>\n';
}
xml += ' </snapshotVersions>\n';
}
xml += ' </versioning>\n';
xml += '</metadata>\n';
return xml;
}
/**
* Parse maven-metadata.xml to metadata object
* Basic XML parsing for Maven metadata
*/
export function parseMetadataXml(xml: string): IMavenMetadata | null {
try {
// Simple regex-based parsing (for basic metadata)
// In production, use a proper XML parser
const groupIdMatch = xml.match(/<groupId>([^<]+)<\/groupId>/);
const artifactIdMatch = xml.match(/<artifactId>([^<]+)<\/artifactId>/);
const latestMatch = xml.match(/<latest>([^<]+)<\/latest>/);
const releaseMatch = xml.match(/<release>([^<]+)<\/release>/);
const lastUpdatedMatch = xml.match(/<lastUpdated>([^<]+)<\/lastUpdated>/);
if (!groupIdMatch || !artifactIdMatch) {
return null;
}
// Parse versions
const versionsMatch = xml.match(/<versions>([\s\S]*?)<\/versions>/);
const versions: string[] = [];
if (versionsMatch) {
const versionMatches = versionsMatch[1].matchAll(/<version>([^<]+)<\/version>/g);
for (const match of versionMatches) {
versions.push(match[1]);
}
}
return {
groupId: groupIdMatch[1],
artifactId: artifactIdMatch[1],
versioning: {
latest: latestMatch ? latestMatch[1] : undefined,
release: releaseMatch ? releaseMatch[1] : undefined,
versions,
lastUpdated: lastUpdatedMatch ? lastUpdatedMatch[1] : formatMavenTimestamp(new Date()),
},
};
} catch (error) {
return null;
}
}
/**
* Escape XML special characters
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Format timestamp in Maven format: yyyyMMddHHmmss
*/
export function formatMavenTimestamp(date: Date): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
/**
* Format SNAPSHOT timestamp: yyyyMMdd.HHmmss
*/
export function formatSnapshotTimestamp(date: Date): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
return `${year}${month}${day}.${hours}${minutes}${seconds}`;
}
/**
* Check if version is a SNAPSHOT
*/
export function isSnapshot(version: string): boolean {
return version.endsWith('-SNAPSHOT');
}
/**
* Validate POM basic structure
*/
export function validatePom(pomXml: string): boolean {
try {
// Basic validation - check for required fields
return (
pomXml.includes('<groupId>') &&
pomXml.includes('<artifactId>') &&
pomXml.includes('<version>') &&
pomXml.includes('<modelVersion>')
);
} catch (error) {
return false;
}
}
/**
* Extract GAV from POM XML
*/
export function extractGAVFromPom(pomXml: string): { groupId: string; artifactId: string; version: string } | null {
try {
const groupIdMatch = pomXml.match(/<groupId>([^<]+)<\/groupId>/);
const artifactIdMatch = pomXml.match(/<artifactId>([^<]+)<\/artifactId>/);
const versionMatch = pomXml.match(/<version>([^<]+)<\/version>/);
if (groupIdMatch && artifactIdMatch && versionMatch) {
return {
groupId: groupIdMatch[1],
artifactId: artifactIdMatch[1],
version: versionMatch[1],
};
}
return null;
} catch (error) {
return null;
}
}

7
ts/maven/index.ts Normal file
View File

@@ -0,0 +1,7 @@
/**
* Maven Registry module exports
*/
export { MavenRegistry } from './classes.mavenregistry.js';
export * from './interfaces.maven.js';
export * from './helpers.maven.js';

View File

@@ -0,0 +1,127 @@
/**
* Maven registry type definitions
* Supports Maven repository protocol for Java artifacts
*/
/**
* Maven coordinate system (GAV + optional classifier)
* Example: com.example:my-library:1.0.0:sources:jar
*/
export interface IMavenCoordinate {
groupId: string; // e.g., "com.example.myapp"
artifactId: string; // e.g., "my-library"
version: string; // e.g., "1.0.0" or "1.0-SNAPSHOT"
classifier?: string; // e.g., "sources", "javadoc"
extension: string; // e.g., "jar", "war", "pom"
}
/**
* Maven metadata (maven-metadata.xml) structure
* Contains version list and latest/release information
*/
export interface IMavenMetadata {
groupId: string;
artifactId: string;
versioning: IMavenVersioning;
}
/**
* Maven versioning information
*/
export interface IMavenVersioning {
latest?: string; // Latest version (including SNAPSHOTs)
release?: string; // Latest release version (excluding SNAPSHOTs)
versions: string[]; // List of all versions
lastUpdated: string; // Format: yyyyMMddHHmmss
snapshot?: IMavenSnapshot; // For SNAPSHOT versions
snapshotVersions?: IMavenSnapshotVersion[]; // For SNAPSHOT builds
}
/**
* SNAPSHOT build information
*/
export interface IMavenSnapshot {
timestamp: string; // Format: yyyyMMdd.HHmmss
buildNumber: number; // Incremental build number
}
/**
* SNAPSHOT version entry
*/
export interface IMavenSnapshotVersion {
classifier?: string;
extension: string;
value: string; // Timestamped version
updated: string; // Format: yyyyMMddHHmmss
}
/**
* Checksums for Maven artifacts
* Maven requires separate checksum files for each artifact
*/
export interface IChecksums {
md5: string; // MD5 hash
sha1: string; // SHA-1 hash (required)
sha256?: string; // SHA-256 hash (optional)
sha512?: string; // SHA-512 hash (optional)
}
/**
* Maven artifact file information
*/
export interface IMavenArtifactFile {
filename: string; // Full filename with extension
data: Buffer; // File content
coordinate: IMavenCoordinate; // Parsed GAV coordinates
checksums?: IChecksums; // Calculated checksums
}
/**
* Maven upload request
* Contains all files for a single version (JAR, POM, sources, etc.)
*/
export interface IMavenUploadRequest {
groupId: string;
artifactId: string;
version: string;
files: IMavenArtifactFile[];
}
/**
* Maven protocol configuration
*/
export interface IMavenProtocolConfig {
enabled: boolean;
basePath: string; // Default: '/maven'
features?: {
snapshots?: boolean; // Support SNAPSHOT versions (default: true)
checksums?: boolean; // Auto-generate checksums (default: true)
metadata?: boolean; // Auto-generate maven-metadata.xml (default: true)
allowedExtensions?: string[]; // Allowed file extensions (default: jar, war, pom, etc.)
};
}
/**
* Maven POM (Project Object Model) minimal structure
* Only essential fields for validation
*/
export interface IMavenPom {
modelVersion: string; // Always "4.0.0"
groupId: string;
artifactId: string;
version: string;
packaging?: string; // jar, war, pom, etc.
name?: string;
description?: string;
}
/**
* Maven repository search result
*/
export interface IMavenSearchResult {
groupId: string;
artifactId: string;
latestVersion: string;
versions: string[];
lastUpdated: string;
}