fix(npm): Replace console logging with structured Smartlog in NPM registry and silence RubyGems helper error logging

This commit is contained in:
2025-11-25 23:25:26 +00:00
parent 35ff286169
commit da1cf8ddeb
5 changed files with 115 additions and 167 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2025-11-25 - 2.2.2 - fix(npm)
Replace console logging with structured Smartlog in NPM registry and silence RubyGems helper error logging
- Replaced console.log calls with this.logger.log (Smartlog) in ts/npm/classes.npmregistry.ts for debug/info/success events
- Converted console.error in NpmRegistry.handleSearch to structured logger.log('error', ...) including the error message
- Removed console.error from ts/rubygems/helpers.rubygems.ts; gem metadata extraction failures are now handled silently by returning null
## 2025-11-25 - 2.2.1 - fix(core) ## 2025-11-25 - 2.2.1 - fix(core)
Normalize binary data handling across registries and add buffer helpers Normalize binary data handling across registries and add buffer helpers

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartregistry', name: '@push.rocks/smartregistry',
version: '2.2.1', version: '2.2.2',
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries' description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
} }

View File

@@ -52,6 +52,60 @@ export class AuthManager {
return token; return token;
} }
/**
* Generic protocol token creation (internal helper)
* @param userId - User ID
* @param protocol - Protocol type (npm, maven, composer, etc.)
* @param readonly - Whether the token is readonly
* @returns UUID token string
*/
private async createProtocolToken(
userId: string,
protocol: TRegistryProtocol,
readonly: boolean
): Promise<string> {
const scopes = readonly
? [`${protocol}:*:*:read`]
: [`${protocol}:*:*:*`];
return this.createUuidToken(userId, protocol, scopes, readonly);
}
/**
* Generic protocol token validation (internal helper)
* @param token - UUID token string
* @param protocol - Expected protocol type
* @returns Auth token object or null
*/
private async validateProtocolToken(
token: string,
protocol: TRegistryProtocol
): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) {
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== protocol) {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
}
/**
* Generic protocol token revocation (internal helper)
* @param token - UUID token string
*/
private async revokeProtocolToken(token: string): Promise<void> {
this.tokenStore.delete(token);
}
// ======================================================================== // ========================================================================
// NPM AUTHENTICATION // NPM AUTHENTICATION
// ======================================================================== // ========================================================================
@@ -66,9 +120,7 @@ export class AuthManager {
if (!this.config.npmTokens.enabled) { if (!this.config.npmTokens.enabled) {
throw new Error('NPM tokens are not enabled'); throw new Error('NPM tokens are not enabled');
} }
return this.createProtocolToken(userId, 'npm', readonly);
const scopes = readonly ? ['npm:*:*:read'] : ['npm:*:*:*'];
return this.createUuidToken(userId, 'npm', scopes, readonly);
} }
/** /**
@@ -77,22 +129,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateNpmToken(token: string): Promise<IAuthToken | null> { public async validateNpmToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'npm');
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'npm') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
} }
/** /**
@@ -100,7 +137,7 @@ export class AuthManager {
* @param token - NPM UUID token * @param token - NPM UUID token
*/ */
public async revokeNpmToken(token: string): Promise<void> { public async revokeNpmToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
/** /**
@@ -265,8 +302,7 @@ export class AuthManager {
* @returns Maven UUID token * @returns Maven UUID token
*/ */
public async createMavenToken(userId: string, readonly: boolean = false): Promise<string> { public async createMavenToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['maven:*:*:read'] : ['maven:*:*:*']; return this.createProtocolToken(userId, 'maven', readonly);
return this.createUuidToken(userId, 'maven', scopes, readonly);
} }
/** /**
@@ -275,22 +311,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateMavenToken(token: string): Promise<IAuthToken | null> { public async validateMavenToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'maven');
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'maven') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
} }
/** /**
@@ -298,7 +319,7 @@ export class AuthManager {
* @param token - Maven UUID token * @param token - Maven UUID token
*/ */
public async revokeMavenToken(token: string): Promise<void> { public async revokeMavenToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
// ======================================================================== // ========================================================================
@@ -312,8 +333,7 @@ export class AuthManager {
* @returns Composer UUID token * @returns Composer UUID token
*/ */
public async createComposerToken(userId: string, readonly: boolean = false): Promise<string> { public async createComposerToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['composer:*:*:read'] : ['composer:*:*:*']; return this.createProtocolToken(userId, 'composer', readonly);
return this.createUuidToken(userId, 'composer', scopes, readonly);
} }
/** /**
@@ -322,22 +342,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateComposerToken(token: string): Promise<IAuthToken | null> { public async validateComposerToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'composer');
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;
} }
/** /**
@@ -345,7 +350,7 @@ export class AuthManager {
* @param token - Composer UUID token * @param token - Composer UUID token
*/ */
public async revokeComposerToken(token: string): Promise<void> { public async revokeComposerToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
// ======================================================================== // ========================================================================
@@ -359,8 +364,7 @@ export class AuthManager {
* @returns Cargo UUID token * @returns Cargo UUID token
*/ */
public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> { public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['cargo:*:*:read'] : ['cargo:*:*:*']; return this.createProtocolToken(userId, 'cargo', readonly);
return this.createUuidToken(userId, 'cargo', scopes, readonly);
} }
/** /**
@@ -369,22 +373,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateCargoToken(token: string): Promise<IAuthToken | null> { public async validateCargoToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'cargo');
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'cargo') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
} }
/** /**
@@ -392,7 +381,7 @@ export class AuthManager {
* @param token - Cargo UUID token * @param token - Cargo UUID token
*/ */
public async revokeCargoToken(token: string): Promise<void> { public async revokeCargoToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
// ======================================================================== // ========================================================================
@@ -406,8 +395,7 @@ export class AuthManager {
* @returns PyPI UUID token * @returns PyPI UUID token
*/ */
public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> { public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['pypi:*:*:read'] : ['pypi:*:*:*']; return this.createProtocolToken(userId, 'pypi', readonly);
return this.createUuidToken(userId, 'pypi', scopes, readonly);
} }
/** /**
@@ -416,22 +404,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validatePypiToken(token: string): Promise<IAuthToken | null> { public async validatePypiToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'pypi');
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'pypi') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
} }
/** /**
@@ -439,7 +412,7 @@ export class AuthManager {
* @param token - PyPI UUID token * @param token - PyPI UUID token
*/ */
public async revokePypiToken(token: string): Promise<void> { public async revokePypiToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
// ======================================================================== // ========================================================================
@@ -453,8 +426,7 @@ export class AuthManager {
* @returns RubyGems UUID token * @returns RubyGems UUID token
*/ */
public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> { public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> {
const scopes = readonly ? ['rubygems:*:*:read'] : ['rubygems:*:*:*']; return this.createProtocolToken(userId, 'rubygems', readonly);
return this.createUuidToken(userId, 'rubygems', scopes, readonly);
} }
/** /**
@@ -463,22 +435,7 @@ export class AuthManager {
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> { public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> {
if (!this.isValidUuid(token)) { return this.validateProtocolToken(token, 'rubygems');
return null;
}
const authToken = this.tokenStore.get(token);
if (!authToken || authToken.type !== 'rubygems') {
return null;
}
// Check expiration if set
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(token);
return null;
}
return authToken;
} }
/** /**
@@ -486,7 +443,7 @@ export class AuthManager {
* @param token - RubyGems UUID token * @param token - RubyGems UUID token
*/ */
public async revokeRubyGemsToken(token: string): Promise<void> { public async revokeRubyGemsToken(token: string): Promise<void> {
this.tokenStore.delete(token); return this.revokeProtocolToken(token);
} }
// ======================================================================== // ========================================================================
@@ -495,57 +452,42 @@ export class AuthManager {
/** /**
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo) * Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
* Optimized: O(1) lookup when protocol hint provided
* @param tokenString - Token string (UUID or JWT) * @param tokenString - Token string (UUID or JWT)
* @param protocol - Expected protocol type * @param protocol - Expected protocol type (optional, improves performance)
* @returns Auth token object or null * @returns Auth token object or null
*/ */
public async validateToken( public async validateToken(
tokenString: string, tokenString: string,
protocol?: TRegistryProtocol protocol?: TRegistryProtocol
): Promise<IAuthToken | null> { ): Promise<IAuthToken | null> {
// Try UUID-based tokens (NPM, Maven, Composer, Cargo, PyPI, RubyGems) // OCI uses JWT (contains dots), not UUID - check first if OCI is expected
if (this.isValidUuid(tokenString)) { if (protocol === 'oci' || tokenString.includes('.')) {
// Try NPM token const ociToken = await this.validateOciToken(tokenString);
const npmToken = await this.validateNpmToken(tokenString); if (ociToken && (!protocol || protocol === 'oci')) {
if (npmToken && (!protocol || protocol === 'npm')) { return ociToken;
return npmToken;
} }
// If protocol was explicitly OCI but validation failed, return null
// Try Maven token if (protocol === 'oci') {
const mavenToken = await this.validateMavenToken(tokenString); return null;
if (mavenToken && (!protocol || protocol === 'maven')) {
return mavenToken;
}
// Try Composer token
const composerToken = await this.validateComposerToken(tokenString);
if (composerToken && (!protocol || protocol === 'composer')) {
return composerToken;
}
// Try Cargo token
const cargoToken = await this.validateCargoToken(tokenString);
if (cargoToken && (!protocol || protocol === 'cargo')) {
return cargoToken;
}
// Try PyPI token
const pypiToken = await this.validatePypiToken(tokenString);
if (pypiToken && (!protocol || protocol === 'pypi')) {
return pypiToken;
}
// Try RubyGems token
const rubygemsToken = await this.validateRubyGemsToken(tokenString);
if (rubygemsToken && (!protocol || protocol === 'rubygems')) {
return rubygemsToken;
} }
} }
// Try OCI JWT // UUID-based tokens: single O(1) Map lookup
const ociToken = await this.validateOciToken(tokenString); if (this.isValidUuid(tokenString)) {
if (ociToken && (!protocol || protocol === 'oci')) { const authToken = this.tokenStore.get(tokenString);
return ociToken; if (authToken) {
// If protocol specified, verify it matches
if (protocol && authToken.type !== protocol) {
return null;
}
// Check expiration
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
this.tokenStore.delete(tokenString);
return null;
}
return authToken;
}
} }
return null; return null;

View File

@@ -113,7 +113,7 @@ export class NpmRegistry extends BaseRegistry {
const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/); const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
if (unpublishVersionMatch && context.method === 'DELETE') { if (unpublishVersionMatch && context.method === 'DELETE') {
const [, packageName, version] = unpublishVersionMatch; const [, packageName, version] = unpublishVersionMatch;
console.log(`[unpublishVersionMatch] packageName=${packageName}, version=${version}`); this.logger.log('debug', 'unpublishVersionMatch', { packageName, version });
return this.unpublishVersion(packageName, version, token); return this.unpublishVersion(packageName, version, token);
} }
@@ -121,7 +121,7 @@ export class NpmRegistry extends BaseRegistry {
const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/); const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
if (unpublishPackageMatch && context.method === 'DELETE') { if (unpublishPackageMatch && context.method === 'DELETE') {
const [, packageName, rev] = unpublishPackageMatch; const [, packageName, rev] = unpublishPackageMatch;
console.log(`[unpublishPackageMatch] packageName=${packageName}, rev=${rev}`); this.logger.log('debug', 'unpublishPackageMatch', { packageName, rev });
return this.unpublishPackage(packageName, token); return this.unpublishPackage(packageName, token);
} }
@@ -129,7 +129,7 @@ export class NpmRegistry extends BaseRegistry {
const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/); const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
if (versionMatch) { if (versionMatch) {
const [, packageName, version] = versionMatch; const [, packageName, version] = versionMatch;
console.log(`[versionMatch] matched! packageName=${packageName}, version=${version}`); this.logger.log('debug', 'versionMatch', { packageName, version });
return this.handlePackageVersion(packageName, version, token); return this.handlePackageVersion(packageName, version, token);
} }
@@ -137,7 +137,7 @@ export class NpmRegistry extends BaseRegistry {
const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/); const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
if (packageMatch) { if (packageMatch) {
const packageName = packageMatch[1]; const packageName = packageMatch[1];
console.log(`[packageMatch] matched! packageName=${packageName}`); this.logger.log('debug', 'packageMatch', { packageName });
return this.handlePackage(context.method, packageName, context.body, context.query, token); return this.handlePackage(context.method, packageName, context.body, context.query, token);
} }
@@ -254,11 +254,11 @@ export class NpmRegistry extends BaseRegistry {
version: string, version: string,
token: IAuthToken | null token: IAuthToken | null
): Promise<IResponse> { ): Promise<IResponse> {
console.log(`[handlePackageVersion] packageName=${packageName}, version=${version}`); this.logger.log('debug', 'handlePackageVersion', { packageName, version });
const packument = await this.storage.getNpmPackument(packageName); const packument = await this.storage.getNpmPackument(packageName);
console.log(`[handlePackageVersion] packument found:`, !!packument); this.logger.log('debug', 'handlePackageVersion packument', { found: !!packument });
if (packument) { if (packument) {
console.log(`[handlePackageVersion] versions:`, Object.keys(packument.versions || {})); this.logger.log('debug', 'handlePackageVersion versions', { versions: Object.keys(packument.versions || {}) });
} }
if (!packument) { if (!packument) {
return { return {
@@ -621,7 +621,7 @@ export class NpmRegistry extends BaseRegistry {
} }
} }
} catch (error) { } catch (error) {
console.error('[handleSearch] Error:', error); this.logger.log('error', 'handleSearch failed', { error: (error as Error).message });
} }
// Apply pagination // Apply pagination

View File

@@ -455,9 +455,8 @@ export async function extractGemMetadata(gemData: Buffer): Promise<{
} }
return null; return null;
} catch (error) { } catch (_error) {
// Log error for debugging but return null gracefully // Error handled gracefully - return null and let caller handle missing metadata
console.error('Failed to extract gem metadata:', error);
return null; return null;
} }
} }