Compare commits

...

4 Commits

Author SHA1 Message Date
jkunz 8b3da7dece v2.9.2
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-30 09:25:26 +00:00
jkunz 45e24ecff3 fix(core,testing): improve type safety and update tests for latest tstest and storage APIs 2026-04-30 09:25:26 +00:00
jkunz 2e2726a4de v2.9.1
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-16 14:18:12 +00:00
jkunz 0e35256062 fix(license): add missing MIT license file to repository 2026-04-16 14:18:12 +00:00
21 changed files with 1731 additions and 2195 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
## 2026-04-30 - 2.9.2 - fix(core,testing)
improve type safety and update tests for latest tstest and storage APIs
- harden header extraction in BaseRegistry to safely handle request contexts with missing or invalid headers
- migrate tests to the current tstest tapbundle APIs and assertion methods
- adjust binary upload tests to use Uint8Array and add null guards for stricter TypeScript checks
- update upstream and S3 cache test helpers to match newer registry and storage interfaces
- enable stricter TypeScript compiler settings and refresh related dependency versions
## 2026-04-16 - 2.9.1 - fix(license)
add missing MIT license file to repository
- Adds the project license file to align the repository contents with the package metadata license declaration.
## 2026-04-16 - 2.9.0 - feat(registry) ## 2026-04-16 - 2.9.0 - feat(registry)
add declarative protocol routing and request-scoped storage hook context across registries add declarative protocol routing and request-scoped storage hook context across registries
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2025 Task Venture Capital GmbH (hello@task.vc)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartregistry", "name": "@push.rocks/smartregistry",
"version": "2.9.0", "version": "2.9.2",
"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",
@@ -10,17 +10,19 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "(tstest test/ --verbose --logfile --timeout 240)", "test": "(tstest test/ --verbose --logfile --timeout 240)",
"build": "(tsbuild --allowimplicitany)", "build": "(tsbuild)",
"buildDocs": "(tsdoc)" "buildDocs": "(tsdoc)"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.4.0", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.0", "@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^2.0.2", "@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.0", "@git.zone/tstest": "^3.6.3",
"@push.rocks/smartarchive": "^5.2.1", "@push.rocks/smartarchive": "^5.2.1",
"@push.rocks/smartstorage": "^6.3.2", "@push.rocks/smartstorage": "^6.4.1",
"@types/node": "^25.5.0" "@types/adm-zip": "^0.5.8",
"@types/lodash.clonedeep": "^4.5.9",
"@types/node": "^25.6.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -40,20 +42,18 @@
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
".smartconfig.json", ".smartconfig.json",
"license",
"readme.md" "readme.md"
], ],
"pnpm": {
"overrides": {}
},
"dependencies": { "dependencies": {
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartbucket": "^4.5.1", "@push.rocks/smartbucket": "^4.6.1",
"@push.rocks/smartlog": "^3.2.1", "@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@tsclass/tsclass": "^9.5.0", "@tsclass/tsclass": "^9.5.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.17",
"minimatch": "^10.2.4" "minimatch": "^10.2.5"
}, },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" "packageManager": "pnpm@10.28.2"
} }
+1263 -1338
View File
File diff suppressed because it is too large Load Diff
+312 -780
View File
File diff suppressed because it is too large Load Diff
+31 -31
View File
@@ -1,4 +1,4 @@
import { tap, expect } from '@git.zone/tstest'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import { RegistryStorage } from '../ts/core/classes.registrystorage.js'; import { RegistryStorage } from '../ts/core/classes.registrystorage.js';
import { CargoRegistry } from '../ts/cargo/classes.cargoregistry.js'; import { CargoRegistry } from '../ts/cargo/classes.cargoregistry.js';
import { AuthManager } from '../ts/core/classes.authmanager.js'; import { AuthManager } from '../ts/core/classes.authmanager.js';
@@ -17,21 +17,21 @@ tap.test('should calculate correct index paths for different crate names', async
const getPath = (storage as any).getCargoIndexPath.bind(storage); const getPath = (storage as any).getCargoIndexPath.bind(storage);
// 1-character names // 1-character names
expect(getPath('a')).to.equal('cargo/index/1/a'); expect(getPath('a')).toEqual('cargo/index/1/a');
expect(getPath('z')).to.equal('cargo/index/1/z'); expect(getPath('z')).toEqual('cargo/index/1/z');
// 2-character names // 2-character names
expect(getPath('io')).to.equal('cargo/index/2/io'); expect(getPath('io')).toEqual('cargo/index/2/io');
expect(getPath('ab')).to.equal('cargo/index/2/ab'); expect(getPath('ab')).toEqual('cargo/index/2/ab');
// 3-character names // 3-character names
expect(getPath('axo')).to.equal('cargo/index/3/a/axo'); expect(getPath('axo')).toEqual('cargo/index/3/a/axo');
expect(getPath('foo')).to.equal('cargo/index/3/f/foo'); expect(getPath('foo')).toEqual('cargo/index/3/f/foo');
// 4+ character names // 4+ character names
expect(getPath('serde')).to.equal('cargo/index/se/rd/serde'); expect(getPath('serde')).toEqual('cargo/index/se/rd/serde');
expect(getPath('tokio')).to.equal('cargo/index/to/ki/tokio'); expect(getPath('tokio')).toEqual('cargo/index/to/ki/tokio');
expect(getPath('my-crate')).to.equal('cargo/index/my/--/my-crate'); expect(getPath('my-crate')).toEqual('cargo/index/my/--/my-crate');
}); });
// Test crate file path calculation // Test crate file path calculation
@@ -46,9 +46,9 @@ tap.test('should calculate correct crate file paths', async () => {
// Access private method for testing // Access private method for testing
const getPath = (storage as any).getCargoCratePath.bind(storage); const getPath = (storage as any).getCargoCratePath.bind(storage);
expect(getPath('serde', '1.0.0')).to.equal('cargo/crates/serde/serde-1.0.0.crate'); expect(getPath('serde', '1.0.0')).toEqual('cargo/crates/serde/serde-1.0.0.crate');
expect(getPath('tokio', '1.28.0')).to.equal('cargo/crates/tokio/tokio-1.28.0.crate'); expect(getPath('tokio', '1.28.0')).toEqual('cargo/crates/tokio/tokio-1.28.0.crate');
expect(getPath('my-crate', '0.1.0')).to.equal('cargo/crates/my-crate/my-crate-0.1.0.crate'); expect(getPath('my-crate', '0.1.0')).toEqual('cargo/crates/my-crate/my-crate-0.1.0.crate');
}); });
// Test crate name validation // Test crate name validation
@@ -73,28 +73,28 @@ tap.test('should validate crate names correctly', async () => {
const validate = (registry as any).validateCrateName.bind(registry); const validate = (registry as any).validateCrateName.bind(registry);
// Valid names // Valid names
expect(validate('serde')).to.be.true; expect(validate('serde')).toBeTrue();
expect(validate('tokio')).to.be.true; expect(validate('tokio')).toBeTrue();
expect(validate('my-crate')).to.be.true; expect(validate('my-crate')).toBeTrue();
expect(validate('my_crate')).to.be.true; expect(validate('my_crate')).toBeTrue();
expect(validate('crate123')).to.be.true; expect(validate('crate123')).toBeTrue();
expect(validate('a')).to.be.true; expect(validate('a')).toBeTrue();
// Invalid names (uppercase not allowed) // Invalid names (uppercase not allowed)
expect(validate('Serde')).to.be.false; expect(validate('Serde')).toBeFalse();
expect(validate('MyCreate')).to.be.false; expect(validate('MyCreate')).toBeFalse();
// Invalid names (special characters) // Invalid names (special characters)
expect(validate('my.crate')).to.be.false; expect(validate('my.crate')).toBeFalse();
expect(validate('my@crate')).to.be.false; expect(validate('my@crate')).toBeFalse();
expect(validate('my crate')).to.be.false; expect(validate('my crate')).toBeFalse();
// Invalid names (too long) // Invalid names (too long)
const longName = 'a'.repeat(65); const longName = 'a'.repeat(65);
expect(validate(longName)).to.be.false; expect(validate(longName)).toBeFalse();
// Invalid names (empty) // Invalid names (empty)
expect(validate('')).to.be.false; expect(validate('')).toBeFalse();
}); });
// Test config.json response // Test config.json response
@@ -122,12 +122,12 @@ tap.test('should return valid config.json', async () => {
query: {}, query: {},
}); });
expect(response.status).to.equal(200); expect(response.status).toEqual(200);
expect(response.headers['Content-Type']).to.equal('application/json'); expect(response.headers['Content-Type']).toEqual('application/json');
const body = await streamToJson(response.body); const body = await streamToJson(response.body);
expect(body).to.be.an('object'); expect(body).toBeTypeOf('object');
expect(body.dl).to.include('/api/v1/crates/{crate}/{version}/download'); expect(body.dl).toInclude('/api/v1/crates/{crate}/{version}/download');
expect(body.api).to.equal('http://localhost:5000/cargo'); expect(body.api).toEqual('http://localhost:5000/cargo');
}); });
export default tap.start(); export default tap.start();
-3
View File
@@ -26,9 +26,6 @@ tap.test('setup: should create DefaultAuthProvider', async () => {
realm: 'https://auth.example.com/token', realm: 'https://auth.example.com/token',
service: 'test-registry', service: 'test-registry',
}, },
mavenTokens: { enabled: true },
cargoTokens: { enabled: true },
composerTokens: { enabled: true },
pypiTokens: { enabled: true }, pypiTokens: { enabled: true },
rubygemsTokens: { enabled: true }, rubygemsTokens: { enabled: true },
}; };
+3 -1
View File
@@ -4,7 +4,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 } from './helpers/registry.js'; import { createTestRegistry, createTestTokens } from './helpers/registry.js';
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js'; import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
// Test context // Test context
let registry: SmartRegistry; let registry: SmartRegistry;
let server: http.Server; let server: http.Server;
+4 -2
View File
@@ -4,7 +4,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, generateTestRunId } 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';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
// Test context // Test context
let registry: SmartRegistry; let registry: SmartRegistry;
let server: http.Server; let server: http.Server;
@@ -208,7 +210,7 @@ async function uploadComposerPackage(
'Content-Type': 'application/zip', 'Content-Type': 'application/zip',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: zipData, body: Uint8Array.from(zipData),
}); });
if (!response.ok) { if (!response.ok) {
+10 -1
View File
@@ -97,6 +97,9 @@ tap.test('NPM: should publish package to smarts3', async () => {
username: 'testuser', username: 'testuser',
password: 'testpass', password: 'testpass',
}); });
if (!userId) {
throw new Error('Expected test user authentication to succeed');
}
const token = await authManager.createNpmToken(userId, false); const token = await authManager.createNpmToken(userId, false);
const packageData = { const packageData = {
@@ -157,6 +160,9 @@ tap.test('OCI: should store blob in smarts3', async () => {
username: 'testuser', username: 'testuser',
password: 'testpass', password: 'testpass',
}); });
if (!userId) {
throw new Error('Expected test user authentication to succeed');
}
const token = await authManager.createOciToken( const token = await authManager.createOciToken(
userId, userId,
['oci:repository:test-image:push'], ['oci:repository:test-image:push'],
@@ -260,8 +266,11 @@ tap.test('Cargo: should store crate in smarts3', async () => {
// Verify stored // Verify stored
const retrievedIndex = await storage.getCargoIndex('test-crate'); const retrievedIndex = await storage.getCargoIndex('test-crate');
expect(retrievedIndex).toBeDefined(); expect(retrievedIndex).toBeDefined();
if (!retrievedIndex) {
throw new Error('Expected Cargo index to be stored');
}
expect(retrievedIndex.length).toEqual(1); expect(retrievedIndex.length).toEqual(1);
expect(retrievedIndex[0].name).toEqual('test-crate'); expect(retrievedIndex[0]?.name).toEqual('test-crate');
}); });
/** /**
+4 -2
View File
@@ -4,7 +4,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, createTestPom, createTestJar } from './helpers/registry.js'; import { createTestRegistry, createTestTokens, createTestPom, createTestJar } from './helpers/registry.js';
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js'; import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
// Test context // Test context
let registry: SmartRegistry; let registry: SmartRegistry;
let server: http.Server; let server: http.Server;
@@ -270,7 +272,7 @@ tap.test('Maven CLI: should verify mvn is installed', async () => {
} catch (error) { } catch (error) {
console.log('Maven CLI not available, skipping native CLI tests'); console.log('Maven CLI not available, skipping native CLI tests');
// Skip remaining tests if Maven is not installed // Skip remaining tests if Maven is not installed
tap.skip.test('Maven CLI: remaining tests skipped - mvn not available'); tap.skip.test('Maven CLI: remaining tests skipped - mvn not available', async () => {});
return; return;
} }
}); });
-1
View File
@@ -4,7 +4,6 @@
*/ */
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 { SmartRegistry } from '../ts/index.js'; import { SmartRegistry } from '../ts/index.js';
import { createTestRegistry, createTestTokens, cleanupS3Bucket } from './helpers/registry.js'; import { createTestRegistry, createTestTokens, cleanupS3Bucket } from './helpers/registry.js';
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js'; import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
+3 -1
View File
@@ -4,7 +4,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 type { IRequestContext, IResponse, IRegistryConfig } from '../ts/core/interfaces.core.js'; import type { IRequestContext, IResponse, IRegistryConfig } from '../ts/core/interfaces.core.js';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
const testQenv = new qenv.Qenv('./', './.nogit'); const testQenv = new qenv.Qenv('./', './.nogit');
/** /**
+4 -2
View File
@@ -4,7 +4,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, generateTestRunId } 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';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
// Test context // Test context
let registry: SmartRegistry; let registry: SmartRegistry;
let server: http.Server; let server: http.Server;
@@ -291,7 +293,7 @@ tap.test('PyPI CLI: should verify twine is installed', async () => {
if (!hasPip && !hasTwine) { if (!hasPip && !hasTwine) {
console.log('Neither pip nor twine available, skipping native CLI tests'); console.log('Neither pip nor twine available, skipping native CLI tests');
tap.skip.test('PyPI CLI: remaining tests skipped - no CLI tools available'); tap.skip.test('PyPI CLI: remaining tests skipped - no CLI tools available', async () => {});
return; return;
} }
}); });
+1 -1
View File
@@ -62,7 +62,7 @@ tap.test('PyPI: should upload wheel file (POST /pypi/)', async () => {
formData.append('pyversion', 'py3'); formData.append('pyversion', 'py3');
formData.append('metadata_version', '2.1'); formData.append('metadata_version', '2.1');
formData.append('sha256_digest', hashes.sha256); formData.append('sha256_digest', hashes.sha256);
formData.append('content', new Blob([testWheelData]), filename); formData.append('content', new Blob([Uint8Array.from(testWheelData)]), filename);
const response = await registry.handleRequest({ const response = await registry.handleRequest({
method: 'POST', method: 'POST',
+3 -1
View File
@@ -4,7 +4,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, createRubyGem } from './helpers/registry.js'; import { createTestRegistry, createTestTokens, createRubyGem } from './helpers/registry.js';
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js'; import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
@@ -13,6 +13,8 @@ import * as url from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const tapNodeTools = new TapNodeTools(tap);
// Test context // Test context
let registry: SmartRegistry; let registry: SmartRegistry;
let server: http.Server; let server: http.Server;
+13 -3
View File
@@ -89,6 +89,17 @@ tap.test('setup: should create S3 storage backend', async () => {
} }
return paths; return paths;
}, },
objectExists: async (key: string): Promise<boolean> => {
try {
const data = await s3Bucket.fastGet({ path: key });
return !!data;
} catch {
return false;
}
},
getMetadata: async (_key: string): Promise<Record<string, string> | null> => {
return null;
},
}; };
expect(storageBackend).toBeTruthy(); expect(storageBackend).toBeTruthy();
@@ -584,9 +595,8 @@ tap.test('cleanup: should stop cache and clean up bucket', async () => {
// Clean up test bucket // Clean up test bucket
if (s3Bucket) { if (s3Bucket) {
try { try {
const files = await s3Bucket.fastList({}); for await (const filePath of s3Bucket.listAllObjects('')) {
for (const file of files) { await s3Bucket.fastRemove({ path: filePath });
await s3Bucket.fastRemove({ path: file.name });
} }
await smartBucket.removeBucket(bucketName); await smartBucket.removeBucket(bucketName);
} catch { } catch {
+21 -11
View File
@@ -9,9 +9,19 @@ import type {
IUpstreamProvider, IUpstreamProvider,
IUpstreamResolutionContext, IUpstreamResolutionContext,
IProtocolUpstreamConfig, IProtocolUpstreamConfig,
IUpstreamRegistryConfig,
} from '../ts/upstream/interfaces.upstream.js'; } from '../ts/upstream/interfaces.upstream.js';
import type { TRegistryProtocol } from '../ts/core/interfaces.core.js'; import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
const createUpstream = (id: string, url: string): IUpstreamRegistryConfig => ({
id,
name: id,
url,
priority: 1,
enabled: true,
auth: { type: 'none' },
});
// ============================================================================= // =============================================================================
// StaticUpstreamProvider Tests // StaticUpstreamProvider Tests
// ============================================================================= // =============================================================================
@@ -19,7 +29,7 @@ import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
tap.test('StaticUpstreamProvider: should return config for configured protocol', async () => { tap.test('StaticUpstreamProvider: should return config for configured protocol', async () => {
const npmConfig: IProtocolUpstreamConfig = { const npmConfig: IProtocolUpstreamConfig = {
enabled: true, enabled: true,
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
}; };
const provider = new StaticUpstreamProvider({ const provider = new StaticUpstreamProvider({
@@ -43,7 +53,7 @@ tap.test('StaticUpstreamProvider: should return null for unconfigured protocol',
const provider = new StaticUpstreamProvider({ const provider = new StaticUpstreamProvider({
npm: { npm: {
enabled: true, enabled: true,
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
}, },
}); });
@@ -62,15 +72,15 @@ tap.test('StaticUpstreamProvider: should support multiple protocols', async () =
const provider = new StaticUpstreamProvider({ const provider = new StaticUpstreamProvider({
npm: { npm: {
enabled: true, enabled: true,
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
}, },
oci: { oci: {
enabled: true, enabled: true,
upstreams: [{ id: 'dockerhub', url: 'https://registry-1.docker.io', priority: 1, enabled: true }], upstreams: [createUpstream('dockerhub', 'https://registry-1.docker.io')],
}, },
maven: { maven: {
enabled: true, enabled: true,
upstreams: [{ id: 'central', url: 'https://repo1.maven.org/maven2', priority: 1, enabled: true }], upstreams: [createUpstream('central', 'https://repo1.maven.org/maven2')],
}, },
}); });
@@ -113,7 +123,7 @@ tap.test('Provider Integration: should create registry with upstream provider',
trackingProvider = createTrackingUpstreamProvider({ trackingProvider = createTrackingUpstreamProvider({
npm: { npm: {
enabled: true, enabled: true,
upstreams: [{ id: 'test-npm', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('test-npm', 'https://registry.npmjs.org')],
}, },
}); });
@@ -200,13 +210,13 @@ tap.test('Custom Provider: should support dynamic resolution based on context',
// Internal packages go to private registry // Internal packages go to private registry
return { return {
enabled: true, enabled: true,
upstreams: [{ id: 'private', url: 'https://private.registry.com', priority: 1, enabled: true }], upstreams: [createUpstream('private', 'https://private.registry.com')],
}; };
} }
// Everything else goes to public registry // Everything else goes to public registry
return { return {
enabled: true, enabled: true,
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('public', 'https://registry.npmjs.org')],
}; };
}, },
}; };
@@ -237,12 +247,12 @@ tap.test('Custom Provider: should support actor-based resolution', async () => {
if (context.actor?.orgId === 'enterprise-org') { if (context.actor?.orgId === 'enterprise-org') {
return { return {
enabled: true, enabled: true,
upstreams: [{ id: 'enterprise', url: 'https://enterprise.registry.com', priority: 1, enabled: true }], upstreams: [createUpstream('enterprise', 'https://enterprise.registry.com')],
}; };
} }
return { return {
enabled: true, enabled: true,
upstreams: [{ id: 'default', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('default', 'https://registry.npmjs.org')],
}; };
}, },
}; };
@@ -277,7 +287,7 @@ tap.test('Custom Provider: should support disabling upstream for specific resour
} }
return { return {
enabled: true, enabled: true,
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }], upstreams: [createUpstream('public', 'https://registry.npmjs.org')],
}; };
}, },
}; };
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartregistry', name: '@push.rocks/smartregistry',
version: '2.9.0', version: '2.9.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'
} }
+6 -1
View File
@@ -6,7 +6,12 @@ import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from './in
*/ */
export abstract class BaseRegistry { export abstract class BaseRegistry {
protected getHeader(contextOrHeaders: IRequestContext | Record<string, string>, name: string): string | undefined { protected getHeader(contextOrHeaders: IRequestContext | Record<string, string>, name: string): string | undefined {
const headers = 'headers' in contextOrHeaders ? contextOrHeaders.headers : contextOrHeaders; const headers: Record<string, string> =
'headers' in contextOrHeaders &&
typeof contextOrHeaders.headers === 'object' &&
contextOrHeaders.headers !== null
? contextOrHeaders.headers
: contextOrHeaders as Record<string, string>;
if (headers[name] !== undefined) { if (headers[name] !== undefined) {
return headers[name]; return headers[name];
} }
+5 -1
View File
@@ -4,7 +4,11 @@
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"noImplicitAny": true,
"types": [
"node"
]
}, },
"exclude": ["dist_*/**/*.d.ts"] "exclude": ["dist_*/**/*.d.ts"]
} }