@push.rocks/smartregistry 2.6.0 โ 2.8.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/.smartconfig.json +24 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cargo/classes.cargoregistry.d.ts +8 -3
- package/dist_ts/cargo/classes.cargoregistry.js +71 -33
- package/dist_ts/classes.smartregistry.js +48 -36
- package/dist_ts/composer/classes.composerregistry.d.ts +14 -3
- package/dist_ts/composer/classes.composerregistry.js +64 -28
- package/dist_ts/core/classes.registrystorage.d.ts +45 -0
- package/dist_ts/core/classes.registrystorage.js +116 -1
- package/dist_ts/core/helpers.stream.d.ts +20 -0
- package/dist_ts/core/helpers.stream.js +59 -0
- package/dist_ts/core/index.d.ts +1 -0
- package/dist_ts/core/index.js +3 -1
- package/dist_ts/core/interfaces.core.d.ts +28 -5
- package/dist_ts/maven/classes.mavenregistry.d.ts +14 -3
- package/dist_ts/maven/classes.mavenregistry.js +78 -27
- package/dist_ts/npm/classes.npmregistry.d.ts +14 -3
- package/dist_ts/npm/classes.npmregistry.js +104 -48
- package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
- package/dist_ts/oci/classes.ociregistry.js +186 -73
- package/dist_ts/oci/classes.ociupstream.d.ts +5 -2
- package/dist_ts/oci/classes.ociupstream.js +17 -10
- package/dist_ts/oci/interfaces.oci.d.ts +4 -0
- package/dist_ts/pypi/classes.pypiregistry.d.ts +8 -3
- package/dist_ts/pypi/classes.pypiregistry.js +88 -50
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +8 -3
- package/dist_ts/rubygems/classes.rubygemsregistry.js +61 -23
- package/dist_ts/rubygems/helpers.rubygems.js +3 -3
- package/dist_ts/upstream/classes.upstreamcache.js +2 -2
- package/dist_ts/upstream/interfaces.upstream.d.ts +72 -1
- package/dist_ts/upstream/interfaces.upstream.js +24 -1
- package/package.json +24 -20
- package/readme.md +354 -812
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +84 -37
- package/ts/classes.smartregistry.ts +49 -35
- package/ts/composer/classes.composerregistry.ts +74 -30
- package/ts/core/classes.registrystorage.ts +133 -2
- package/ts/core/helpers.stream.ts +63 -0
- package/ts/core/index.ts +3 -0
- package/ts/core/interfaces.core.ts +29 -5
- package/ts/maven/classes.mavenregistry.ts +89 -28
- package/ts/npm/classes.npmregistry.ts +118 -49
- package/ts/oci/classes.ociregistry.ts +205 -77
- package/ts/oci/classes.ociupstream.ts +18 -8
- package/ts/oci/interfaces.oci.ts +4 -0
- package/ts/pypi/classes.pypiregistry.ts +100 -54
- package/ts/rubygems/classes.rubygemsregistry.ts +69 -24
- package/ts/rubygems/helpers.rubygems.ts +2 -2
- package/ts/upstream/classes.upstreamcache.ts +1 -1
- package/ts/upstream/interfaces.upstream.ts +82 -1
- package/npmextra.json +0 -18
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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**, **Composer/Packagist**, **PyPI (Python Package Index)**, and **RubyGems 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** โ everything you need to build a unified container and package registry in one library.
|
|
4
4
|
|
|
5
5
|
## Issue Reporting and Security
|
|
6
6
|
|
|
@@ -18,91 +18,57 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
18
18
|
- **RubyGems Registry**: Ruby gem registry with compact index protocol
|
|
19
19
|
|
|
20
20
|
### ๐๏ธ Unified Architecture
|
|
21
|
-
- **Composable Design**: Core infrastructure with protocol plugins
|
|
22
|
-
- **Shared Storage**: Cloud-agnostic S3-compatible backend
|
|
21
|
+
- **Composable Design**: Core infrastructure with protocol plugins โ enable only what you need
|
|
22
|
+
- **Shared Storage**: Cloud-agnostic S3-compatible backend via [@push.rocks/smartbucket](https://www.npmjs.com/package/@push.rocks/smartbucket) with standardized `IS3Descriptor` from [@tsclass/tsclass](https://www.npmjs.com/package/@tsclass/tsclass)
|
|
23
23
|
- **Unified Authentication**: Scope-based permissions across all protocols
|
|
24
|
-
- **Path-based Routing**: `/oci
|
|
24
|
+
- **Path-based Routing**: `/oci/*`, `/npm/*`, `/maven/*`, `/cargo/*`, `/composer/*`, `/pypi/*`, `/rubygems/*`
|
|
25
25
|
|
|
26
26
|
### ๐ Authentication & Authorization
|
|
27
27
|
- NPM UUID tokens for package operations
|
|
28
28
|
- OCI JWT tokens for container operations
|
|
29
|
+
- Protocol-specific tokens for Maven, Cargo, Composer, PyPI, and RubyGems
|
|
29
30
|
- Unified scope system: `npm:package:foo:write`, `oci:repository:bar:push`
|
|
30
|
-
- Pluggable
|
|
31
|
-
|
|
32
|
-
### ๐ฆ
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- โ
Dist-tag management
|
|
45
|
-
- โ
Token management
|
|
46
|
-
|
|
47
|
-
**Maven Features:**
|
|
48
|
-
- โ
Artifact upload/download
|
|
49
|
-
- โ
POM and metadata management
|
|
50
|
-
- โ
Snapshot and release versions
|
|
51
|
-
- โ
Checksum verification (MD5, SHA1)
|
|
52
|
-
|
|
53
|
-
**Cargo Features:**
|
|
54
|
-
- โ
Crate publish (.crate files)
|
|
55
|
-
- โ
Sparse HTTP protocol (modern index)
|
|
56
|
-
- โ
Version yank/unyank
|
|
57
|
-
- โ
Dependency resolution
|
|
58
|
-
- โ
Search functionality
|
|
59
|
-
|
|
60
|
-
**Composer Features:**
|
|
61
|
-
- โ
Package publish/download (ZIP format)
|
|
62
|
-
- โ
Composer v2 repository API
|
|
63
|
-
- โ
Package metadata (packages.json)
|
|
64
|
-
- โ
Version management
|
|
65
|
-
- โ
Dependency resolution
|
|
66
|
-
- โ
PSR-4/PSR-0 autoloading support
|
|
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
|
|
31
|
+
- **Pluggable Auth Provider** (`IAuthProvider`): Integrate LDAP, OAuth, SSO, or any custom auth
|
|
32
|
+
|
|
33
|
+
### ๐ฆ Protocol Feature Matrix
|
|
34
|
+
|
|
35
|
+
| Feature | OCI | NPM | Maven | Cargo | Composer | PyPI | RubyGems |
|
|
36
|
+
|---------|-----|-----|-------|-------|----------|------|----------|
|
|
37
|
+
| Publish/Upload | โ
| โ
| โ
| โ
| โ
| โ
| โ
|
|
|
38
|
+
| Download | โ
| โ
| โ
| โ
| โ
| โ
| โ
|
|
|
39
|
+
| Search | โ | โ
| โ | โ
| โ | โ | โ |
|
|
40
|
+
| Version Yank | โ | โ | โ | โ
| โ | โ | โ
|
|
|
41
|
+
| Metadata API | โ
| โ
| โ
| โ
| โ
| โ
| โ
|
|
|
42
|
+
| Token Auth | โ
| โ
| โ
| โ
| โ
| โ
| โ
|
|
|
43
|
+
| Checksum Verification | โ
| โ
| โ
| โ
| โ | โ
| โ
|
|
|
44
|
+
| Upstream Proxy | โ
| โ
| โ | โ | โ | โ | โ |
|
|
84
45
|
|
|
85
46
|
### ๐ Upstream Proxy & Caching
|
|
86
47
|
- **Multi-Upstream Support**: Configure multiple upstream registries per protocol with priority ordering
|
|
87
48
|
- **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
|
|
49
|
+
- **S3-Backed Cache**: Persistent caching using existing S3 storage
|
|
89
50
|
- **Circuit Breaker**: Automatic failover with configurable thresholds
|
|
90
51
|
- **Stale-While-Revalidate**: Serve cached content while refreshing in background
|
|
91
52
|
- **Content-Aware TTLs**: Different TTLs for immutable (tarballs) vs mutable (metadata) content
|
|
92
53
|
|
|
54
|
+
### ๐ Streaming-First Architecture
|
|
55
|
+
- **Web Streams API** (`ReadableStream<Uint8Array>`) โ cross-runtime (Node, Deno, Bun)
|
|
56
|
+
- **Zero-copy downloads**: Binary artifacts stream directly from S3 to the HTTP response
|
|
57
|
+
- **OCI upload streaming**: Chunked blob uploads stored as temp S3 objects, not accumulated in memory
|
|
58
|
+
- **Unified response type**: Every `response.body` is a `ReadableStream` โ one pattern for all consumers
|
|
59
|
+
|
|
93
60
|
### ๐ Enterprise Extensibility
|
|
94
|
-
- **Pluggable Auth Provider** (`IAuthProvider`): Integrate LDAP, OAuth, SSO, or custom auth systems
|
|
95
61
|
- **Storage Event Hooks** (`IStorageHooks`): Quota tracking, audit logging, virus scanning, cache invalidation
|
|
96
62
|
- **Request Actor Context**: Pass user/org info through requests for audit trails and rate limiting
|
|
97
63
|
|
|
98
64
|
## ๐ฅ Installation
|
|
99
65
|
|
|
100
66
|
```bash
|
|
101
|
-
# Using npm
|
|
102
|
-
npm install @push.rocks/smartregistry
|
|
103
|
-
|
|
104
67
|
# Using pnpm (recommended)
|
|
105
68
|
pnpm add @push.rocks/smartregistry
|
|
69
|
+
|
|
70
|
+
# Using npm
|
|
71
|
+
npm install @push.rocks/smartregistry
|
|
106
72
|
```
|
|
107
73
|
|
|
108
74
|
## ๐ Quick Start
|
|
@@ -130,40 +96,20 @@ const config: IRegistryConfig = {
|
|
|
130
96
|
service: 'my-registry',
|
|
131
97
|
},
|
|
132
98
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
},
|
|
141
|
-
maven: {
|
|
142
|
-
enabled: true,
|
|
143
|
-
basePath: '/maven',
|
|
144
|
-
},
|
|
145
|
-
cargo: {
|
|
146
|
-
enabled: true,
|
|
147
|
-
basePath: '/cargo',
|
|
148
|
-
},
|
|
149
|
-
composer: {
|
|
150
|
-
enabled: true,
|
|
151
|
-
basePath: '/composer',
|
|
152
|
-
},
|
|
153
|
-
pypi: {
|
|
154
|
-
enabled: true,
|
|
155
|
-
basePath: '/pypi',
|
|
156
|
-
},
|
|
157
|
-
rubygems: {
|
|
158
|
-
enabled: true,
|
|
159
|
-
basePath: '/rubygems',
|
|
160
|
-
},
|
|
99
|
+
// Enable only the protocols you need
|
|
100
|
+
oci: { enabled: true, basePath: '/oci' },
|
|
101
|
+
npm: { enabled: true, basePath: '/npm' },
|
|
102
|
+
maven: { enabled: true, basePath: '/maven' },
|
|
103
|
+
cargo: { enabled: true, basePath: '/cargo' },
|
|
104
|
+
composer: { enabled: true, basePath: '/composer' },
|
|
105
|
+
pypi: { enabled: true, basePath: '/pypi' },
|
|
106
|
+
rubygems: { enabled: true, basePath: '/rubygems' },
|
|
161
107
|
};
|
|
162
108
|
|
|
163
109
|
const registry = new SmartRegistry(config);
|
|
164
110
|
await registry.init();
|
|
165
111
|
|
|
166
|
-
// Handle
|
|
112
|
+
// Handle any incoming HTTP request โ the router does the rest
|
|
167
113
|
const response = await registry.handleRequest({
|
|
168
114
|
method: 'GET',
|
|
169
115
|
path: '/npm/express',
|
|
@@ -174,6 +120,27 @@ const response = await registry.handleRequest({
|
|
|
174
120
|
|
|
175
121
|
## ๐๏ธ Architecture
|
|
176
122
|
|
|
123
|
+
### Request Flow
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
HTTP Request
|
|
127
|
+
โ
|
|
128
|
+
SmartRegistry (orchestrator)
|
|
129
|
+
โ
|
|
130
|
+
Path-based routing
|
|
131
|
+
โโโ /oci/* โ OciRegistry
|
|
132
|
+
โโโ /npm/* โ NpmRegistry
|
|
133
|
+
โโโ /maven/* โ MavenRegistry
|
|
134
|
+
โโโ /cargo/* โ CargoRegistry
|
|
135
|
+
โโโ /composer/* โ ComposerRegistry
|
|
136
|
+
โโโ /pypi/* โ PypiRegistry
|
|
137
|
+
โโโ /rubygems/* โ RubyGemsRegistry
|
|
138
|
+
โ
|
|
139
|
+
Shared Storage & Auth
|
|
140
|
+
โ
|
|
141
|
+
S3-compatible backend
|
|
142
|
+
```
|
|
143
|
+
|
|
177
144
|
### Directory Structure
|
|
178
145
|
|
|
179
146
|
```
|
|
@@ -184,59 +151,33 @@ ts/
|
|
|
184
151
|
โ โโโ classes.authmanager.ts
|
|
185
152
|
โ โโโ interfaces.core.ts
|
|
186
153
|
โโโ oci/ # OCI implementation
|
|
187
|
-
โ โโโ classes.ociregistry.ts
|
|
188
|
-
โ โโโ interfaces.oci.ts
|
|
189
154
|
โโโ npm/ # NPM implementation
|
|
190
|
-
โ โโโ classes.npmregistry.ts
|
|
191
|
-
โ โโโ interfaces.npm.ts
|
|
192
155
|
โโโ maven/ # Maven implementation
|
|
193
156
|
โโโ cargo/ # Cargo implementation
|
|
194
157
|
โโโ composer/ # Composer implementation
|
|
195
158
|
โโโ pypi/ # PyPI implementation
|
|
196
159
|
โโโ rubygems/ # RubyGems implementation
|
|
160
|
+
โโโ upstream/ # Upstream proxy infrastructure
|
|
197
161
|
โโโ classes.smartregistry.ts # Main orchestrator
|
|
198
162
|
```
|
|
199
163
|
|
|
200
|
-
### Request Flow
|
|
201
|
-
|
|
202
|
-
```
|
|
203
|
-
HTTP Request
|
|
204
|
-
โ
|
|
205
|
-
SmartRegistry (orchestrator)
|
|
206
|
-
โ
|
|
207
|
-
Path-based routing
|
|
208
|
-
โโโ /oci/* โ OciRegistry
|
|
209
|
-
โโโ /npm/* โ NpmRegistry
|
|
210
|
-
โโโ /maven/* โ MavenRegistry
|
|
211
|
-
โโโ /cargo/* โ CargoRegistry
|
|
212
|
-
โโโ /composer/* โ ComposerRegistry
|
|
213
|
-
โโโ /pypi/* โ PypiRegistry
|
|
214
|
-
โโโ /rubygems/* โ RubyGemsRegistry
|
|
215
|
-
โ
|
|
216
|
-
Shared Storage & Auth
|
|
217
|
-
โ
|
|
218
|
-
S3-compatible backend
|
|
219
|
-
```
|
|
220
|
-
|
|
221
164
|
## ๐ก Usage Examples
|
|
222
165
|
|
|
223
166
|
### ๐ณ OCI Registry (Container Images)
|
|
224
167
|
|
|
225
168
|
```typescript
|
|
226
|
-
// Pull
|
|
169
|
+
// Pull a manifest
|
|
227
170
|
const response = await registry.handleRequest({
|
|
228
171
|
method: 'GET',
|
|
229
|
-
path: '/oci/
|
|
230
|
-
headers: {
|
|
231
|
-
'Authorization': 'Bearer <token>',
|
|
232
|
-
},
|
|
172
|
+
path: '/oci/library/nginx/manifests/latest',
|
|
173
|
+
headers: { 'Authorization': 'Bearer <token>' },
|
|
233
174
|
query: {},
|
|
234
175
|
});
|
|
235
176
|
|
|
236
|
-
// Push a blob
|
|
177
|
+
// Push a blob (two-step upload)
|
|
237
178
|
const uploadInit = await registry.handleRequest({
|
|
238
179
|
method: 'POST',
|
|
239
|
-
path: '/oci/
|
|
180
|
+
path: '/oci/myapp/blobs/uploads/',
|
|
240
181
|
headers: { 'Authorization': 'Bearer <token>' },
|
|
241
182
|
query: {},
|
|
242
183
|
});
|
|
@@ -245,17 +186,17 @@ const uploadId = uploadInit.headers['Docker-Upload-UUID'];
|
|
|
245
186
|
|
|
246
187
|
await registry.handleRequest({
|
|
247
188
|
method: 'PUT',
|
|
248
|
-
path: `/oci/
|
|
189
|
+
path: `/oci/myapp/blobs/uploads/${uploadId}`,
|
|
249
190
|
headers: { 'Authorization': 'Bearer <token>' },
|
|
250
191
|
query: { digest: 'sha256:abc123...' },
|
|
251
192
|
body: blobData,
|
|
252
193
|
});
|
|
253
194
|
```
|
|
254
195
|
|
|
255
|
-
### ๐ฆ NPM Registry
|
|
196
|
+
### ๐ฆ NPM Registry
|
|
256
197
|
|
|
257
198
|
```typescript
|
|
258
|
-
//
|
|
199
|
+
// Get package metadata
|
|
259
200
|
const metadata = await registry.handleRequest({
|
|
260
201
|
method: 'GET',
|
|
261
202
|
path: '/npm/express',
|
|
@@ -263,14 +204,6 @@ const metadata = await registry.handleRequest({
|
|
|
263
204
|
query: {},
|
|
264
205
|
});
|
|
265
206
|
|
|
266
|
-
// Download tarball
|
|
267
|
-
const tarball = await registry.handleRequest({
|
|
268
|
-
method: 'GET',
|
|
269
|
-
path: '/npm/express/-/express-4.18.0.tgz',
|
|
270
|
-
headers: {},
|
|
271
|
-
query: {},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
207
|
// Publish a package
|
|
275
208
|
const publishResponse = await registry.handleRequest({
|
|
276
209
|
method: 'PUT',
|
|
@@ -279,9 +212,7 @@ const publishResponse = await registry.handleRequest({
|
|
|
279
212
|
query: {},
|
|
280
213
|
body: {
|
|
281
214
|
name: 'my-package',
|
|
282
|
-
versions: {
|
|
283
|
-
'1.0.0': { /* version metadata */ },
|
|
284
|
-
},
|
|
215
|
+
versions: { '1.0.0': { /* version metadata */ } },
|
|
285
216
|
'dist-tags': { latest: '1.0.0' },
|
|
286
217
|
_attachments: {
|
|
287
218
|
'my-package-1.0.0.tgz': {
|
|
@@ -294,7 +225,7 @@ const publishResponse = await registry.handleRequest({
|
|
|
294
225
|
});
|
|
295
226
|
|
|
296
227
|
// Search packages
|
|
297
|
-
const
|
|
228
|
+
const search = await registry.handleRequest({
|
|
298
229
|
method: 'GET',
|
|
299
230
|
path: '/npm/-/v1/search',
|
|
300
231
|
headers: {},
|
|
@@ -305,7 +236,7 @@ const searchResults = await registry.handleRequest({
|
|
|
305
236
|
### ๐ฆ Cargo Registry (Rust Crates)
|
|
306
237
|
|
|
307
238
|
```typescript
|
|
308
|
-
// Get config
|
|
239
|
+
// Get registry config (required for Cargo sparse protocol)
|
|
309
240
|
const config = await registry.handleRequest({
|
|
310
241
|
method: 'GET',
|
|
311
242
|
path: '/cargo/config.json',
|
|
@@ -313,54 +244,22 @@ const config = await registry.handleRequest({
|
|
|
313
244
|
query: {},
|
|
314
245
|
});
|
|
315
246
|
|
|
316
|
-
// Get index file for a crate
|
|
317
|
-
const index = await registry.handleRequest({
|
|
318
|
-
method: 'GET',
|
|
319
|
-
path: '/cargo/se/rd/serde', // Path based on crate name length
|
|
320
|
-
headers: {},
|
|
321
|
-
query: {},
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// Download a crate file
|
|
325
|
-
const crateFile = await registry.handleRequest({
|
|
326
|
-
method: 'GET',
|
|
327
|
-
path: '/cargo/api/v1/crates/serde/1.0.0/download',
|
|
328
|
-
headers: {},
|
|
329
|
-
query: {},
|
|
330
|
-
});
|
|
331
|
-
|
|
332
247
|
// Publish a crate (binary format: [4 bytes JSON len][JSON][4 bytes crate len][.crate])
|
|
333
248
|
const publishResponse = await registry.handleRequest({
|
|
334
249
|
method: 'PUT',
|
|
335
250
|
path: '/cargo/api/v1/crates/new',
|
|
336
|
-
headers: { 'Authorization': '<cargo-token>' },
|
|
251
|
+
headers: { 'Authorization': '<cargo-token>' },
|
|
337
252
|
query: {},
|
|
338
|
-
body: binaryPublishData,
|
|
253
|
+
body: binaryPublishData,
|
|
339
254
|
});
|
|
340
255
|
|
|
341
|
-
// Yank a version
|
|
342
|
-
|
|
256
|
+
// Yank a version
|
|
257
|
+
await registry.handleRequest({
|
|
343
258
|
method: 'DELETE',
|
|
344
259
|
path: '/cargo/api/v1/crates/my-crate/0.1.0/yank',
|
|
345
260
|
headers: { 'Authorization': '<cargo-token>' },
|
|
346
261
|
query: {},
|
|
347
262
|
});
|
|
348
|
-
|
|
349
|
-
// Unyank a version
|
|
350
|
-
const unyankResponse = await registry.handleRequest({
|
|
351
|
-
method: 'PUT',
|
|
352
|
-
path: '/cargo/api/v1/crates/my-crate/0.1.0/unyank',
|
|
353
|
-
headers: { 'Authorization': '<cargo-token>' },
|
|
354
|
-
query: {},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Search crates
|
|
358
|
-
const search = await registry.handleRequest({
|
|
359
|
-
method: 'GET',
|
|
360
|
-
path: '/cargo/api/v1/crates',
|
|
361
|
-
headers: {},
|
|
362
|
-
query: { q: 'serde', per_page: '10' },
|
|
363
|
-
});
|
|
364
263
|
```
|
|
365
264
|
|
|
366
265
|
**Using with Cargo CLI:**
|
|
@@ -369,28 +268,17 @@ const search = await registry.handleRequest({
|
|
|
369
268
|
# .cargo/config.toml
|
|
370
269
|
[registries.myregistry]
|
|
371
270
|
index = "sparse+https://registry.example.com/cargo/"
|
|
372
|
-
|
|
373
|
-
[registries.myregistry.credential-provider]
|
|
374
|
-
# Or use credentials directly:
|
|
375
|
-
# [registries.myregistry]
|
|
376
|
-
# token = "your-api-token"
|
|
377
271
|
```
|
|
378
272
|
|
|
379
273
|
```bash
|
|
380
|
-
# Publish to custom registry
|
|
381
274
|
cargo publish --registry=myregistry
|
|
382
|
-
|
|
383
|
-
# Install from custom registry
|
|
384
275
|
cargo install --registry=myregistry my-crate
|
|
385
|
-
|
|
386
|
-
# Search custom registry
|
|
387
|
-
cargo search --registry=myregistry tokio
|
|
388
276
|
```
|
|
389
277
|
|
|
390
278
|
### ๐ผ Composer Registry (PHP Packages)
|
|
391
279
|
|
|
392
280
|
```typescript
|
|
393
|
-
// Get repository root
|
|
281
|
+
// Get repository root
|
|
394
282
|
const packagesJson = await registry.handleRequest({
|
|
395
283
|
method: 'GET',
|
|
396
284
|
path: '/composer/packages.json',
|
|
@@ -398,75 +286,34 @@ const packagesJson = await registry.handleRequest({
|
|
|
398
286
|
query: {},
|
|
399
287
|
});
|
|
400
288
|
|
|
401
|
-
//
|
|
402
|
-
const metadata = await registry.handleRequest({
|
|
403
|
-
method: 'GET',
|
|
404
|
-
path: '/composer/p2/vendor/package.json',
|
|
405
|
-
headers: {},
|
|
406
|
-
query: {},
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// Upload a package (ZIP with composer.json)
|
|
410
|
-
const zipBuffer = await readFile('package.zip');
|
|
289
|
+
// Upload a package (ZIP with composer.json inside)
|
|
411
290
|
const uploadResponse = await registry.handleRequest({
|
|
412
291
|
method: 'PUT',
|
|
413
292
|
path: '/composer/packages/vendor/package',
|
|
414
|
-
headers: { 'Authorization':
|
|
293
|
+
headers: { 'Authorization': 'Bearer <composer-token>' },
|
|
415
294
|
query: {},
|
|
416
295
|
body: zipBuffer,
|
|
417
296
|
});
|
|
418
|
-
|
|
419
|
-
// Download package ZIP
|
|
420
|
-
const download = await registry.handleRequest({
|
|
421
|
-
method: 'GET',
|
|
422
|
-
path: '/composer/dists/vendor/package/ref123.zip',
|
|
423
|
-
headers: {},
|
|
424
|
-
query: {},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// List all packages
|
|
428
|
-
const list = await registry.handleRequest({
|
|
429
|
-
method: 'GET',
|
|
430
|
-
path: '/composer/packages/list.json',
|
|
431
|
-
headers: {},
|
|
432
|
-
query: {},
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// Delete a specific version
|
|
436
|
-
const deleteVersion = await registry.handleRequest({
|
|
437
|
-
method: 'DELETE',
|
|
438
|
-
path: '/composer/packages/vendor/package/1.0.0',
|
|
439
|
-
headers: { 'Authorization': `Bearer <composer-token>` },
|
|
440
|
-
query: {},
|
|
441
|
-
});
|
|
442
297
|
```
|
|
443
298
|
|
|
444
299
|
**Using with Composer CLI:**
|
|
445
300
|
|
|
446
301
|
```json
|
|
447
|
-
// composer.json
|
|
448
302
|
{
|
|
449
303
|
"repositories": [
|
|
450
|
-
{
|
|
451
|
-
"type": "composer",
|
|
452
|
-
"url": "https://registry.example.com/composer"
|
|
453
|
-
}
|
|
304
|
+
{ "type": "composer", "url": "https://registry.example.com/composer" }
|
|
454
305
|
]
|
|
455
306
|
}
|
|
456
307
|
```
|
|
457
308
|
|
|
458
309
|
```bash
|
|
459
|
-
# Install from custom registry
|
|
460
310
|
composer require vendor/package
|
|
461
|
-
|
|
462
|
-
# Update packages
|
|
463
|
-
composer update
|
|
464
311
|
```
|
|
465
312
|
|
|
466
313
|
### ๐ PyPI Registry (Python Packages)
|
|
467
314
|
|
|
468
315
|
```typescript
|
|
469
|
-
// Get package index (PEP 503 HTML
|
|
316
|
+
// Get package index (PEP 503 HTML)
|
|
470
317
|
const htmlIndex = await registry.handleRequest({
|
|
471
318
|
method: 'GET',
|
|
472
319
|
path: '/simple/requests/',
|
|
@@ -474,7 +321,7 @@ const htmlIndex = await registry.handleRequest({
|
|
|
474
321
|
query: {},
|
|
475
322
|
});
|
|
476
323
|
|
|
477
|
-
// Get package index (PEP 691 JSON
|
|
324
|
+
// Get package index (PEP 691 JSON)
|
|
478
325
|
const jsonIndex = await registry.handleRequest({
|
|
479
326
|
method: 'GET',
|
|
480
327
|
path: '/simple/requests/',
|
|
@@ -482,89 +329,38 @@ const jsonIndex = await registry.handleRequest({
|
|
|
482
329
|
query: {},
|
|
483
330
|
});
|
|
484
331
|
|
|
485
|
-
// Upload a
|
|
486
|
-
const formData = new FormData();
|
|
487
|
-
formData.append(':action', 'file_upload');
|
|
488
|
-
formData.append('protocol_version', '1');
|
|
489
|
-
formData.append('name', 'my-package');
|
|
490
|
-
formData.append('version', '1.0.0');
|
|
491
|
-
formData.append('filetype', 'bdist_wheel');
|
|
492
|
-
formData.append('pyversion', 'py3');
|
|
493
|
-
formData.append('metadata_version', '2.1');
|
|
494
|
-
formData.append('sha256_digest', 'abc123...');
|
|
495
|
-
formData.append('content', packageFile, { filename: 'my_package-1.0.0-py3-none-any.whl' });
|
|
496
|
-
|
|
332
|
+
// Upload a package
|
|
497
333
|
const upload = await registry.handleRequest({
|
|
498
334
|
method: 'POST',
|
|
499
|
-
path: '/pypi/
|
|
335
|
+
path: '/pypi/',
|
|
500
336
|
headers: {
|
|
501
|
-
'Authorization':
|
|
337
|
+
'Authorization': 'Bearer <pypi-token>',
|
|
502
338
|
'Content-Type': 'multipart/form-data',
|
|
503
339
|
},
|
|
504
340
|
query: {},
|
|
505
|
-
body:
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// Download a specific version
|
|
517
|
-
const download = await registry.handleRequest({
|
|
518
|
-
method: 'GET',
|
|
519
|
-
path: '/packages/my-package/my_package-1.0.0-py3-none-any.whl',
|
|
520
|
-
headers: {},
|
|
521
|
-
query: {},
|
|
341
|
+
body: {
|
|
342
|
+
':action': 'file_upload',
|
|
343
|
+
protocol_version: '1',
|
|
344
|
+
name: 'my-package',
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
filetype: 'bdist_wheel',
|
|
347
|
+
content: wheelData,
|
|
348
|
+
filename: 'my_package-1.0.0-py3-none-any.whl',
|
|
349
|
+
},
|
|
522
350
|
});
|
|
523
351
|
```
|
|
524
352
|
|
|
525
353
|
**Using with pip:**
|
|
526
354
|
|
|
527
355
|
```bash
|
|
528
|
-
# Install from custom registry
|
|
529
356
|
pip install --index-url https://registry.example.com/simple/ my-package
|
|
530
|
-
|
|
531
|
-
# Upload to custom registry
|
|
532
|
-
python -m twine upload --repository-url https://registry.example.com/pypi/legacy/ dist/*
|
|
533
|
-
|
|
534
|
-
# Configure in pip.conf or pip.ini
|
|
535
|
-
[global]
|
|
536
|
-
index-url = https://registry.example.com/simple/
|
|
357
|
+
python -m twine upload --repository-url https://registry.example.com/pypi/ dist/*
|
|
537
358
|
```
|
|
538
359
|
|
|
539
|
-
### ๐ RubyGems Registry
|
|
360
|
+
### ๐ RubyGems Registry
|
|
540
361
|
|
|
541
362
|
```typescript
|
|
542
|
-
//
|
|
543
|
-
const versions = await registry.handleRequest({
|
|
544
|
-
method: 'GET',
|
|
545
|
-
path: '/rubygems/versions',
|
|
546
|
-
headers: {},
|
|
547
|
-
query: {},
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// Get gem-specific info
|
|
551
|
-
const gemInfo = await registry.handleRequest({
|
|
552
|
-
method: 'GET',
|
|
553
|
-
path: '/rubygems/info/rails',
|
|
554
|
-
headers: {},
|
|
555
|
-
query: {},
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
// Get list of all gem names
|
|
559
|
-
const names = await registry.handleRequest({
|
|
560
|
-
method: 'GET',
|
|
561
|
-
path: '/rubygems/names',
|
|
562
|
-
headers: {},
|
|
563
|
-
query: {},
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
// Upload a gem file
|
|
567
|
-
const gemBuffer = await readFile('my-gem-1.0.0.gem');
|
|
363
|
+
// Upload a gem
|
|
568
364
|
const uploadGem = await registry.handleRequest({
|
|
569
365
|
method: 'POST',
|
|
570
366
|
path: '/rubygems/api/v1/gems',
|
|
@@ -573,34 +369,10 @@ const uploadGem = await registry.handleRequest({
|
|
|
573
369
|
body: gemBuffer,
|
|
574
370
|
});
|
|
575
371
|
|
|
576
|
-
//
|
|
577
|
-
const
|
|
578
|
-
method: 'DELETE',
|
|
579
|
-
path: '/rubygems/api/v1/gems/yank',
|
|
580
|
-
headers: { 'Authorization': '<rubygems-api-key>' },
|
|
581
|
-
query: { gem_name: 'my-gem', version: '1.0.0' },
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Unyank a version
|
|
585
|
-
const unyank = await registry.handleRequest({
|
|
586
|
-
method: 'PUT',
|
|
587
|
-
path: '/rubygems/api/v1/gems/unyank',
|
|
588
|
-
headers: { 'Authorization': '<rubygems-api-key>' },
|
|
589
|
-
query: { gem_name: 'my-gem', version: '1.0.0' },
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
// Get gem version metadata
|
|
593
|
-
const versionMeta = await registry.handleRequest({
|
|
594
|
-
method: 'GET',
|
|
595
|
-
path: '/rubygems/api/v1/versions/rails.json',
|
|
596
|
-
headers: {},
|
|
597
|
-
query: {},
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
// Download gem file
|
|
601
|
-
const gemDownload = await registry.handleRequest({
|
|
372
|
+
// Get compact index
|
|
373
|
+
const versions = await registry.handleRequest({
|
|
602
374
|
method: 'GET',
|
|
603
|
-
path: '/rubygems/
|
|
375
|
+
path: '/rubygems/versions',
|
|
604
376
|
headers: {},
|
|
605
377
|
query: {},
|
|
606
378
|
});
|
|
@@ -612,180 +384,114 @@ const gemDownload = await registry.handleRequest({
|
|
|
612
384
|
# Gemfile
|
|
613
385
|
source 'https://registry.example.com/rubygems' do
|
|
614
386
|
gem 'my-gem'
|
|
615
|
-
gem 'rails'
|
|
616
387
|
end
|
|
617
388
|
```
|
|
618
389
|
|
|
619
390
|
```bash
|
|
620
|
-
# Install gems
|
|
621
|
-
bundle install
|
|
622
|
-
|
|
623
|
-
# Push gem to custom registry
|
|
624
391
|
gem push my-gem-1.0.0.gem --host https://registry.example.com/rubygems
|
|
625
|
-
|
|
626
|
-
# Configure gem source
|
|
627
|
-
gem sources --add https://registry.example.com/rubygems/
|
|
628
|
-
gem sources --remove https://rubygems.org/
|
|
392
|
+
bundle install
|
|
629
393
|
```
|
|
630
394
|
|
|
631
395
|
### ๐ Authentication
|
|
632
396
|
|
|
633
397
|
```typescript
|
|
634
|
-
// Get auth manager instance
|
|
635
398
|
const authManager = registry.getAuthManager();
|
|
636
399
|
|
|
637
400
|
// Authenticate user
|
|
638
|
-
const userId = await authManager.authenticate({
|
|
639
|
-
username: 'user',
|
|
640
|
-
password: 'pass',
|
|
641
|
-
});
|
|
401
|
+
const userId = await authManager.authenticate({ username: 'user', password: 'pass' });
|
|
642
402
|
|
|
643
|
-
// Create
|
|
403
|
+
// Create protocol-specific tokens
|
|
644
404
|
const npmToken = await authManager.createNpmToken(userId, false);
|
|
405
|
+
const ociToken = await authManager.createOciToken(userId, ['oci:repository:myapp:push'], 3600);
|
|
406
|
+
const pypiToken = await authManager.createPypiToken(userId, false);
|
|
407
|
+
const cargoToken = await authManager.createCargoToken(userId, false);
|
|
408
|
+
const composerToken = await authManager.createComposerToken(userId, false);
|
|
409
|
+
const rubygemsToken = await authManager.createRubyGemsToken(userId, false);
|
|
645
410
|
|
|
646
|
-
//
|
|
647
|
-
const ociToken = await authManager.createOciToken(
|
|
648
|
-
userId,
|
|
649
|
-
['oci:repository:myapp:push', 'oci:repository:myapp:pull'],
|
|
650
|
-
3600
|
|
651
|
-
);
|
|
652
|
-
|
|
653
|
-
// Validate any token
|
|
411
|
+
// Validate and check permissions
|
|
654
412
|
const token = await authManager.validateToken(npmToken, 'npm');
|
|
655
|
-
|
|
656
|
-
// Check permissions
|
|
657
|
-
const canWrite = await authManager.authorize(
|
|
658
|
-
token,
|
|
659
|
-
'npm:package:my-package',
|
|
660
|
-
'write'
|
|
661
|
-
);
|
|
413
|
+
const canWrite = await authManager.authorize(token, 'npm:package:my-package', 'write');
|
|
662
414
|
```
|
|
663
415
|
|
|
664
416
|
### ๐ Upstream Proxy Configuration
|
|
665
417
|
|
|
666
418
|
```typescript
|
|
667
|
-
import { SmartRegistry,
|
|
419
|
+
import { SmartRegistry, StaticUpstreamProvider } from '@push.rocks/smartregistry';
|
|
668
420
|
|
|
669
|
-
const
|
|
670
|
-
storage: { /* S3 config */ },
|
|
671
|
-
auth: { /* Auth config */ },
|
|
421
|
+
const upstreamProvider = new StaticUpstreamProvider({
|
|
672
422
|
npm: {
|
|
673
423
|
enabled: true,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
},
|
|
424
|
+
upstreams: [
|
|
425
|
+
{
|
|
426
|
+
id: 'company-private',
|
|
427
|
+
url: 'https://npm.internal.company.com',
|
|
428
|
+
priority: 1,
|
|
429
|
+
enabled: true,
|
|
430
|
+
scopeRules: [{ pattern: '@company/*', action: 'include' }],
|
|
431
|
+
auth: { type: 'bearer', token: process.env.NPM_PRIVATE_TOKEN },
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: 'npmjs',
|
|
435
|
+
url: 'https://registry.npmjs.org',
|
|
436
|
+
priority: 10,
|
|
437
|
+
enabled: true,
|
|
438
|
+
scopeRules: [{ pattern: '@company/*', action: 'exclude' }],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
cache: { enabled: true, staleWhileRevalidate: true },
|
|
707
442
|
},
|
|
708
443
|
oci: {
|
|
709
444
|
enabled: true,
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
},
|
|
445
|
+
upstreams: [
|
|
446
|
+
{ id: 'dockerhub', url: 'https://registry-1.docker.io', priority: 1, enabled: true },
|
|
447
|
+
],
|
|
733
448
|
},
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
const registry = new SmartRegistry(config);
|
|
737
|
-
await registry.init();
|
|
449
|
+
});
|
|
738
450
|
|
|
739
|
-
|
|
740
|
-
|
|
451
|
+
const registry = new SmartRegistry({
|
|
452
|
+
storage: { /* S3 config */ },
|
|
453
|
+
auth: { /* Auth config */ },
|
|
454
|
+
upstreamProvider,
|
|
455
|
+
npm: { enabled: true, basePath: '/npm' },
|
|
456
|
+
oci: { enabled: true, basePath: '/oci' },
|
|
457
|
+
});
|
|
741
458
|
```
|
|
742
459
|
|
|
743
460
|
### ๐ Custom Auth Provider
|
|
744
461
|
|
|
745
462
|
```typescript
|
|
746
|
-
import { SmartRegistry, IAuthProvider, IAuthToken,
|
|
463
|
+
import { SmartRegistry, IAuthProvider, IAuthToken, TRegistryProtocol } from '@push.rocks/smartregistry';
|
|
747
464
|
|
|
748
|
-
// Implement custom auth (e.g., LDAP, OAuth)
|
|
749
465
|
class LdapAuthProvider implements IAuthProvider {
|
|
750
|
-
|
|
466
|
+
async init() { /* connect to LDAP */ }
|
|
751
467
|
|
|
752
|
-
async authenticate(credentials
|
|
468
|
+
async authenticate(credentials) {
|
|
753
469
|
const result = await this.ldapClient.bind(credentials.username, credentials.password);
|
|
754
470
|
return result.success ? credentials.username : null;
|
|
755
471
|
}
|
|
756
472
|
|
|
757
473
|
async validateToken(token: string, protocol?: TRegistryProtocol): Promise<IAuthToken | null> {
|
|
758
474
|
const session = await this.sessionStore.get(token);
|
|
759
|
-
|
|
760
|
-
return {
|
|
761
|
-
userId: session.userId,
|
|
762
|
-
scopes: session.scopes,
|
|
763
|
-
readonly: session.readonly,
|
|
764
|
-
created: session.created,
|
|
765
|
-
};
|
|
475
|
+
return session ? { userId: session.userId, scopes: session.scopes } : null;
|
|
766
476
|
}
|
|
767
477
|
|
|
768
|
-
async createToken(userId: string, protocol: TRegistryProtocol, options
|
|
478
|
+
async createToken(userId: string, protocol: TRegistryProtocol, options?) {
|
|
769
479
|
const token = crypto.randomUUID();
|
|
770
480
|
await this.sessionStore.set(token, { userId, protocol, ...options });
|
|
771
481
|
return token;
|
|
772
482
|
}
|
|
773
483
|
|
|
774
|
-
async revokeToken(token: string)
|
|
775
|
-
await this.sessionStore.delete(token);
|
|
776
|
-
}
|
|
484
|
+
async revokeToken(token: string) { await this.sessionStore.delete(token); }
|
|
777
485
|
|
|
778
|
-
async authorize(token: IAuthToken | null, resource: string, action: string)
|
|
779
|
-
if (!token) return action === 'read';
|
|
780
|
-
// Check LDAP groups, roles, etc.
|
|
486
|
+
async authorize(token: IAuthToken | null, resource: string, action: string) {
|
|
487
|
+
if (!token) return action === 'read';
|
|
781
488
|
return this.checkPermissions(token.userId, resource, action);
|
|
782
489
|
}
|
|
783
490
|
}
|
|
784
491
|
|
|
785
|
-
// Use custom provider
|
|
786
492
|
const registry = new SmartRegistry({
|
|
787
493
|
...config,
|
|
788
|
-
authProvider: new LdapAuthProvider(
|
|
494
|
+
authProvider: new LdapAuthProvider(),
|
|
789
495
|
});
|
|
790
496
|
```
|
|
791
497
|
|
|
@@ -795,12 +501,10 @@ const registry = new SmartRegistry({
|
|
|
795
501
|
import { SmartRegistry, IStorageHooks, IStorageHookContext } from '@push.rocks/smartregistry';
|
|
796
502
|
|
|
797
503
|
const storageHooks: IStorageHooks = {
|
|
798
|
-
// Block uploads that exceed quota
|
|
799
504
|
async beforePut(ctx: IStorageHookContext) {
|
|
800
505
|
if (ctx.actor?.orgId) {
|
|
801
506
|
const usage = await getStorageUsage(ctx.actor.orgId);
|
|
802
507
|
const quota = await getQuota(ctx.actor.orgId);
|
|
803
|
-
|
|
804
508
|
if (usage + (ctx.metadata?.size || 0) > quota) {
|
|
805
509
|
return { allowed: false, reason: 'Storage quota exceeded' };
|
|
806
510
|
}
|
|
@@ -808,13 +512,7 @@ const storageHooks: IStorageHooks = {
|
|
|
808
512
|
return { allowed: true };
|
|
809
513
|
},
|
|
810
514
|
|
|
811
|
-
// Update usage tracking after successful upload
|
|
812
515
|
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
516
|
await auditLog.write({
|
|
819
517
|
action: 'storage.put',
|
|
820
518
|
key: ctx.key,
|
|
@@ -824,35 +522,21 @@ const storageHooks: IStorageHooks = {
|
|
|
824
522
|
});
|
|
825
523
|
},
|
|
826
524
|
|
|
827
|
-
// Prevent deletion of protected packages
|
|
828
525
|
async beforeDelete(ctx: IStorageHookContext) {
|
|
829
526
|
if (await isProtectedPackage(ctx.key)) {
|
|
830
527
|
return { allowed: false, reason: 'Cannot delete protected package' };
|
|
831
528
|
}
|
|
832
529
|
return { allowed: true };
|
|
833
530
|
},
|
|
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
531
|
};
|
|
845
532
|
|
|
846
|
-
const registry = new SmartRegistry({
|
|
847
|
-
...config,
|
|
848
|
-
storageHooks,
|
|
849
|
-
});
|
|
533
|
+
const registry = new SmartRegistry({ ...config, storageHooks });
|
|
850
534
|
```
|
|
851
535
|
|
|
852
536
|
### ๐ค Request Actor Context
|
|
853
537
|
|
|
854
538
|
```typescript
|
|
855
|
-
// Pass actor information
|
|
539
|
+
// Pass actor information for audit/quota tracking
|
|
856
540
|
const response = await registry.handleRequest({
|
|
857
541
|
method: 'PUT',
|
|
858
542
|
path: '/npm/my-package',
|
|
@@ -865,35 +549,25 @@ const response = await registry.handleRequest({
|
|
|
865
549
|
ip: req.ip,
|
|
866
550
|
userAgent: req.headers['user-agent'],
|
|
867
551
|
orgId: 'org-456',
|
|
868
|
-
sessionId: 'session-xyz',
|
|
869
552
|
},
|
|
870
553
|
});
|
|
871
|
-
|
|
872
|
-
// Actor info is available in storage hooks for quota/audit
|
|
873
554
|
```
|
|
874
555
|
|
|
875
556
|
## โ๏ธ Configuration
|
|
876
557
|
|
|
877
558
|
### Storage Configuration
|
|
878
559
|
|
|
879
|
-
|
|
560
|
+
Extends `IS3Descriptor` from `@tsclass/tsclass`:
|
|
880
561
|
|
|
881
562
|
```typescript
|
|
882
|
-
import type { IS3Descriptor } from '@tsclass/tsclass';
|
|
883
|
-
|
|
884
|
-
storage: IS3Descriptor & {
|
|
885
|
-
bucketName: string; // Bucket name for registry storage
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Example:
|
|
889
563
|
storage: {
|
|
890
564
|
accessKey: string; // S3 access key
|
|
891
565
|
accessSecret: string; // S3 secret key
|
|
892
566
|
endpoint: string; // S3 endpoint (e.g., 's3.amazonaws.com')
|
|
893
567
|
port?: number; // Default: 443
|
|
894
568
|
useSsl?: boolean; // Default: true
|
|
895
|
-
region?: string; // AWS region
|
|
896
|
-
bucketName: string; // Bucket name for
|
|
569
|
+
region?: string; // AWS region
|
|
570
|
+
bucketName: string; // Bucket name for registry storage
|
|
897
571
|
}
|
|
898
572
|
```
|
|
899
573
|
|
|
@@ -901,306 +575,227 @@ storage: {
|
|
|
901
575
|
|
|
902
576
|
```typescript
|
|
903
577
|
auth: {
|
|
904
|
-
jwtSecret: string;
|
|
578
|
+
jwtSecret: string;
|
|
905
579
|
tokenStore: 'memory' | 'redis' | 'database';
|
|
906
|
-
npmTokens: {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
};
|
|
910
|
-
ociTokens: {
|
|
911
|
-
enabled: boolean;
|
|
912
|
-
realm: string; // Auth server URL
|
|
913
|
-
service: string; // Service name
|
|
914
|
-
};
|
|
580
|
+
npmTokens: { enabled: boolean; defaultReadonly?: boolean };
|
|
581
|
+
ociTokens: { enabled: boolean; realm: string; service: string };
|
|
582
|
+
pypiTokens: { enabled: boolean };
|
|
583
|
+
rubygemsTokens: { enabled: boolean };
|
|
915
584
|
}
|
|
916
585
|
```
|
|
917
586
|
|
|
918
587
|
### Protocol Configuration
|
|
919
588
|
|
|
920
|
-
|
|
921
|
-
oci?: {
|
|
922
|
-
enabled: boolean;
|
|
923
|
-
basePath: string; // Default: '/oci'
|
|
924
|
-
features?: {
|
|
925
|
-
referrers?: boolean;
|
|
926
|
-
deletion?: boolean;
|
|
927
|
-
};
|
|
928
|
-
}
|
|
589
|
+
Each protocol accepts:
|
|
929
590
|
|
|
930
|
-
|
|
591
|
+
```typescript
|
|
592
|
+
{
|
|
931
593
|
enabled: boolean;
|
|
932
|
-
basePath: string; //
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
unpublish?: boolean;
|
|
936
|
-
search?: boolean;
|
|
937
|
-
};
|
|
594
|
+
basePath: string; // URL prefix, e.g. '/npm'
|
|
595
|
+
registryUrl?: string; // Public-facing base URL (used in generated metadata links)
|
|
596
|
+
features?: Record<string, boolean>;
|
|
938
597
|
}
|
|
939
598
|
```
|
|
940
599
|
|
|
600
|
+
The `registryUrl` is important when the registry is served behind a reverse proxy or on a non-default port. For example, if your server is at `https://registry.example.com`, set `registryUrl: 'https://registry.example.com/npm'` for the NPM protocol so that generated metadata URLs point to the correct host.
|
|
601
|
+
|
|
941
602
|
## ๐ API Reference
|
|
942
603
|
|
|
943
604
|
### Core Classes
|
|
944
605
|
|
|
945
606
|
#### SmartRegistry
|
|
946
607
|
|
|
947
|
-
Main orchestrator
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
####
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
- `GET /dists/{vendor}/{package}/{ref}.zip` - Download package ZIP
|
|
1059
|
-
- `PUT /packages/{vendor}/{package}` - Upload package (requires auth)
|
|
1060
|
-
- `DELETE /packages/{vendor}/{package}` - Delete entire package
|
|
1061
|
-
- `DELETE /packages/{vendor}/{package}/{version}` - Delete specific version
|
|
1062
|
-
|
|
1063
|
-
#### PypiRegistry
|
|
1064
|
-
|
|
1065
|
-
PyPI (Python Package Index) registry implementing PEP 503 and PEP 691.
|
|
1066
|
-
|
|
1067
|
-
**Endpoints:**
|
|
1068
|
-
- `GET /simple/` - List all packages (HTML or JSON)
|
|
1069
|
-
- `GET /simple/{package}/` - List package files (HTML or JSON)
|
|
1070
|
-
- `POST /legacy/` - Upload package (multipart/form-data)
|
|
1071
|
-
- `GET /pypi/{package}/json` - Package metadata API
|
|
1072
|
-
- `GET /pypi/{package}/{version}/json` - Version-specific metadata
|
|
1073
|
-
- `GET /packages/{package}/{filename}` - Download package file
|
|
1074
|
-
|
|
1075
|
-
**Features:**
|
|
1076
|
-
- PEP 503 Simple Repository API (HTML)
|
|
1077
|
-
- PEP 691 JSON-based Simple API
|
|
1078
|
-
- Content negotiation via Accept header
|
|
1079
|
-
- Package name normalization
|
|
1080
|
-
- Hash verification (SHA256, MD5, Blake2b)
|
|
1081
|
-
|
|
1082
|
-
#### RubyGemsRegistry
|
|
1083
|
-
|
|
1084
|
-
RubyGems registry with compact index protocol for modern Bundler.
|
|
1085
|
-
|
|
1086
|
-
**Endpoints:**
|
|
1087
|
-
- `GET /versions` - Master versions file (all gems)
|
|
1088
|
-
- `GET /info/{gem}` - Gem-specific info file
|
|
1089
|
-
- `GET /names` - List of all gem names
|
|
1090
|
-
- `POST /api/v1/gems` - Upload gem file
|
|
1091
|
-
- `DELETE /api/v1/gems/yank` - Yank (deprecate) version
|
|
1092
|
-
- `PUT /api/v1/gems/unyank` - Unyank version
|
|
1093
|
-
- `GET /api/v1/versions/{gem}.json` - Version metadata
|
|
1094
|
-
- `GET /gems/{gem}-{version}.gem` - Download gem file
|
|
1095
|
-
|
|
1096
|
-
**Features:**
|
|
1097
|
-
- Compact Index format (append-only text files)
|
|
1098
|
-
- Platform-specific gems support
|
|
1099
|
-
- Yank/unyank functionality
|
|
1100
|
-
- Checksum calculations (MD5 for index, SHA256 for gems)
|
|
1101
|
-
- Legacy Marshal API compatibility
|
|
608
|
+
Main orchestrator โ routes requests to the appropriate protocol handler.
|
|
609
|
+
|
|
610
|
+
| Method | Description |
|
|
611
|
+
|--------|-------------|
|
|
612
|
+
| `init()` | Initialize the registry and all enabled protocols |
|
|
613
|
+
| `handleRequest(context)` | Route and handle an HTTP request |
|
|
614
|
+
| `getStorage()` | Get the shared `RegistryStorage` instance |
|
|
615
|
+
| `getAuthManager()` | Get the shared `AuthManager` instance |
|
|
616
|
+
| `getRegistry(protocol)` | Get a specific protocol handler by name |
|
|
617
|
+
| `isInitialized()` | Check if the registry has been initialized |
|
|
618
|
+
| `destroy()` | Clean up resources |
|
|
619
|
+
|
|
620
|
+
### Protocol Endpoints
|
|
621
|
+
|
|
622
|
+
#### OCI Registry
|
|
623
|
+
|
|
624
|
+
| Method | Path | Description |
|
|
625
|
+
|--------|------|-------------|
|
|
626
|
+
| `GET` | `/{name}/manifests/{ref}` | Get manifest by tag or digest |
|
|
627
|
+
| `PUT` | `/{name}/manifests/{ref}` | Push manifest |
|
|
628
|
+
| `GET` | `/{name}/blobs/{digest}` | Get blob |
|
|
629
|
+
| `POST` | `/{name}/blobs/uploads/` | Initiate blob upload |
|
|
630
|
+
| `PUT` | `/{name}/blobs/uploads/{uuid}` | Complete blob upload |
|
|
631
|
+
| `GET` | `/{name}/tags/list` | List tags |
|
|
632
|
+
| `GET` | `/{name}/referrers/{digest}` | Get referrers (OCI 1.1) |
|
|
633
|
+
|
|
634
|
+
#### NPM Registry
|
|
635
|
+
|
|
636
|
+
| Method | Path | Description |
|
|
637
|
+
|--------|------|-------------|
|
|
638
|
+
| `GET` | `/{package}` | Get package metadata (packument) |
|
|
639
|
+
| `PUT` | `/{package}` | Publish package |
|
|
640
|
+
| `GET` | `/{package}/-/{tarball}` | Download tarball |
|
|
641
|
+
| `GET` | `/-/v1/search?text=...` | Search packages |
|
|
642
|
+
| `PUT` | `/-/user/org.couchdb.user:{user}` | Login |
|
|
643
|
+
| `GET/POST/DELETE` | `/-/npm/v1/tokens` | Token management |
|
|
644
|
+
| `PUT` | `/-/package/{pkg}/dist-tags/{tag}` | Manage dist-tags |
|
|
645
|
+
|
|
646
|
+
#### Maven Repository
|
|
647
|
+
|
|
648
|
+
| Method | Path | Description |
|
|
649
|
+
|--------|------|-------------|
|
|
650
|
+
| `PUT` | `/{group}/{artifact}/{version}/{file}` | Upload artifact |
|
|
651
|
+
| `GET` | `/{group}/{artifact}/{version}/{file}` | Download artifact |
|
|
652
|
+
| `GET` | `/{group}/{artifact}/maven-metadata.xml` | Get metadata |
|
|
653
|
+
|
|
654
|
+
#### Cargo Registry
|
|
655
|
+
|
|
656
|
+
| Method | Path | Description |
|
|
657
|
+
|--------|------|-------------|
|
|
658
|
+
| `GET` | `/config.json` | Registry configuration |
|
|
659
|
+
| `GET` | `/{p1}/{p2}/{name}` | Sparse index entry |
|
|
660
|
+
| `PUT` | `/api/v1/crates/new` | Publish crate (binary format) |
|
|
661
|
+
| `GET` | `/api/v1/crates/{crate}/{version}/download` | Download .crate |
|
|
662
|
+
| `DELETE` | `/api/v1/crates/{crate}/{version}/yank` | Yank version |
|
|
663
|
+
| `PUT` | `/api/v1/crates/{crate}/{version}/unyank` | Unyank version |
|
|
664
|
+
| `GET` | `/api/v1/crates?q=...` | Search crates |
|
|
665
|
+
|
|
666
|
+
#### Composer Registry
|
|
667
|
+
|
|
668
|
+
| Method | Path | Description |
|
|
669
|
+
|--------|------|-------------|
|
|
670
|
+
| `GET` | `/packages.json` | Repository metadata |
|
|
671
|
+
| `GET` | `/p2/{vendor}/{package}.json` | Package version metadata |
|
|
672
|
+
| `GET` | `/packages/list.json` | List all packages |
|
|
673
|
+
| `GET` | `/dists/{vendor}/{package}/{ref}.zip` | Download package ZIP |
|
|
674
|
+
| `PUT` | `/packages/{vendor}/{package}` | Upload package |
|
|
675
|
+
| `DELETE` | `/packages/{vendor}/{package}[/{version}]` | Delete package/version |
|
|
676
|
+
|
|
677
|
+
#### PyPI Registry
|
|
678
|
+
|
|
679
|
+
| Method | Path | Description |
|
|
680
|
+
|--------|------|-------------|
|
|
681
|
+
| `GET` | `/simple/` | List all packages (PEP 503/691) |
|
|
682
|
+
| `GET` | `/simple/{package}/` | List package files |
|
|
683
|
+
| `POST` | `/` | Upload package (multipart) |
|
|
684
|
+
| `GET` | `/pypi/{package}/json` | Package metadata API |
|
|
685
|
+
| `GET` | `/pypi/{package}/{version}/json` | Version metadata |
|
|
686
|
+
| `GET` | `/packages/{package}/{filename}` | Download file |
|
|
687
|
+
|
|
688
|
+
#### RubyGems Registry
|
|
689
|
+
|
|
690
|
+
| Method | Path | Description |
|
|
691
|
+
|--------|------|-------------|
|
|
692
|
+
| `GET` | `/versions` | Master versions file (compact index) |
|
|
693
|
+
| `GET` | `/info/{gem}` | Gem info file |
|
|
694
|
+
| `GET` | `/names` | List all gem names |
|
|
695
|
+
| `POST` | `/api/v1/gems` | Upload .gem file |
|
|
696
|
+
| `DELETE` | `/api/v1/gems/yank` | Yank version |
|
|
697
|
+
| `PUT` | `/api/v1/gems/unyank` | Unyank version |
|
|
698
|
+
| `GET` | `/api/v1/versions/{gem}.json` | Version metadata |
|
|
699
|
+
| `GET` | `/gems/{gem}-{version}.gem` | Download .gem file |
|
|
700
|
+
|
|
701
|
+
## ๐ฏ Scope Format
|
|
702
|
+
|
|
703
|
+
Unified scope format across all protocols:
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
{protocol}:{type}:{name}:{action}
|
|
707
|
+
|
|
708
|
+
Examples:
|
|
709
|
+
npm:package:express:read # Read express package
|
|
710
|
+
npm:package:*:write # Write any package
|
|
711
|
+
oci:repository:nginx:pull # Pull nginx image
|
|
712
|
+
oci:repository:*:push # Push any image
|
|
713
|
+
cargo:crate:serde:write # Write serde crate
|
|
714
|
+
composer:package:vendor/pkg:read # Read Composer package
|
|
715
|
+
pypi:package:requests:read # Read PyPI package
|
|
716
|
+
rubygems:gem:rails:write # Write RubyGems gem
|
|
717
|
+
{protocol}:*:*:* # Full access for a protocol
|
|
718
|
+
```
|
|
1102
719
|
|
|
1103
720
|
## ๐๏ธ Storage Structure
|
|
1104
721
|
|
|
1105
722
|
```
|
|
1106
723
|
bucket/
|
|
1107
724
|
โโโ oci/
|
|
1108
|
-
โ โโโ blobs/
|
|
1109
|
-
โ
|
|
1110
|
-
โ
|
|
1111
|
-
โ โ โโโ {repository}/{digest}
|
|
1112
|
-
โ โโโ tags/
|
|
1113
|
-
โ โโโ {repository}/tags.json
|
|
725
|
+
โ โโโ blobs/sha256/{hash}
|
|
726
|
+
โ โโโ manifests/{repository}/{digest}
|
|
727
|
+
โ โโโ tags/{repository}/tags.json
|
|
1114
728
|
โโโ npm/
|
|
1115
|
-
โ
|
|
1116
|
-
โ
|
|
1117
|
-
โ
|
|
1118
|
-
โ โ โ โโโ {name}-{ver}.tgz # Tarball
|
|
1119
|
-
โ โ โโโ @{scope}/{name}/
|
|
1120
|
-
โ โ โโโ index.json
|
|
1121
|
-
โ โ โโโ {name}-{ver}.tgz
|
|
1122
|
-
โ โโโ users/
|
|
1123
|
-
โ โโโ {username}.json
|
|
729
|
+
โ โโโ packages/{name}/
|
|
730
|
+
โ โโโ index.json # Packument
|
|
731
|
+
โ โโโ {name}-{ver}.tgz # Tarball
|
|
1124
732
|
โโโ maven/
|
|
1125
|
-
โ โโโ artifacts/
|
|
1126
|
-
โ
|
|
1127
|
-
โ โ โโโ {artifact}-{version}.jar
|
|
1128
|
-
โ โ โโโ {artifact}-{version}.pom
|
|
1129
|
-
โ โ โโโ {artifact}-{version}.{ext}
|
|
1130
|
-
โ โโโ metadata/
|
|
1131
|
-
โ โโโ {group-path}/{artifact}/maven-metadata.xml
|
|
733
|
+
โ โโโ artifacts/{group}/{artifact}/{version}/
|
|
734
|
+
โ โโโ metadata/{group}/{artifact}/maven-metadata.xml
|
|
1132
735
|
โโโ cargo/
|
|
1133
|
-
โ โโโ config.json
|
|
1134
|
-
โ โโโ index/
|
|
1135
|
-
โ
|
|
1136
|
-
โ โ โโโ 2/{name} # 2-char crate names (e.g., "io")
|
|
1137
|
-
โ โ โโโ 3/{c}/{name} # 3-char crate names (e.g., "3/a/axo")
|
|
1138
|
-
โ โ โโโ {p1}/{p2}/{name} # 4+ char (e.g., "se/rd/serde")
|
|
1139
|
-
โ โโโ crates/
|
|
1140
|
-
โ โโโ {name}/{name}-{version}.crate # Gzipped tar archives
|
|
736
|
+
โ โโโ config.json
|
|
737
|
+
โ โโโ index/{p1}/{p2}/{name} # Sparse index
|
|
738
|
+
โ โโโ crates/{name}/{name}-{ver}.crate
|
|
1141
739
|
โโโ composer/
|
|
1142
|
-
โ โโโ packages/
|
|
1143
|
-
โ
|
|
1144
|
-
โ
|
|
1145
|
-
โ โโโ {reference}.zip # Package ZIP files
|
|
740
|
+
โ โโโ packages/{vendor}/{package}/
|
|
741
|
+
โ โโโ metadata.json
|
|
742
|
+
โ โโโ {reference}.zip
|
|
1146
743
|
โโโ pypi/
|
|
1147
|
-
โ โโโ simple/
|
|
1148
|
-
โ
|
|
1149
|
-
โ
|
|
1150
|
-
โ
|
|
1151
|
-
โ โ โโโ {package}/{filename} # .whl and .tar.gz files
|
|
1152
|
-
โ โโโ metadata/
|
|
1153
|
-
โ โโโ {package}/metadata.json # Package metadata
|
|
744
|
+
โ โโโ simple/index.html
|
|
745
|
+
โ โโโ simple/{package}/index.html
|
|
746
|
+
โ โโโ packages/{package}/{filename}
|
|
747
|
+
โ โโโ metadata/{package}/metadata.json
|
|
1154
748
|
โโโ rubygems/
|
|
1155
|
-
โโโ versions
|
|
1156
|
-
โโโ info/{gemname}
|
|
1157
|
-
โโโ names
|
|
1158
|
-
โโโ gems/{gemname}-{version}.gem
|
|
749
|
+
โโโ versions
|
|
750
|
+
โโโ info/{gemname}
|
|
751
|
+
โโโ names
|
|
752
|
+
โโโ gems/{gemname}-{version}.gem
|
|
1159
753
|
```
|
|
1160
754
|
|
|
1161
|
-
##
|
|
755
|
+
## ๐ Streaming Architecture
|
|
1162
756
|
|
|
1163
|
-
|
|
757
|
+
All responses from `SmartRegistry.handleRequest()` use the **Web Streams API**. The `body` field on `IResponse` is always a `ReadableStream<Uint8Array>` โ whether the content is a 2GB container image layer or a tiny JSON metadata response.
|
|
1164
758
|
|
|
1165
|
-
|
|
1166
|
-
{protocol}:{type}:{name}:{action}
|
|
759
|
+
### How It Works
|
|
1167
760
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
npm:*:*:* # Full NPM access
|
|
761
|
+
- **Binary downloads** (blobs, tarballs, .crate, .zip, .whl, .gem) stream directly from S3 to the response โ zero buffering in memory
|
|
762
|
+
- **JSON/metadata responses** are automatically wrapped into a `ReadableStream` at the API boundary
|
|
763
|
+
- **OCI chunked uploads** store each PATCH chunk as a temp S3 object instead of accumulating in memory, then stream-assemble during the final PUT with incremental SHA-256 verification
|
|
1172
764
|
|
|
1173
|
-
|
|
1174
|
-
oci:repository:*:push # Push any image
|
|
1175
|
-
oci:*:*:* # Full OCI access
|
|
765
|
+
### Stream Helpers
|
|
1176
766
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
maven:*:*:* # Full Maven access
|
|
1180
|
-
|
|
1181
|
-
cargo:crate:serde:write # Write serde crate
|
|
1182
|
-
cargo:crate:*:read # Read any crate
|
|
1183
|
-
cargo:*:*:* # Full Cargo access
|
|
767
|
+
```typescript
|
|
768
|
+
import { streamToBuffer, streamToJson, toReadableStream } from '@push.rocks/smartregistry';
|
|
1184
769
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
composer:*:*:* # Full Composer access
|
|
770
|
+
// Consume a stream into a Buffer
|
|
771
|
+
const buffer = await streamToBuffer(response.body);
|
|
1188
772
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
pypi:*:*:* # Full PyPI access
|
|
773
|
+
// Consume a stream into parsed JSON
|
|
774
|
+
const data = await streamToJson(response.body);
|
|
1192
775
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
rubygems:*:*:* # Full RubyGems access
|
|
776
|
+
// Create a ReadableStream from any data type
|
|
777
|
+
const stream = toReadableStream({ hello: 'world' });
|
|
1196
778
|
```
|
|
1197
779
|
|
|
1198
|
-
|
|
780
|
+
### Consuming in Node.js HTTP Servers
|
|
781
|
+
|
|
782
|
+
Since Node.js `http.ServerResponse` uses Node streams, bridge with `Readable.fromWeb()`:
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
import { Readable } from 'stream';
|
|
786
|
+
|
|
787
|
+
if (response.body) {
|
|
788
|
+
Readable.fromWeb(response.body).pipe(res);
|
|
789
|
+
} else {
|
|
790
|
+
res.end();
|
|
791
|
+
}
|
|
792
|
+
```
|
|
1199
793
|
|
|
1200
|
-
|
|
794
|
+
## ๐ Integration with Express
|
|
1201
795
|
|
|
1202
796
|
```typescript
|
|
1203
797
|
import express from 'express';
|
|
798
|
+
import { Readable } from 'stream';
|
|
1204
799
|
import { SmartRegistry } from '@push.rocks/smartregistry';
|
|
1205
800
|
|
|
1206
801
|
const app = express();
|
|
@@ -1217,16 +812,13 @@ app.all('*', async (req, res) => {
|
|
|
1217
812
|
});
|
|
1218
813
|
|
|
1219
814
|
res.status(response.status);
|
|
1220
|
-
Object.entries(response.headers)
|
|
815
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
1221
816
|
res.setHeader(key, value);
|
|
1222
|
-
}
|
|
817
|
+
}
|
|
1223
818
|
|
|
1224
819
|
if (response.body) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
} else {
|
|
1228
|
-
res.json(response.body);
|
|
1229
|
-
}
|
|
820
|
+
// All response bodies are ReadableStream<Uint8Array> โ pipe to HTTP response
|
|
821
|
+
Readable.fromWeb(response.body).pipe(res);
|
|
1230
822
|
} else {
|
|
1231
823
|
res.end();
|
|
1232
824
|
}
|
|
@@ -1235,110 +827,60 @@ app.all('*', async (req, res) => {
|
|
|
1235
827
|
app.listen(5000);
|
|
1236
828
|
```
|
|
1237
829
|
|
|
1238
|
-
##
|
|
1239
|
-
|
|
1240
|
-
```bash
|
|
1241
|
-
# Install dependencies
|
|
1242
|
-
pnpm install
|
|
1243
|
-
|
|
1244
|
-
# Build
|
|
1245
|
-
pnpm run build
|
|
1246
|
-
|
|
1247
|
-
# Test
|
|
1248
|
-
pnpm test
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
## ๐งช Testing with smarts3
|
|
1252
|
-
|
|
1253
|
-
smartregistry works seamlessly with [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3), a local S3-compatible server for testing. This allows you to test the registry without needing cloud credentials or external services.
|
|
830
|
+
## ๐งช Testing with smartstorage
|
|
1254
831
|
|
|
1255
|
-
|
|
832
|
+
smartregistry works seamlessly with [@push.rocks/smartstorage](https://code.foss.global/push.rocks/smartstorage), a local S3-compatible server for testing โ no cloud credentials needed.
|
|
1256
833
|
|
|
1257
834
|
```typescript
|
|
1258
|
-
import {
|
|
835
|
+
import { SmartStorage } from '@push.rocks/smartstorage';
|
|
1259
836
|
import { SmartRegistry } from '@push.rocks/smartregistry';
|
|
1260
837
|
|
|
1261
838
|
// Start local S3 server
|
|
1262
|
-
const s3Server = await
|
|
1263
|
-
server: { port: 3456 },
|
|
839
|
+
const s3Server = await SmartStorage.createAndStart({
|
|
840
|
+
server: { port: 3456, silent: true },
|
|
1264
841
|
storage: { cleanSlate: true },
|
|
1265
842
|
});
|
|
1266
843
|
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1269
|
-
const s3Descriptor = {
|
|
1270
|
-
endpoint: 'localhost',
|
|
1271
|
-
port: 3456,
|
|
1272
|
-
accessKey: 'test',
|
|
1273
|
-
accessSecret: 'test',
|
|
1274
|
-
useSsl: false,
|
|
1275
|
-
region: 'us-east-1',
|
|
1276
|
-
};
|
|
844
|
+
// Get S3 descriptor from the running server
|
|
845
|
+
const s3Descriptor = await s3Server.getStorageDescriptor();
|
|
1277
846
|
|
|
1278
|
-
// Create registry with smarts3 configuration
|
|
1279
847
|
const registry = new SmartRegistry({
|
|
1280
|
-
storage: {
|
|
1281
|
-
|
|
1282
|
-
bucketName: 'my-test-registry',
|
|
1283
|
-
},
|
|
1284
|
-
auth: {
|
|
1285
|
-
jwtSecret: 'test-secret',
|
|
1286
|
-
tokenStore: 'memory',
|
|
1287
|
-
npmTokens: { enabled: true },
|
|
1288
|
-
ociTokens: {
|
|
1289
|
-
enabled: true,
|
|
1290
|
-
realm: 'https://auth.example.com/token',
|
|
1291
|
-
service: 'my-registry',
|
|
1292
|
-
},
|
|
1293
|
-
},
|
|
848
|
+
storage: { ...s3Descriptor, bucketName: 'my-test-registry' },
|
|
849
|
+
auth: { jwtSecret: 'test', tokenStore: 'memory', npmTokens: { enabled: true } },
|
|
1294
850
|
npm: { enabled: true, basePath: '/npm' },
|
|
1295
851
|
oci: { enabled: true, basePath: '/oci' },
|
|
1296
|
-
pypi: { enabled: true, basePath: '/pypi' },
|
|
1297
|
-
cargo: { enabled: true, basePath: '/cargo' },
|
|
1298
852
|
});
|
|
1299
|
-
|
|
1300
853
|
await registry.init();
|
|
1301
854
|
|
|
1302
|
-
//
|
|
1303
|
-
// Your tests here
|
|
1304
|
-
|
|
1305
|
-
// Cleanup
|
|
855
|
+
// ... run your tests ...
|
|
1306
856
|
await s3Server.stop();
|
|
1307
857
|
```
|
|
1308
858
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
- โ
**Zero Setup** - No cloud credentials or external services needed
|
|
1312
|
-
- โ
**Fast** - Local filesystem storage, no network latency
|
|
1313
|
-
- โ
**Isolated** - Clean slate per test run, no shared state
|
|
1314
|
-
- โ
**CI/CD Ready** - Works in automated pipelines without configuration
|
|
1315
|
-
- โ
**Full Compatibility** - Implements S3 API, works with IS3Descriptor
|
|
1316
|
-
|
|
1317
|
-
### Running Integration Tests
|
|
859
|
+
## ๐ ๏ธ Development
|
|
1318
860
|
|
|
1319
861
|
```bash
|
|
1320
|
-
#
|
|
1321
|
-
pnpm
|
|
1322
|
-
|
|
1323
|
-
# Run all tests (includes smarts3)
|
|
1324
|
-
pnpm test
|
|
862
|
+
pnpm install # Install dependencies
|
|
863
|
+
pnpm run build # Build
|
|
864
|
+
pnpm test # Run all tests
|
|
1325
865
|
```
|
|
1326
866
|
|
|
1327
867
|
## License and Legal Information
|
|
1328
868
|
|
|
1329
|
-
This repository contains open-source code
|
|
869
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
1330
870
|
|
|
1331
871
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
1332
872
|
|
|
1333
873
|
### Trademarks
|
|
1334
874
|
|
|
1335
|
-
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein.
|
|
875
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
876
|
+
|
|
877
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
1336
878
|
|
|
1337
879
|
### Company Information
|
|
1338
880
|
|
|
1339
881
|
Task Venture Capital GmbH
|
|
1340
|
-
Registered at District
|
|
882
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
1341
883
|
|
|
1342
|
-
For any legal inquiries or
|
|
884
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
1343
885
|
|
|
1344
886
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|