Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfadc89b5a | |||
| eb91a3f75b | |||
| 58a21a6bbb | |||
| da1cf8ddeb |
15
changelog.md
15
changelog.md
@@ -1,5 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-11-27 - 2.2.3 - fix(tests)
|
||||||
|
Use unique test run IDs and add S3 cleanup in test helpers to avoid cross-run conflicts
|
||||||
|
|
||||||
|
- Add generateTestRunId() helper in test/helpers/registry.ts to produce unique IDs for each test run
|
||||||
|
- Update PyPI and Composer native CLI tests to use generated testPackageName / unauth-pkg-<id> to avoid package name collisions between runs
|
||||||
|
- Import smartbucket and add S3 bucket cleanup logic in test helpers to remove leftover objects between test runs
|
||||||
|
- Improve test robustness by skipping upload-dependent checks when tools (twine/composer) are not available and logging outputs for debugging
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartregistry",
|
"name": "@push.rocks/smartregistry",
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -1,11 +1,63 @@
|
|||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from '@push.rocks/qenv';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as smartarchive from '@push.rocks/smartarchive';
|
import * as smartarchive from '@push.rocks/smartarchive';
|
||||||
|
import * as smartbucket from '@push.rocks/smartbucket';
|
||||||
import { SmartRegistry } from '../../ts/classes.smartregistry.js';
|
import { SmartRegistry } from '../../ts/classes.smartregistry.js';
|
||||||
import type { IRegistryConfig } from '../../ts/core/interfaces.core.js';
|
import type { IRegistryConfig } from '../../ts/core/interfaces.core.js';
|
||||||
|
|
||||||
const testQenv = new qenv.Qenv('./', './.nogit');
|
const testQenv = new qenv.Qenv('./', './.nogit');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up S3 bucket contents for a fresh test run
|
||||||
|
* @param prefix Optional prefix to delete (e.g., 'cargo/', 'npm/', 'composer/')
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Generate a unique test run ID for avoiding conflicts between test runs
|
||||||
|
* Uses timestamp + random suffix for uniqueness
|
||||||
|
*/
|
||||||
|
export function generateTestRunId(): string {
|
||||||
|
const timestamp = Date.now().toString(36);
|
||||||
|
const random = Math.random().toString(36).substring(2, 6);
|
||||||
|
return `${timestamp}${random}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupS3Bucket(prefix?: string): Promise<void> {
|
||||||
|
const s3AccessKey = await testQenv.getEnvVarOnDemand('S3_ACCESSKEY');
|
||||||
|
const s3SecretKey = await testQenv.getEnvVarOnDemand('S3_SECRETKEY');
|
||||||
|
const s3Endpoint = await testQenv.getEnvVarOnDemand('S3_ENDPOINT');
|
||||||
|
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT');
|
||||||
|
|
||||||
|
const s3 = new smartbucket.SmartBucket({
|
||||||
|
accessKey: s3AccessKey || 'minioadmin',
|
||||||
|
accessSecret: s3SecretKey || 'minioadmin',
|
||||||
|
endpoint: s3Endpoint || 'localhost',
|
||||||
|
port: parseInt(s3Port || '9000', 10),
|
||||||
|
useSsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bucket = await s3.getBucket('test-registry');
|
||||||
|
if (bucket) {
|
||||||
|
if (prefix) {
|
||||||
|
// Delete only objects with the given prefix
|
||||||
|
const files = await bucket.fastList({ prefix });
|
||||||
|
for (const file of files) {
|
||||||
|
await bucket.fastRemove({ path: file.name });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete all objects in the bucket
|
||||||
|
const files = await bucket.fastList({});
|
||||||
|
for (const file of files) {
|
||||||
|
await bucket.fastRemove({ path: file.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Bucket might not exist yet, that's fine
|
||||||
|
console.log('Cleanup: No bucket to clean or error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a test SmartRegistry instance with all protocols enabled
|
* Create a test SmartRegistry instance with all protocols enabled
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createComposerZip } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createComposerZip, generateTestRunId } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
@@ -21,6 +21,11 @@ let registryPort: number;
|
|||||||
let composerToken: string;
|
let composerToken: string;
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
let composerHome: string;
|
let composerHome: string;
|
||||||
|
let hasComposer = false;
|
||||||
|
|
||||||
|
// Unique test run ID to avoid conflicts between test runs
|
||||||
|
const testRunId = generateTestRunId();
|
||||||
|
const testPackageName = `testvendor/test-pkg-${testRunId}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTTP server wrapper around SmartRegistry
|
* Create HTTP server wrapper around SmartRegistry
|
||||||
@@ -235,12 +240,11 @@ tap.test('Composer CLI: should verify composer is installed', async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await tapNodeTools.runCommand('composer --version');
|
const result = await tapNodeTools.runCommand('composer --version');
|
||||||
console.log('Composer version output:', result.stdout.substring(0, 200));
|
console.log('Composer version output:', result.stdout.substring(0, 200));
|
||||||
|
hasComposer = result.exitCode === 0;
|
||||||
expect(result.exitCode).toEqual(0);
|
expect(result.exitCode).toEqual(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Composer CLI not available, skipping native CLI tests');
|
console.log('Composer CLI not available, skipping native CLI tests');
|
||||||
// Skip remaining tests if Composer is not installed
|
hasComposer = false;
|
||||||
tap.skip.test('Composer CLI: remaining tests skipped - composer not available');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -284,27 +288,32 @@ tap.test('Composer CLI: should verify server is responding', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should upload a package via API', async () => {
|
tap.test('Composer CLI: should upload a package via API', async () => {
|
||||||
const vendorPackage = 'testvendor/test-package';
|
|
||||||
const version = '1.0.0';
|
const version = '1.0.0';
|
||||||
|
|
||||||
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl);
|
await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
|
||||||
|
|
||||||
// Verify package exists via packages.json
|
// Verify package exists via p2 metadata endpoint (more reliable than packages.json for new packages)
|
||||||
const response = await fetch(`${registryUrl}/composer/packages.json`);
|
const metadataResponse = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
|
||||||
expect(response.status).toEqual(200);
|
expect(metadataResponse.status).toEqual(200);
|
||||||
|
|
||||||
const packagesJson = await response.json();
|
const metadata = await metadataResponse.json();
|
||||||
expect(packagesJson.packages).toBeDefined();
|
expect(metadata.packages).toBeDefined();
|
||||||
expect(packagesJson.packages[vendorPackage]).toBeDefined();
|
expect(metadata.packages[testPackageName]).toBeDefined();
|
||||||
|
expect(metadata.packages[testPackageName].length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should require package from registry', async () => {
|
tap.test('Composer CLI: should require package from registry', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
createComposerProject(projectDir, registryUrl);
|
createComposerProject(projectDir, registryUrl);
|
||||||
|
|
||||||
// Try to require the package we uploaded
|
// Try to require the package we uploaded
|
||||||
const result = await runComposerCommand(
|
const result = await runComposerCommand(
|
||||||
'require testvendor/test-package:1.0.0 --no-interaction',
|
`require ${testPackageName}:1.0.0 --no-interaction`,
|
||||||
projectDir
|
projectDir
|
||||||
);
|
);
|
||||||
console.log('composer require output:', result.stdout);
|
console.log('composer require output:', result.stdout);
|
||||||
@@ -314,8 +323,15 @@ tap.test('Composer CLI: should require package from registry', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should verify package in vendor directory', async () => {
|
tap.test('Composer CLI: should verify package in vendor directory', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
const packageDir = path.join(projectDir, 'vendor', 'testvendor', 'test-package');
|
// Parse vendor/package from testPackageName (e.g., "testvendor/test-pkg-abc123")
|
||||||
|
const [vendor, pkg] = testPackageName.split('/');
|
||||||
|
const packageDir = path.join(projectDir, 'vendor', vendor, pkg);
|
||||||
|
|
||||||
expect(fs.existsSync(packageDir)).toEqual(true);
|
expect(fs.existsSync(packageDir)).toEqual(true);
|
||||||
|
|
||||||
@@ -325,25 +341,36 @@ tap.test('Composer CLI: should verify package in vendor directory', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should upload second version', async () => {
|
tap.test('Composer CLI: should upload second version', async () => {
|
||||||
const vendorPackage = 'testvendor/test-package';
|
|
||||||
const version = '2.0.0';
|
const version = '2.0.0';
|
||||||
|
|
||||||
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl);
|
await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
|
||||||
|
|
||||||
// Verify both versions exist
|
// Verify both versions exist via p2 metadata endpoint (Composer v2 format)
|
||||||
const response = await fetch(`${registryUrl}/composer/packages.json`);
|
const response = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
|
||||||
const packagesJson = await response.json();
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
expect(packagesJson.packages[vendorPackage]['1.0.0']).toBeDefined();
|
const metadata = await response.json();
|
||||||
expect(packagesJson.packages[vendorPackage]['2.0.0']).toBeDefined();
|
expect(metadata.packages).toBeDefined();
|
||||||
|
expect(metadata.packages[testPackageName]).toBeDefined();
|
||||||
|
// Check that both versions are present
|
||||||
|
const versions = metadata.packages[testPackageName];
|
||||||
|
expect(versions.length).toBeGreaterThanOrEqual(2);
|
||||||
|
const versionNumbers = versions.map((v: any) => v.version);
|
||||||
|
expect(versionNumbers).toContain('1.0.0');
|
||||||
|
expect(versionNumbers).toContain('2.0.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should update to new version', async () => {
|
tap.test('Composer CLI: should update to new version', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
|
|
||||||
// Update to version 2.0.0
|
// Update to version 2.0.0
|
||||||
const result = await runComposerCommand(
|
const result = await runComposerCommand(
|
||||||
'require testvendor/test-package:2.0.0 --no-interaction',
|
`require ${testPackageName}:2.0.0 --no-interaction`,
|
||||||
projectDir
|
projectDir
|
||||||
);
|
);
|
||||||
console.log('composer update output:', result.stdout);
|
console.log('composer update output:', result.stdout);
|
||||||
@@ -355,11 +382,16 @@ tap.test('Composer CLI: should update to new version', async () => {
|
|||||||
expect(fs.existsSync(lockPath)).toEqual(true);
|
expect(fs.existsSync(lockPath)).toEqual(true);
|
||||||
|
|
||||||
const lockContent = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
const lockContent = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
||||||
const pkg = lockContent.packages.find((p: any) => p.name === 'testvendor/test-package');
|
const pkg = lockContent.packages.find((p: any) => p.name === testPackageName);
|
||||||
expect(pkg?.version).toEqual('2.0.0');
|
expect(pkg?.version).toEqual('2.0.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should search for packages', async () => {
|
tap.test('Composer CLI: should search for packages', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
|
|
||||||
// Search for packages (may not work on all Composer versions)
|
// Search for packages (may not work on all Composer versions)
|
||||||
@@ -375,23 +407,33 @@ tap.test('Composer CLI: should search for packages', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should show package info', async () => {
|
tap.test('Composer CLI: should show package info', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
|
|
||||||
const result = await runComposerCommand(
|
const result = await runComposerCommand(
|
||||||
'show testvendor/test-package --no-interaction',
|
`show ${testPackageName} --no-interaction`,
|
||||||
projectDir
|
projectDir
|
||||||
);
|
);
|
||||||
console.log('composer show output:', result.stdout);
|
console.log('composer show output:', result.stdout);
|
||||||
|
|
||||||
expect(result.exitCode).toEqual(0);
|
expect(result.exitCode).toEqual(0);
|
||||||
expect(result.stdout).toContain('testvendor/test-package');
|
expect(result.stdout).toContain(testPackageName);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Composer CLI: should remove package', async () => {
|
tap.test('Composer CLI: should remove package', async () => {
|
||||||
|
if (!hasComposer) {
|
||||||
|
console.log('Skipping - composer not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projectDir = path.join(testDir, 'consumer-project');
|
const projectDir = path.join(testDir, 'consumer-project');
|
||||||
|
|
||||||
const result = await runComposerCommand(
|
const result = await runComposerCommand(
|
||||||
'remove testvendor/test-package --no-interaction',
|
`remove ${testPackageName} --no-interaction`,
|
||||||
projectDir
|
projectDir
|
||||||
);
|
);
|
||||||
console.log('composer remove output:', result.stdout);
|
console.log('composer remove output:', result.stdout);
|
||||||
@@ -399,7 +441,8 @@ tap.test('Composer CLI: should remove package', async () => {
|
|||||||
expect(result.exitCode).toEqual(0);
|
expect(result.exitCode).toEqual(0);
|
||||||
|
|
||||||
// Verify package is removed from vendor
|
// Verify package is removed from vendor
|
||||||
const packageDir = path.join(projectDir, 'vendor', 'testvendor', 'test-package');
|
const [vendor, pkg] = testPackageName.split('/');
|
||||||
|
const packageDir = path.join(projectDir, 'vendor', vendor, pkg);
|
||||||
expect(fs.existsSync(packageDir)).toEqual(false);
|
expect(fs.existsSync(packageDir)).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createPythonWheel, createPythonSdist } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createPythonWheel, createPythonSdist, generateTestRunId } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
@@ -24,6 +24,10 @@ let pipHome: string;
|
|||||||
let hasPip = false;
|
let hasPip = false;
|
||||||
let hasTwine = false;
|
let hasTwine = false;
|
||||||
|
|
||||||
|
// Unique test run ID to avoid conflicts between test runs
|
||||||
|
const testRunId = generateTestRunId();
|
||||||
|
const testPackageName = `test-pypi-pkg-${testRunId}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTTP server wrapper around SmartRegistry
|
* Create HTTP server wrapper around SmartRegistry
|
||||||
*/
|
*/
|
||||||
@@ -347,9 +351,8 @@ tap.test('PyPI CLI: should upload wheel with twine', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'test-pypi-pkg';
|
|
||||||
const version = '1.0.0';
|
const version = '1.0.0';
|
||||||
const wheelPath = await createTestWheelFile(packageName, version, testDir);
|
const wheelPath = await createTestWheelFile(testPackageName, version, testDir);
|
||||||
|
|
||||||
expect(fs.existsSync(wheelPath)).toEqual(true);
|
expect(fs.existsSync(wheelPath)).toEqual(true);
|
||||||
|
|
||||||
@@ -369,9 +372,7 @@ tap.test('PyPI CLI: should verify package in simple index', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'test-pypi-pkg';
|
const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
|
||||||
|
|
||||||
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
|
|
||||||
expect(response.status).toEqual(200);
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
@@ -384,9 +385,8 @@ tap.test('PyPI CLI: should upload sdist with twine', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'test-pypi-pkg';
|
|
||||||
const version = '1.1.0';
|
const version = '1.1.0';
|
||||||
const sdistPath = await createTestSdistFile(packageName, version, testDir);
|
const sdistPath = await createTestSdistFile(testPackageName, version, testDir);
|
||||||
|
|
||||||
expect(fs.existsSync(sdistPath)).toEqual(true);
|
expect(fs.existsSync(sdistPath)).toEqual(true);
|
||||||
|
|
||||||
@@ -406,9 +406,7 @@ tap.test('PyPI CLI: should list all versions in simple index', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'test-pypi-pkg';
|
const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
|
||||||
|
|
||||||
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
|
|
||||||
expect(response.status).toEqual(200);
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
@@ -422,14 +420,12 @@ tap.test('PyPI CLI: should get JSON metadata', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'test-pypi-pkg';
|
const response = await fetch(`${registryUrl}/pypi/pypi/${testPackageName}/json`);
|
||||||
|
|
||||||
const response = await fetch(`${registryUrl}/pypi/pypi/${packageName}/json`);
|
|
||||||
expect(response.status).toEqual(200);
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
const metadata = await response.json();
|
const metadata = await response.json();
|
||||||
expect(metadata.info).toBeDefined();
|
expect(metadata.info).toBeDefined();
|
||||||
expect(metadata.info.name).toEqual(packageName);
|
expect(metadata.info.name).toEqual(testPackageName);
|
||||||
expect(metadata.releases).toBeDefined();
|
expect(metadata.releases).toBeDefined();
|
||||||
expect(metadata.releases['1.0.0']).toBeDefined();
|
expect(metadata.releases['1.0.0']).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -445,7 +441,7 @@ tap.test('PyPI CLI: should download package with pip', async () => {
|
|||||||
|
|
||||||
// Download (not install) the package
|
// Download (not install) the package
|
||||||
const result = await runPipCommand(
|
const result = await runPipCommand(
|
||||||
`download test-pypi-pkg==1.0.0 --dest "${downloadDir}" --no-deps`,
|
`download ${testPackageName}==1.0.0 --dest "${downloadDir}" --no-deps`,
|
||||||
testDir
|
testDir
|
||||||
);
|
);
|
||||||
console.log('pip download output:', result.stdout);
|
console.log('pip download output:', result.stdout);
|
||||||
@@ -457,14 +453,17 @@ tap.test('PyPI CLI: should download package with pip', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('PyPI CLI: should search for packages via API', async () => {
|
tap.test('PyPI CLI: should search for packages via API', async () => {
|
||||||
const packageName = 'test-pypi-pkg';
|
if (!hasTwine) {
|
||||||
|
console.log('Skipping - twine not available (no packages uploaded)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the JSON API to search/list
|
// Use the JSON API to search/list
|
||||||
const response = await fetch(`${registryUrl}/pypi/pypi/${packageName}/json`);
|
const response = await fetch(`${registryUrl}/pypi/pypi/${testPackageName}/json`);
|
||||||
expect(response.status).toEqual(200);
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
const metadata = await response.json();
|
const metadata = await response.json();
|
||||||
expect(metadata.info.name).toEqual(packageName);
|
expect(metadata.info.name).toEqual(testPackageName);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('PyPI CLI: should fail upload without auth', async () => {
|
tap.test('PyPI CLI: should fail upload without auth', async () => {
|
||||||
@@ -473,9 +472,9 @@ tap.test('PyPI CLI: should fail upload without auth', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = 'unauth-pkg';
|
const unauthPkgName = `unauth-pkg-${testRunId}`;
|
||||||
const version = '1.0.0';
|
const version = '1.0.0';
|
||||||
const wheelPath = await createTestWheelFile(packageName, version, testDir);
|
const wheelPath = await createTestWheelFile(unauthPkgName, version, testDir);
|
||||||
|
|
||||||
// Create a pypirc without proper credentials
|
// Create a pypirc without proper credentials
|
||||||
const badPypircPath = path.join(testDir, '.bad-pypirc');
|
const badPypircPath = path.join(testDir, '.bad-pypirc');
|
||||||
|
|||||||
@@ -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.3',
|
||||||
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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user