@push.rocks/smartregistry 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.smartregistry.d.ts +33 -2
- package/dist_ts/classes.smartregistry.js +38 -5
- package/dist_ts/core/classes.authmanager.d.ts +30 -80
- package/dist_ts/core/classes.authmanager.js +63 -337
- package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
- package/dist_ts/core/classes.defaultauthprovider.js +311 -0
- package/dist_ts/core/classes.registrystorage.d.ts +70 -4
- package/dist_ts/core/classes.registrystorage.js +165 -5
- package/dist_ts/core/index.d.ts +3 -0
- package/dist_ts/core/index.js +7 -2
- package/dist_ts/core/interfaces.auth.d.ts +83 -0
- package/dist_ts/core/interfaces.auth.js +2 -0
- package/dist_ts/core/interfaces.core.d.ts +35 -0
- package/dist_ts/core/interfaces.storage.d.ts +120 -0
- package/dist_ts/core/interfaces.storage.js +2 -0
- package/dist_ts/upstream/classes.baseupstream.d.ts +2 -2
- package/dist_ts/upstream/classes.baseupstream.js +16 -14
- package/dist_ts/upstream/classes.upstreamcache.d.ts +69 -22
- package/dist_ts/upstream/classes.upstreamcache.js +207 -50
- package/package.json +1 -1
- package/readme.md +225 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.smartregistry.ts +39 -4
- package/ts/core/classes.authmanager.ts +74 -412
- package/ts/core/classes.defaultauthprovider.ts +393 -0
- package/ts/core/classes.registrystorage.ts +199 -5
- package/ts/core/index.ts +8 -1
- package/ts/core/interfaces.auth.ts +91 -0
- package/ts/core/interfaces.core.ts +39 -0
- package/ts/core/interfaces.storage.ts +130 -0
- package/ts/upstream/classes.baseupstream.ts +20 -15
- package/ts/upstream/classes.upstreamcache.ts +256 -53
package/readme.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Issue Reporting and Security
|
|
6
6
|
|
|
7
|
-
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
@@ -82,6 +82,19 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
82
82
|
- ✅ Dependency resolution
|
|
83
83
|
- ✅ Legacy API compatibility
|
|
84
84
|
|
|
85
|
+
### 🌐 Upstream Proxy & Caching
|
|
86
|
+
- **Multi-Upstream Support**: Configure multiple upstream registries per protocol with priority ordering
|
|
87
|
+
- **Scope-Based Routing**: Route specific packages/scopes to different upstreams (e.g., `@company/*` → private registry)
|
|
88
|
+
- **S3-Backed Cache**: Persistent caching using existing S3 storage with URL-based cache paths
|
|
89
|
+
- **Circuit Breaker**: Automatic failover with configurable thresholds
|
|
90
|
+
- **Stale-While-Revalidate**: Serve cached content while refreshing in background
|
|
91
|
+
- **Content-Aware TTLs**: Different TTLs for immutable (tarballs) vs mutable (metadata) content
|
|
92
|
+
|
|
93
|
+
### 🔌 Enterprise Extensibility
|
|
94
|
+
- **Pluggable Auth Provider** (`IAuthProvider`): Integrate LDAP, OAuth, SSO, or custom auth systems
|
|
95
|
+
- **Storage Event Hooks** (`IStorageHooks`): Quota tracking, audit logging, virus scanning, cache invalidation
|
|
96
|
+
- **Request Actor Context**: Pass user/org info through requests for audit trails and rate limiting
|
|
97
|
+
|
|
85
98
|
## 📥 Installation
|
|
86
99
|
|
|
87
100
|
```bash
|
|
@@ -648,6 +661,217 @@ const canWrite = await authManager.authorize(
|
|
|
648
661
|
);
|
|
649
662
|
```
|
|
650
663
|
|
|
664
|
+
### 🌐 Upstream Proxy Configuration
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
import { SmartRegistry, IRegistryConfig } from '@push.rocks/smartregistry';
|
|
668
|
+
|
|
669
|
+
const config: IRegistryConfig = {
|
|
670
|
+
storage: { /* S3 config */ },
|
|
671
|
+
auth: { /* Auth config */ },
|
|
672
|
+
npm: {
|
|
673
|
+
enabled: true,
|
|
674
|
+
basePath: '/npm',
|
|
675
|
+
upstream: {
|
|
676
|
+
enabled: true,
|
|
677
|
+
upstreams: [
|
|
678
|
+
{
|
|
679
|
+
id: 'company-private',
|
|
680
|
+
name: 'Company Private NPM',
|
|
681
|
+
url: 'https://npm.internal.company.com',
|
|
682
|
+
priority: 1, // Lower = higher priority
|
|
683
|
+
enabled: true,
|
|
684
|
+
scopeRules: [
|
|
685
|
+
{ pattern: '@company/*', action: 'include' },
|
|
686
|
+
{ pattern: '@internal/*', action: 'include' },
|
|
687
|
+
],
|
|
688
|
+
auth: { type: 'bearer', token: process.env.NPM_PRIVATE_TOKEN },
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 'npmjs',
|
|
692
|
+
name: 'NPM Public Registry',
|
|
693
|
+
url: 'https://registry.npmjs.org',
|
|
694
|
+
priority: 10,
|
|
695
|
+
enabled: true,
|
|
696
|
+
scopeRules: [
|
|
697
|
+
{ pattern: '@company/*', action: 'exclude' },
|
|
698
|
+
{ pattern: '@internal/*', action: 'exclude' },
|
|
699
|
+
],
|
|
700
|
+
auth: { type: 'none' },
|
|
701
|
+
cache: { defaultTtlSeconds: 300 },
|
|
702
|
+
resilience: { timeoutMs: 30000, maxRetries: 3 },
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
cache: { enabled: true, staleWhileRevalidate: true },
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
oci: {
|
|
709
|
+
enabled: true,
|
|
710
|
+
basePath: '/oci',
|
|
711
|
+
upstream: {
|
|
712
|
+
enabled: true,
|
|
713
|
+
upstreams: [
|
|
714
|
+
{
|
|
715
|
+
id: 'dockerhub',
|
|
716
|
+
name: 'Docker Hub',
|
|
717
|
+
url: 'https://registry-1.docker.io',
|
|
718
|
+
priority: 1,
|
|
719
|
+
enabled: true,
|
|
720
|
+
auth: { type: 'none' },
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
id: 'ghcr',
|
|
724
|
+
name: 'GitHub Container Registry',
|
|
725
|
+
url: 'https://ghcr.io',
|
|
726
|
+
priority: 2,
|
|
727
|
+
enabled: true,
|
|
728
|
+
scopeRules: [{ pattern: 'ghcr.io/*', action: 'include' }],
|
|
729
|
+
auth: { type: 'bearer', token: process.env.GHCR_TOKEN },
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const registry = new SmartRegistry(config);
|
|
737
|
+
await registry.init();
|
|
738
|
+
|
|
739
|
+
// Requests for @company/* packages go to private registry
|
|
740
|
+
// Other packages proxy through to npmjs.org with caching
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### 🔌 Custom Auth Provider
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
import { SmartRegistry, IAuthProvider, IAuthToken, ICredentials, TRegistryProtocol } from '@push.rocks/smartregistry';
|
|
747
|
+
|
|
748
|
+
// Implement custom auth (e.g., LDAP, OAuth)
|
|
749
|
+
class LdapAuthProvider implements IAuthProvider {
|
|
750
|
+
constructor(private ldapClient: LdapClient) {}
|
|
751
|
+
|
|
752
|
+
async authenticate(credentials: ICredentials): Promise<string | null> {
|
|
753
|
+
const result = await this.ldapClient.bind(credentials.username, credentials.password);
|
|
754
|
+
return result.success ? credentials.username : null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async validateToken(token: string, protocol?: TRegistryProtocol): Promise<IAuthToken | null> {
|
|
758
|
+
const session = await this.sessionStore.get(token);
|
|
759
|
+
if (!session) return null;
|
|
760
|
+
return {
|
|
761
|
+
userId: session.userId,
|
|
762
|
+
scopes: session.scopes,
|
|
763
|
+
readonly: session.readonly,
|
|
764
|
+
created: session.created,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async createToken(userId: string, protocol: TRegistryProtocol, options?: ITokenOptions): Promise<string> {
|
|
769
|
+
const token = crypto.randomUUID();
|
|
770
|
+
await this.sessionStore.set(token, { userId, protocol, ...options });
|
|
771
|
+
return token;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async revokeToken(token: string): Promise<void> {
|
|
775
|
+
await this.sessionStore.delete(token);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async authorize(token: IAuthToken | null, resource: string, action: string): Promise<boolean> {
|
|
779
|
+
if (!token) return action === 'read'; // Anonymous read-only
|
|
780
|
+
// Check LDAP groups, roles, etc.
|
|
781
|
+
return this.checkPermissions(token.userId, resource, action);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Use custom provider
|
|
786
|
+
const registry = new SmartRegistry({
|
|
787
|
+
...config,
|
|
788
|
+
authProvider: new LdapAuthProvider(ldapClient),
|
|
789
|
+
});
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### 📊 Storage Hooks (Quota & Audit)
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
import { SmartRegistry, IStorageHooks, IStorageHookContext } from '@push.rocks/smartregistry';
|
|
796
|
+
|
|
797
|
+
const storageHooks: IStorageHooks = {
|
|
798
|
+
// Block uploads that exceed quota
|
|
799
|
+
async beforePut(ctx: IStorageHookContext) {
|
|
800
|
+
if (ctx.actor?.orgId) {
|
|
801
|
+
const usage = await getStorageUsage(ctx.actor.orgId);
|
|
802
|
+
const quota = await getQuota(ctx.actor.orgId);
|
|
803
|
+
|
|
804
|
+
if (usage + (ctx.metadata?.size || 0) > quota) {
|
|
805
|
+
return { allowed: false, reason: 'Storage quota exceeded' };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return { allowed: true };
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
// Update usage tracking after successful upload
|
|
812
|
+
async afterPut(ctx: IStorageHookContext) {
|
|
813
|
+
if (ctx.actor?.orgId && ctx.metadata?.size) {
|
|
814
|
+
await incrementUsage(ctx.actor.orgId, ctx.metadata.size);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Audit log
|
|
818
|
+
await auditLog.write({
|
|
819
|
+
action: 'storage.put',
|
|
820
|
+
key: ctx.key,
|
|
821
|
+
protocol: ctx.protocol,
|
|
822
|
+
actor: ctx.actor,
|
|
823
|
+
timestamp: ctx.timestamp,
|
|
824
|
+
});
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
// Prevent deletion of protected packages
|
|
828
|
+
async beforeDelete(ctx: IStorageHookContext) {
|
|
829
|
+
if (await isProtectedPackage(ctx.key)) {
|
|
830
|
+
return { allowed: false, reason: 'Cannot delete protected package' };
|
|
831
|
+
}
|
|
832
|
+
return { allowed: true };
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
// Log all access for compliance
|
|
836
|
+
async afterGet(ctx: IStorageHookContext) {
|
|
837
|
+
await accessLog.write({
|
|
838
|
+
action: 'storage.get',
|
|
839
|
+
key: ctx.key,
|
|
840
|
+
actor: ctx.actor,
|
|
841
|
+
timestamp: ctx.timestamp,
|
|
842
|
+
});
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const registry = new SmartRegistry({
|
|
847
|
+
...config,
|
|
848
|
+
storageHooks,
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### 👤 Request Actor Context
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
// Pass actor information through requests for audit/quota tracking
|
|
856
|
+
const response = await registry.handleRequest({
|
|
857
|
+
method: 'PUT',
|
|
858
|
+
path: '/npm/my-package',
|
|
859
|
+
headers: { 'Authorization': 'Bearer <token>' },
|
|
860
|
+
query: {},
|
|
861
|
+
body: packageData,
|
|
862
|
+
actor: {
|
|
863
|
+
userId: 'user123',
|
|
864
|
+
tokenId: 'token-abc',
|
|
865
|
+
ip: req.ip,
|
|
866
|
+
userAgent: req.headers['user-agent'],
|
|
867
|
+
orgId: 'org-456',
|
|
868
|
+
sessionId: 'session-xyz',
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Actor info is available in storage hooks for quota/audit
|
|
873
|
+
```
|
|
874
|
+
|
|
651
875
|
## ⚙️ Configuration
|
|
652
876
|
|
|
653
877
|
### Storage Configuration
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartregistry',
|
|
6
|
-
version: '2.
|
|
6
|
+
version: '2.5.0',
|
|
7
7
|
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
|
8
8
|
}
|
|
@@ -11,8 +11,39 @@ import { PypiRegistry } from './pypi/classes.pypiregistry.js';
|
|
|
11
11
|
import { RubyGemsRegistry } from './rubygems/classes.rubygemsregistry.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Main registry orchestrator
|
|
15
|
-
* Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems)
|
|
14
|
+
* Main registry orchestrator.
|
|
15
|
+
* Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems).
|
|
16
|
+
*
|
|
17
|
+
* Supports pluggable authentication and storage hooks:
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Basic usage with default in-memory auth
|
|
22
|
+
* const registry = new SmartRegistry(config);
|
|
23
|
+
*
|
|
24
|
+
* // With custom auth provider (LDAP, OAuth, etc.)
|
|
25
|
+
* const registry = new SmartRegistry({
|
|
26
|
+
* ...config,
|
|
27
|
+
* authProvider: new LdapAuthProvider(ldapClient),
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // With storage hooks for quota tracking
|
|
31
|
+
* const registry = new SmartRegistry({
|
|
32
|
+
* ...config,
|
|
33
|
+
* storageHooks: {
|
|
34
|
+
* beforePut: async (ctx) => {
|
|
35
|
+
* const quota = await getQuota(ctx.actor?.orgId);
|
|
36
|
+
* if (ctx.metadata?.size > quota) {
|
|
37
|
+
* return { allowed: false, reason: 'Quota exceeded' };
|
|
38
|
+
* }
|
|
39
|
+
* return { allowed: true };
|
|
40
|
+
* },
|
|
41
|
+
* afterPut: async (ctx) => {
|
|
42
|
+
* await auditLog('storage.put', ctx);
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
16
47
|
*/
|
|
17
48
|
export class SmartRegistry {
|
|
18
49
|
private storage: RegistryStorage;
|
|
@@ -23,8 +54,12 @@ export class SmartRegistry {
|
|
|
23
54
|
|
|
24
55
|
constructor(config: IRegistryConfig) {
|
|
25
56
|
this.config = config;
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
|
|
58
|
+
// Create storage with optional hooks
|
|
59
|
+
this.storage = new RegistryStorage(config.storage, config.storageHooks);
|
|
60
|
+
|
|
61
|
+
// Create auth manager with optional custom provider
|
|
62
|
+
this.authManager = new AuthManager(config.auth, config.authProvider);
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
/**
|