@push.rocks/smartregistry 1.4.1 → 1.6.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 +3 -3
- package/dist_ts/classes.smartregistry.d.ts +2 -2
- package/dist_ts/classes.smartregistry.js +37 -2
- package/dist_ts/core/classes.authmanager.d.ts +55 -1
- package/dist_ts/core/classes.authmanager.js +138 -3
- package/dist_ts/core/classes.registrystorage.d.ts +145 -0
- package/dist_ts/core/classes.registrystorage.js +392 -1
- package/dist_ts/core/interfaces.core.d.ts +13 -1
- package/dist_ts/index.d.ts +3 -1
- package/dist_ts/index.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +70 -0
- package/dist_ts/pypi/classes.pypiregistry.js +482 -0
- package/dist_ts/pypi/helpers.pypi.d.ts +84 -0
- package/dist_ts/pypi/helpers.pypi.js +263 -0
- package/dist_ts/pypi/index.d.ts +7 -0
- package/dist_ts/pypi/index.js +8 -0
- package/dist_ts/pypi/interfaces.pypi.d.ts +301 -0
- package/dist_ts/pypi/interfaces.pypi.js +6 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +86 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.js +475 -0
- package/dist_ts/rubygems/helpers.rubygems.d.ts +143 -0
- package/dist_ts/rubygems/helpers.rubygems.js +312 -0
- package/dist_ts/rubygems/index.d.ts +7 -0
- package/dist_ts/rubygems/index.js +8 -0
- package/dist_ts/rubygems/interfaces.rubygems.d.ts +236 -0
- package/dist_ts/rubygems/interfaces.rubygems.js +6 -0
- package/package.json +2 -2
- package/readme.hints.md +438 -2
- package/readme.md +288 -13
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.smartregistry.ts +41 -2
- package/ts/core/classes.authmanager.ts +161 -2
- package/ts/core/classes.registrystorage.ts +463 -0
- package/ts/core/interfaces.core.ts +13 -1
- package/ts/index.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +580 -0
- package/ts/pypi/helpers.pypi.ts +299 -0
- package/ts/pypi/index.ts +8 -0
- package/ts/pypi/interfaces.pypi.ts +316 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +598 -0
- package/ts/rubygems/helpers.rubygems.ts +398 -0
- package/ts/rubygems/index.ts +8 -0
- package/ts/rubygems/interfaces.rubygems.ts +251 -0
package/readme.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @push.rocks/smartregistry
|
|
2
2
|
|
|
3
|
-
> 🚀 A composable TypeScript library implementing **OCI Distribution Specification v1.1**, **NPM Registry API**, **Maven Repository**, **Cargo/crates.io Registry**,
|
|
3
|
+
> 🚀 A composable TypeScript library implementing **OCI Distribution Specification v1.1**, **NPM Registry API**, **Maven Repository**, **Cargo/crates.io Registry**, **Composer/Packagist**, **PyPI (Python Package Index)**, and **RubyGems Registry** for building unified container and package registries.
|
|
4
|
+
|
|
5
|
+
## Issue Reporting and Security
|
|
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 want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
4
8
|
|
|
5
9
|
## ✨ Features
|
|
6
10
|
|
|
@@ -10,12 +14,14 @@
|
|
|
10
14
|
- **Maven Repository**: Java/JVM artifact management with POM support
|
|
11
15
|
- **Cargo/crates.io Registry**: Rust crate registry with sparse HTTP protocol
|
|
12
16
|
- **Composer/Packagist**: PHP package registry with Composer v2 protocol
|
|
17
|
+
- **PyPI (Python Package Index)**: Python package registry with PEP 503/691 support
|
|
18
|
+
- **RubyGems Registry**: Ruby gem registry with compact index protocol
|
|
13
19
|
|
|
14
20
|
### 🏗️ Unified Architecture
|
|
15
21
|
- **Composable Design**: Core infrastructure with protocol plugins
|
|
16
22
|
- **Shared Storage**: Cloud-agnostic S3-compatible backend ([@push.rocks/smartbucket](https://www.npmjs.com/package/@push.rocks/smartbucket))
|
|
17
23
|
- **Unified Authentication**: Scope-based permissions across all protocols
|
|
18
|
-
- **Path-based Routing**: `/oci/*` for containers, `/npm/*` for packages, `/maven/*` for Java artifacts, `/cargo/*` for Rust crates, `/composer/*` for PHP packages
|
|
24
|
+
- **Path-based Routing**: `/oci/*` for containers, `/npm/*` for packages, `/maven/*` for Java artifacts, `/cargo/*` for Rust crates, `/composer/*` for PHP packages, `/pypi/*` for Python packages, `/rubygems/*` for Ruby gems
|
|
19
25
|
|
|
20
26
|
### 🔐 Authentication & Authorization
|
|
21
27
|
- NPM UUID tokens for package operations
|
|
@@ -59,6 +65,23 @@
|
|
|
59
65
|
- ✅ Dependency resolution
|
|
60
66
|
- ✅ PSR-4/PSR-0 autoloading support
|
|
61
67
|
|
|
68
|
+
**PyPI Features:**
|
|
69
|
+
- ✅ PEP 503 Simple Repository API (HTML)
|
|
70
|
+
- ✅ PEP 691 JSON-based Simple API
|
|
71
|
+
- ✅ Package upload (wheel and sdist)
|
|
72
|
+
- ✅ Package name normalization
|
|
73
|
+
- ✅ Hash verification (SHA256, MD5, Blake2b)
|
|
74
|
+
- ✅ Content negotiation (JSON/HTML)
|
|
75
|
+
- ✅ Metadata API (JSON endpoints)
|
|
76
|
+
|
|
77
|
+
**RubyGems Features:**
|
|
78
|
+
- ✅ Compact Index protocol (modern Bundler)
|
|
79
|
+
- ✅ Gem publish/download (.gem files)
|
|
80
|
+
- ✅ Version yank/unyank
|
|
81
|
+
- ✅ Platform-specific gems
|
|
82
|
+
- ✅ Dependency resolution
|
|
83
|
+
- ✅ Legacy API compatibility
|
|
84
|
+
|
|
62
85
|
## 📥 Installation
|
|
63
86
|
|
|
64
87
|
```bash
|
|
@@ -114,6 +137,14 @@ const config: IRegistryConfig = {
|
|
|
114
137
|
enabled: true,
|
|
115
138
|
basePath: '/composer',
|
|
116
139
|
},
|
|
140
|
+
pypi: {
|
|
141
|
+
enabled: true,
|
|
142
|
+
basePath: '/pypi',
|
|
143
|
+
},
|
|
144
|
+
rubygems: {
|
|
145
|
+
enabled: true,
|
|
146
|
+
basePath: '/rubygems',
|
|
147
|
+
},
|
|
117
148
|
};
|
|
118
149
|
|
|
119
150
|
const registry = new SmartRegistry(config);
|
|
@@ -145,6 +176,11 @@ ts/
|
|
|
145
176
|
├── npm/ # NPM implementation
|
|
146
177
|
│ ├── classes.npmregistry.ts
|
|
147
178
|
│ └── interfaces.npm.ts
|
|
179
|
+
├── maven/ # Maven implementation
|
|
180
|
+
├── cargo/ # Cargo implementation
|
|
181
|
+
├── composer/ # Composer implementation
|
|
182
|
+
├── pypi/ # PyPI implementation
|
|
183
|
+
├── rubygems/ # RubyGems implementation
|
|
148
184
|
└── classes.smartregistry.ts # Main orchestrator
|
|
149
185
|
```
|
|
150
186
|
|
|
@@ -157,7 +193,12 @@ SmartRegistry (orchestrator)
|
|
|
157
193
|
↓
|
|
158
194
|
Path-based routing
|
|
159
195
|
├─→ /oci/* → OciRegistry
|
|
160
|
-
|
|
196
|
+
├─→ /npm/* → NpmRegistry
|
|
197
|
+
├─→ /maven/* → MavenRegistry
|
|
198
|
+
├─→ /cargo/* → CargoRegistry
|
|
199
|
+
├─→ /composer/* → ComposerRegistry
|
|
200
|
+
├─→ /pypi/* → PypiRegistry
|
|
201
|
+
└─→ /rubygems/* → RubyGemsRegistry
|
|
161
202
|
↓
|
|
162
203
|
Shared Storage & Auth
|
|
163
204
|
↓
|
|
@@ -409,6 +450,171 @@ composer require vendor/package
|
|
|
409
450
|
composer update
|
|
410
451
|
```
|
|
411
452
|
|
|
453
|
+
### 🐍 PyPI Registry (Python Packages)
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// Get package index (PEP 503 HTML format)
|
|
457
|
+
const htmlIndex = await registry.handleRequest({
|
|
458
|
+
method: 'GET',
|
|
459
|
+
path: '/simple/requests/',
|
|
460
|
+
headers: { 'Accept': 'text/html' },
|
|
461
|
+
query: {},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Get package index (PEP 691 JSON format)
|
|
465
|
+
const jsonIndex = await registry.handleRequest({
|
|
466
|
+
method: 'GET',
|
|
467
|
+
path: '/simple/requests/',
|
|
468
|
+
headers: { 'Accept': 'application/vnd.pypi.simple.v1+json' },
|
|
469
|
+
query: {},
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Upload a Python package (wheel or sdist)
|
|
473
|
+
const formData = new FormData();
|
|
474
|
+
formData.append(':action', 'file_upload');
|
|
475
|
+
formData.append('protocol_version', '1');
|
|
476
|
+
formData.append('name', 'my-package');
|
|
477
|
+
formData.append('version', '1.0.0');
|
|
478
|
+
formData.append('filetype', 'bdist_wheel');
|
|
479
|
+
formData.append('pyversion', 'py3');
|
|
480
|
+
formData.append('metadata_version', '2.1');
|
|
481
|
+
formData.append('sha256_digest', 'abc123...');
|
|
482
|
+
formData.append('content', packageFile, { filename: 'my_package-1.0.0-py3-none-any.whl' });
|
|
483
|
+
|
|
484
|
+
const upload = await registry.handleRequest({
|
|
485
|
+
method: 'POST',
|
|
486
|
+
path: '/pypi/legacy/',
|
|
487
|
+
headers: {
|
|
488
|
+
'Authorization': `Bearer <pypi-token>`,
|
|
489
|
+
'Content-Type': 'multipart/form-data',
|
|
490
|
+
},
|
|
491
|
+
query: {},
|
|
492
|
+
body: formData,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Get package metadata (PyPI JSON API)
|
|
496
|
+
const metadata = await registry.handleRequest({
|
|
497
|
+
method: 'GET',
|
|
498
|
+
path: '/pypi/my-package/json',
|
|
499
|
+
headers: {},
|
|
500
|
+
query: {},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Download a specific version
|
|
504
|
+
const download = await registry.handleRequest({
|
|
505
|
+
method: 'GET',
|
|
506
|
+
path: '/packages/my-package/my_package-1.0.0-py3-none-any.whl',
|
|
507
|
+
headers: {},
|
|
508
|
+
query: {},
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Using with pip:**
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
# Install from custom registry
|
|
516
|
+
pip install --index-url https://registry.example.com/simple/ my-package
|
|
517
|
+
|
|
518
|
+
# Upload to custom registry
|
|
519
|
+
python -m twine upload --repository-url https://registry.example.com/pypi/legacy/ dist/*
|
|
520
|
+
|
|
521
|
+
# Configure in pip.conf or pip.ini
|
|
522
|
+
[global]
|
|
523
|
+
index-url = https://registry.example.com/simple/
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 💎 RubyGems Registry (Ruby Gems)
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// Get versions file (compact index)
|
|
530
|
+
const versions = await registry.handleRequest({
|
|
531
|
+
method: 'GET',
|
|
532
|
+
path: '/rubygems/versions',
|
|
533
|
+
headers: {},
|
|
534
|
+
query: {},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Get gem-specific info
|
|
538
|
+
const gemInfo = await registry.handleRequest({
|
|
539
|
+
method: 'GET',
|
|
540
|
+
path: '/rubygems/info/rails',
|
|
541
|
+
headers: {},
|
|
542
|
+
query: {},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Get list of all gem names
|
|
546
|
+
const names = await registry.handleRequest({
|
|
547
|
+
method: 'GET',
|
|
548
|
+
path: '/rubygems/names',
|
|
549
|
+
headers: {},
|
|
550
|
+
query: {},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Upload a gem file
|
|
554
|
+
const gemBuffer = await readFile('my-gem-1.0.0.gem');
|
|
555
|
+
const uploadGem = await registry.handleRequest({
|
|
556
|
+
method: 'POST',
|
|
557
|
+
path: '/rubygems/api/v1/gems',
|
|
558
|
+
headers: { 'Authorization': '<rubygems-api-key>' },
|
|
559
|
+
query: {},
|
|
560
|
+
body: gemBuffer,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Yank a version (make unavailable for install)
|
|
564
|
+
const yank = await registry.handleRequest({
|
|
565
|
+
method: 'DELETE',
|
|
566
|
+
path: '/rubygems/api/v1/gems/yank',
|
|
567
|
+
headers: { 'Authorization': '<rubygems-api-key>' },
|
|
568
|
+
query: { gem_name: 'my-gem', version: '1.0.0' },
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Unyank a version
|
|
572
|
+
const unyank = await registry.handleRequest({
|
|
573
|
+
method: 'PUT',
|
|
574
|
+
path: '/rubygems/api/v1/gems/unyank',
|
|
575
|
+
headers: { 'Authorization': '<rubygems-api-key>' },
|
|
576
|
+
query: { gem_name: 'my-gem', version: '1.0.0' },
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Get gem version metadata
|
|
580
|
+
const versionMeta = await registry.handleRequest({
|
|
581
|
+
method: 'GET',
|
|
582
|
+
path: '/rubygems/api/v1/versions/rails.json',
|
|
583
|
+
headers: {},
|
|
584
|
+
query: {},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Download gem file
|
|
588
|
+
const gemDownload = await registry.handleRequest({
|
|
589
|
+
method: 'GET',
|
|
590
|
+
path: '/rubygems/gems/rails-7.0.0.gem',
|
|
591
|
+
headers: {},
|
|
592
|
+
query: {},
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Using with Bundler:**
|
|
597
|
+
|
|
598
|
+
```ruby
|
|
599
|
+
# Gemfile
|
|
600
|
+
source 'https://registry.example.com/rubygems' do
|
|
601
|
+
gem 'my-gem'
|
|
602
|
+
gem 'rails'
|
|
603
|
+
end
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
# Install gems
|
|
608
|
+
bundle install
|
|
609
|
+
|
|
610
|
+
# Push gem to custom registry
|
|
611
|
+
gem push my-gem-1.0.0.gem --host https://registry.example.com/rubygems
|
|
612
|
+
|
|
613
|
+
# Configure gem source
|
|
614
|
+
gem sources --add https://registry.example.com/rubygems/
|
|
615
|
+
gem sources --remove https://rubygems.org/
|
|
616
|
+
```
|
|
617
|
+
|
|
412
618
|
### 🔐 Authentication
|
|
413
619
|
|
|
414
620
|
```typescript
|
|
@@ -530,6 +736,20 @@ Unified storage abstraction for both OCI and NPM content.
|
|
|
530
736
|
- `getNpmTarball(name, version)` - Get tarball
|
|
531
737
|
- `putNpmTarball(name, version, data)` - Store tarball
|
|
532
738
|
|
|
739
|
+
**PyPI Methods:**
|
|
740
|
+
- `getPypiPackageMetadata(name)` - Get package metadata
|
|
741
|
+
- `putPypiPackageMetadata(name, data)` - Store package metadata
|
|
742
|
+
- `getPypiPackageFile(name, filename)` - Get package file
|
|
743
|
+
- `putPypiPackageFile(name, filename, data)` - Store package file
|
|
744
|
+
|
|
745
|
+
**RubyGems Methods:**
|
|
746
|
+
- `getRubyGemsVersions()` - Get versions index
|
|
747
|
+
- `putRubyGemsVersions(data)` - Store versions index
|
|
748
|
+
- `getRubyGemsInfo(gemName)` - Get gem info
|
|
749
|
+
- `putRubyGemsInfo(gemName, data)` - Store gem info
|
|
750
|
+
- `getRubyGem(gemName, version)` - Get .gem file
|
|
751
|
+
- `putRubyGem(gemName, version, data)` - Store .gem file
|
|
752
|
+
|
|
533
753
|
#### AuthManager
|
|
534
754
|
|
|
535
755
|
Unified authentication manager supporting both NPM and OCI authentication schemes.
|
|
@@ -607,11 +827,45 @@ Composer v2 repository API compliant implementation.
|
|
|
607
827
|
- `DELETE /packages/{vendor}/{package}` - Delete entire package
|
|
608
828
|
- `DELETE /packages/{vendor}/{package}/{version}` - Delete specific version
|
|
609
829
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
830
|
+
#### PypiRegistry
|
|
831
|
+
|
|
832
|
+
PyPI (Python Package Index) registry implementing PEP 503 and PEP 691.
|
|
833
|
+
|
|
834
|
+
**Endpoints:**
|
|
835
|
+
- `GET /simple/` - List all packages (HTML or JSON)
|
|
836
|
+
- `GET /simple/{package}/` - List package files (HTML or JSON)
|
|
837
|
+
- `POST /legacy/` - Upload package (multipart/form-data)
|
|
838
|
+
- `GET /pypi/{package}/json` - Package metadata API
|
|
839
|
+
- `GET /pypi/{package}/{version}/json` - Version-specific metadata
|
|
840
|
+
- `GET /packages/{package}/{filename}` - Download package file
|
|
841
|
+
|
|
842
|
+
**Features:**
|
|
843
|
+
- PEP 503 Simple Repository API (HTML)
|
|
844
|
+
- PEP 691 JSON-based Simple API
|
|
845
|
+
- Content negotiation via Accept header
|
|
846
|
+
- Package name normalization
|
|
847
|
+
- Hash verification (SHA256, MD5, Blake2b)
|
|
848
|
+
|
|
849
|
+
#### RubyGemsRegistry
|
|
850
|
+
|
|
851
|
+
RubyGems registry with compact index protocol for modern Bundler.
|
|
852
|
+
|
|
853
|
+
**Endpoints:**
|
|
854
|
+
- `GET /versions` - Master versions file (all gems)
|
|
855
|
+
- `GET /info/{gem}` - Gem-specific info file
|
|
856
|
+
- `GET /names` - List of all gem names
|
|
857
|
+
- `POST /api/v1/gems` - Upload gem file
|
|
858
|
+
- `DELETE /api/v1/gems/yank` - Yank (deprecate) version
|
|
859
|
+
- `PUT /api/v1/gems/unyank` - Unyank version
|
|
860
|
+
- `GET /api/v1/versions/{gem}.json` - Version metadata
|
|
861
|
+
- `GET /gems/{gem}-{version}.gem` - Download gem file
|
|
862
|
+
|
|
863
|
+
**Features:**
|
|
864
|
+
- Compact Index format (append-only text files)
|
|
865
|
+
- Platform-specific gems support
|
|
866
|
+
- Yank/unyank functionality
|
|
867
|
+
- Checksum calculations (MD5 for index, SHA256 for gems)
|
|
868
|
+
- Legacy Marshal API compatibility
|
|
615
869
|
|
|
616
870
|
## 🗄️ Storage Structure
|
|
617
871
|
|
|
@@ -651,11 +905,24 @@ bucket/
|
|
|
651
905
|
│ │ └── {p1}/{p2}/{name} # 4+ char (e.g., "se/rd/serde")
|
|
652
906
|
│ └── crates/
|
|
653
907
|
│ └── {name}/{name}-{version}.crate # Gzipped tar archives
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
908
|
+
├── composer/
|
|
909
|
+
│ └── packages/
|
|
910
|
+
│ └── {vendor}/{package}/
|
|
911
|
+
│ ├── metadata.json # All versions metadata
|
|
912
|
+
│ └── {reference}.zip # Package ZIP files
|
|
913
|
+
├── pypi/
|
|
914
|
+
│ ├── simple/ # PEP 503 HTML files
|
|
915
|
+
│ │ ├── index.html # All packages list
|
|
916
|
+
│ │ └── {package}/index.html # Package versions list
|
|
917
|
+
│ ├── packages/
|
|
918
|
+
│ │ └── {package}/{filename} # .whl and .tar.gz files
|
|
919
|
+
│ └── metadata/
|
|
920
|
+
│ └── {package}/metadata.json # Package metadata
|
|
921
|
+
└── rubygems/
|
|
922
|
+
├── versions # Master versions file
|
|
923
|
+
├── info/{gemname} # Per-gem info files
|
|
924
|
+
├── names # All gem names
|
|
925
|
+
└── gems/{gemname}-{version}.gem # .gem files
|
|
659
926
|
```
|
|
660
927
|
|
|
661
928
|
## 🎯 Scope Format
|
|
@@ -685,6 +952,14 @@ Examples:
|
|
|
685
952
|
composer:package:vendor/package:read # Read Composer package
|
|
686
953
|
composer:package:*:write # Write any package
|
|
687
954
|
composer:*:*:* # Full Composer access
|
|
955
|
+
|
|
956
|
+
pypi:package:my-package:read # Read PyPI package
|
|
957
|
+
pypi:package:*:write # Write any package
|
|
958
|
+
pypi:*:*:* # Full PyPI access
|
|
959
|
+
|
|
960
|
+
rubygems:gem:rails:read # Read RubyGems gem
|
|
961
|
+
rubygems:gem:*:write # Write any gem
|
|
962
|
+
rubygems:*:*:* # Full RubyGems access
|
|
688
963
|
```
|
|
689
964
|
|
|
690
965
|
## 🔌 Integration Examples
|
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: '1.
|
|
7
|
-
description: '
|
|
6
|
+
version: '1.6.0',
|
|
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
|
}
|
|
@@ -7,10 +7,12 @@ import { NpmRegistry } from './npm/classes.npmregistry.js';
|
|
|
7
7
|
import { MavenRegistry } from './maven/classes.mavenregistry.js';
|
|
8
8
|
import { CargoRegistry } from './cargo/classes.cargoregistry.js';
|
|
9
9
|
import { ComposerRegistry } from './composer/classes.composerregistry.js';
|
|
10
|
+
import { PypiRegistry } from './pypi/classes.pypiregistry.js';
|
|
11
|
+
import { RubyGemsRegistry } from './rubygems/classes.rubygemsregistry.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Main registry orchestrator
|
|
13
|
-
* Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, or
|
|
15
|
+
* Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems)
|
|
14
16
|
*/
|
|
15
17
|
export class SmartRegistry {
|
|
16
18
|
private storage: RegistryStorage;
|
|
@@ -81,6 +83,24 @@ export class SmartRegistry {
|
|
|
81
83
|
this.registries.set('composer', composerRegistry);
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
// Initialize PyPI registry if enabled
|
|
87
|
+
if (this.config.pypi?.enabled) {
|
|
88
|
+
const pypiBasePath = this.config.pypi.basePath || '/pypi';
|
|
89
|
+
const registryUrl = `http://localhost:5000`; // TODO: Make configurable
|
|
90
|
+
const pypiRegistry = new PypiRegistry(this.storage, this.authManager, pypiBasePath, registryUrl);
|
|
91
|
+
await pypiRegistry.init();
|
|
92
|
+
this.registries.set('pypi', pypiRegistry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initialize RubyGems registry if enabled
|
|
96
|
+
if (this.config.rubygems?.enabled) {
|
|
97
|
+
const rubygemsBasePath = this.config.rubygems.basePath || '/rubygems';
|
|
98
|
+
const registryUrl = `http://localhost:5000${rubygemsBasePath}`; // TODO: Make configurable
|
|
99
|
+
const rubygemsRegistry = new RubyGemsRegistry(this.storage, this.authManager, rubygemsBasePath, registryUrl);
|
|
100
|
+
await rubygemsRegistry.init();
|
|
101
|
+
this.registries.set('rubygems', rubygemsRegistry);
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
this.initialized = true;
|
|
85
105
|
}
|
|
86
106
|
|
|
@@ -131,6 +151,25 @@ export class SmartRegistry {
|
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
153
|
|
|
154
|
+
// Route to PyPI registry (also handles /simple prefix)
|
|
155
|
+
if (this.config.pypi?.enabled) {
|
|
156
|
+
const pypiBasePath = this.config.pypi.basePath || '/pypi';
|
|
157
|
+
if (path.startsWith(pypiBasePath) || path.startsWith('/simple')) {
|
|
158
|
+
const pypiRegistry = this.registries.get('pypi');
|
|
159
|
+
if (pypiRegistry) {
|
|
160
|
+
return pypiRegistry.handleRequest(context);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Route to RubyGems registry
|
|
166
|
+
if (this.config.rubygems?.enabled && path.startsWith(this.config.rubygems.basePath)) {
|
|
167
|
+
const rubygemsRegistry = this.registries.get('rubygems');
|
|
168
|
+
if (rubygemsRegistry) {
|
|
169
|
+
return rubygemsRegistry.handleRequest(context);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
134
173
|
// No matching registry
|
|
135
174
|
return {
|
|
136
175
|
status: 404,
|
|
@@ -159,7 +198,7 @@ export class SmartRegistry {
|
|
|
159
198
|
/**
|
|
160
199
|
* Get a specific registry handler
|
|
161
200
|
*/
|
|
162
|
-
public getRegistry(protocol: 'oci' | 'npm' | 'maven' | 'cargo' | 'composer'): BaseRegistry | undefined {
|
|
201
|
+
public getRegistry(protocol: 'oci' | 'npm' | 'maven' | 'cargo' | 'composer' | 'pypi' | 'rubygems'): BaseRegistry | undefined {
|
|
163
202
|
return this.registries.get(protocol);
|
|
164
203
|
}
|
|
165
204
|
|
|
@@ -317,12 +317,153 @@ export class AuthManager {
|
|
|
317
317
|
this.tokenStore.delete(token);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// ========================================================================
|
|
321
|
+
// CARGO TOKEN MANAGEMENT
|
|
322
|
+
// ========================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a Cargo token
|
|
326
|
+
* @param userId - User ID
|
|
327
|
+
* @param readonly - Whether the token is readonly
|
|
328
|
+
* @returns Cargo UUID token
|
|
329
|
+
*/
|
|
330
|
+
public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
331
|
+
const scopes = readonly ? ['cargo:*:*:read'] : ['cargo:*:*:*'];
|
|
332
|
+
return this.createUuidToken(userId, 'cargo', scopes, readonly);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Validate a Cargo token
|
|
337
|
+
* @param token - Cargo UUID token
|
|
338
|
+
* @returns Auth token object or null
|
|
339
|
+
*/
|
|
340
|
+
public async validateCargoToken(token: string): Promise<IAuthToken | null> {
|
|
341
|
+
if (!this.isValidUuid(token)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const authToken = this.tokenStore.get(token);
|
|
346
|
+
if (!authToken || authToken.type !== 'cargo') {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check expiration if set
|
|
351
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
352
|
+
this.tokenStore.delete(token);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return authToken;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Revoke a Cargo token
|
|
361
|
+
* @param token - Cargo UUID token
|
|
362
|
+
*/
|
|
363
|
+
public async revokeCargoToken(token: string): Promise<void> {
|
|
364
|
+
this.tokenStore.delete(token);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ========================================================================
|
|
368
|
+
// PYPI AUTHENTICATION
|
|
369
|
+
// ========================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create a PyPI token
|
|
373
|
+
* @param userId - User ID
|
|
374
|
+
* @param readonly - Whether the token is readonly
|
|
375
|
+
* @returns PyPI UUID token
|
|
376
|
+
*/
|
|
377
|
+
public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
378
|
+
const scopes = readonly ? ['pypi:*:*:read'] : ['pypi:*:*:*'];
|
|
379
|
+
return this.createUuidToken(userId, 'pypi', scopes, readonly);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Validate a PyPI token
|
|
384
|
+
* @param token - PyPI UUID token
|
|
385
|
+
* @returns Auth token object or null
|
|
386
|
+
*/
|
|
387
|
+
public async validatePypiToken(token: string): Promise<IAuthToken | null> {
|
|
388
|
+
if (!this.isValidUuid(token)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const authToken = this.tokenStore.get(token);
|
|
393
|
+
if (!authToken || authToken.type !== 'pypi') {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check expiration if set
|
|
398
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
399
|
+
this.tokenStore.delete(token);
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return authToken;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Revoke a PyPI token
|
|
408
|
+
* @param token - PyPI UUID token
|
|
409
|
+
*/
|
|
410
|
+
public async revokePypiToken(token: string): Promise<void> {
|
|
411
|
+
this.tokenStore.delete(token);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ========================================================================
|
|
415
|
+
// RUBYGEMS AUTHENTICATION
|
|
416
|
+
// ========================================================================
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a RubyGems token
|
|
420
|
+
* @param userId - User ID
|
|
421
|
+
* @param readonly - Whether the token is readonly
|
|
422
|
+
* @returns RubyGems UUID token
|
|
423
|
+
*/
|
|
424
|
+
public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
425
|
+
const scopes = readonly ? ['rubygems:*:*:read'] : ['rubygems:*:*:*'];
|
|
426
|
+
return this.createUuidToken(userId, 'rubygems', scopes, readonly);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Validate a RubyGems token
|
|
431
|
+
* @param token - RubyGems UUID token
|
|
432
|
+
* @returns Auth token object or null
|
|
433
|
+
*/
|
|
434
|
+
public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> {
|
|
435
|
+
if (!this.isValidUuid(token)) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const authToken = this.tokenStore.get(token);
|
|
440
|
+
if (!authToken || authToken.type !== 'rubygems') {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check expiration if set
|
|
445
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
446
|
+
this.tokenStore.delete(token);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return authToken;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Revoke a RubyGems token
|
|
455
|
+
* @param token - RubyGems UUID token
|
|
456
|
+
*/
|
|
457
|
+
public async revokeRubyGemsToken(token: string): Promise<void> {
|
|
458
|
+
this.tokenStore.delete(token);
|
|
459
|
+
}
|
|
460
|
+
|
|
320
461
|
// ========================================================================
|
|
321
462
|
// UNIFIED AUTHENTICATION
|
|
322
463
|
// ========================================================================
|
|
323
464
|
|
|
324
465
|
/**
|
|
325
|
-
* Validate any token (NPM, Maven,
|
|
466
|
+
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
|
|
326
467
|
* @param tokenString - Token string (UUID or JWT)
|
|
327
468
|
* @param protocol - Expected protocol type
|
|
328
469
|
* @returns Auth token object or null
|
|
@@ -331,7 +472,7 @@ export class AuthManager {
|
|
|
331
472
|
tokenString: string,
|
|
332
473
|
protocol?: TRegistryProtocol
|
|
333
474
|
): Promise<IAuthToken | null> {
|
|
334
|
-
// Try UUID-based tokens (NPM, Maven, Composer)
|
|
475
|
+
// Try UUID-based tokens (NPM, Maven, Composer, Cargo, PyPI, RubyGems)
|
|
335
476
|
if (this.isValidUuid(tokenString)) {
|
|
336
477
|
// Try NPM token
|
|
337
478
|
const npmToken = await this.validateNpmToken(tokenString);
|
|
@@ -350,6 +491,24 @@ export class AuthManager {
|
|
|
350
491
|
if (composerToken && (!protocol || protocol === 'composer')) {
|
|
351
492
|
return composerToken;
|
|
352
493
|
}
|
|
494
|
+
|
|
495
|
+
// Try Cargo token
|
|
496
|
+
const cargoToken = await this.validateCargoToken(tokenString);
|
|
497
|
+
if (cargoToken && (!protocol || protocol === 'cargo')) {
|
|
498
|
+
return cargoToken;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Try PyPI token
|
|
502
|
+
const pypiToken = await this.validatePypiToken(tokenString);
|
|
503
|
+
if (pypiToken && (!protocol || protocol === 'pypi')) {
|
|
504
|
+
return pypiToken;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Try RubyGems token
|
|
508
|
+
const rubygemsToken = await this.validateRubyGemsToken(tokenString);
|
|
509
|
+
if (rubygemsToken && (!protocol || protocol === 'rubygems')) {
|
|
510
|
+
return rubygemsToken;
|
|
511
|
+
}
|
|
353
512
|
}
|
|
354
513
|
|
|
355
514
|
// Try OCI JWT
|