@push.rocks/smartregistry 2.5.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.
Files changed (52) hide show
  1. package/.smartconfig.json +24 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/cargo/classes.cargoregistry.d.ts +8 -3
  4. package/dist_ts/cargo/classes.cargoregistry.js +71 -33
  5. package/dist_ts/classes.smartregistry.js +48 -36
  6. package/dist_ts/composer/classes.composerregistry.d.ts +14 -3
  7. package/dist_ts/composer/classes.composerregistry.js +64 -28
  8. package/dist_ts/core/classes.registrystorage.d.ts +45 -0
  9. package/dist_ts/core/classes.registrystorage.js +116 -1
  10. package/dist_ts/core/helpers.stream.d.ts +20 -0
  11. package/dist_ts/core/helpers.stream.js +59 -0
  12. package/dist_ts/core/index.d.ts +1 -0
  13. package/dist_ts/core/index.js +3 -1
  14. package/dist_ts/core/interfaces.core.d.ts +28 -5
  15. package/dist_ts/maven/classes.mavenregistry.d.ts +14 -3
  16. package/dist_ts/maven/classes.mavenregistry.js +78 -27
  17. package/dist_ts/npm/classes.npmregistry.d.ts +14 -3
  18. package/dist_ts/npm/classes.npmregistry.js +104 -48
  19. package/dist_ts/oci/classes.ociregistry.d.ts +19 -3
  20. package/dist_ts/oci/classes.ociregistry.js +186 -73
  21. package/dist_ts/oci/classes.ociupstream.d.ts +5 -2
  22. package/dist_ts/oci/classes.ociupstream.js +17 -10
  23. package/dist_ts/oci/interfaces.oci.d.ts +4 -0
  24. package/dist_ts/pypi/classes.pypiregistry.d.ts +8 -3
  25. package/dist_ts/pypi/classes.pypiregistry.js +88 -50
  26. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +8 -3
  27. package/dist_ts/rubygems/classes.rubygemsregistry.js +61 -23
  28. package/dist_ts/rubygems/helpers.rubygems.js +3 -3
  29. package/dist_ts/upstream/classes.upstreamcache.js +2 -2
  30. package/dist_ts/upstream/interfaces.upstream.d.ts +72 -1
  31. package/dist_ts/upstream/interfaces.upstream.js +24 -1
  32. package/package.json +24 -20
  33. package/readme.md +354 -812
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/cargo/classes.cargoregistry.ts +84 -37
  36. package/ts/classes.smartregistry.ts +49 -35
  37. package/ts/composer/classes.composerregistry.ts +74 -30
  38. package/ts/core/classes.registrystorage.ts +133 -2
  39. package/ts/core/helpers.stream.ts +63 -0
  40. package/ts/core/index.ts +3 -0
  41. package/ts/core/interfaces.core.ts +29 -5
  42. package/ts/maven/classes.mavenregistry.ts +89 -28
  43. package/ts/npm/classes.npmregistry.ts +118 -49
  44. package/ts/oci/classes.ociregistry.ts +205 -77
  45. package/ts/oci/classes.ociupstream.ts +18 -8
  46. package/ts/oci/interfaces.oci.ts +4 -0
  47. package/ts/pypi/classes.pypiregistry.ts +100 -54
  48. package/ts/rubygems/classes.rubygemsregistry.ts +69 -24
  49. package/ts/rubygems/helpers.rubygems.ts +2 -2
  50. package/ts/upstream/classes.upstreamcache.ts +1 -1
  51. package/ts/upstream/interfaces.upstream.ts +82 -1
  52. 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** for building unified container and package registries.
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 using [@push.rocks/smartbucket](https://www.npmjs.com/package/@push.rocks/smartbucket) with standardized `IS3Descriptor` from [@tsclass/tsclass](https://www.npmjs.com/package/@tsclass/tsclass)
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/*` 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
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 via async callbacks
31
-
32
- ### ๐Ÿ“ฆ Comprehensive Feature Set
33
-
34
- **OCI Features:**
35
- - โœ… Pull operations (manifests, blobs)
36
- - โœ… Push operations (chunked uploads)
37
- - โœ… Content discovery (tags, referrers API)
38
- - โœ… Content management (deletion)
39
-
40
- **NPM Features:**
41
- - โœ… Package publish/unpublish
42
- - โœ… Package download (tarballs)
43
- - โœ… Metadata & search
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 with URL-based cache paths
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
- oci: {
134
- enabled: true,
135
- basePath: '/oci',
136
- },
137
- npm: {
138
- enabled: true,
139
- basePath: '/npm',
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 requests
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 an image
169
+ // Pull a manifest
227
170
  const response = await registry.handleRequest({
228
171
  method: 'GET',
229
- path: '/oci/v2/library/nginx/manifests/latest',
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/v2/myapp/blobs/uploads/',
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/v2/myapp/blobs/uploads/${uploadId}`,
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 (Packages)
196
+ ### ๐Ÿ“ฆ NPM Registry
256
197
 
257
198
  ```typescript
258
- // Install a package (get metadata)
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 searchResults = await registry.handleRequest({
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.json (required for Cargo)
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>' }, // No "Bearer" prefix
251
+ headers: { 'Authorization': '<cargo-token>' },
337
252
  query: {},
338
- body: binaryPublishData, // Length-prefixed binary format
253
+ body: binaryPublishData,
339
254
  });
340
255
 
341
- // Yank a version (deprecate without deleting)
342
- const yankResponse = await registry.handleRequest({
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 (packages.json)
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
- // Get package metadata
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': `Bearer <composer-token>` },
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 format)
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 format)
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 Python package (wheel or sdist)
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/legacy/',
335
+ path: '/pypi/',
500
336
  headers: {
501
- 'Authorization': `Bearer <pypi-token>`,
337
+ 'Authorization': 'Bearer <pypi-token>',
502
338
  'Content-Type': 'multipart/form-data',
503
339
  },
504
340
  query: {},
505
- body: formData,
506
- });
507
-
508
- // Get package metadata (PyPI JSON API)
509
- const metadata = await registry.handleRequest({
510
- method: 'GET',
511
- path: '/pypi/my-package/json',
512
- headers: {},
513
- query: {},
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 (Ruby Gems)
360
+ ### ๐Ÿ’Ž RubyGems Registry
540
361
 
541
362
  ```typescript
542
- // Get versions file (compact index)
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
- // Yank a version (make unavailable for install)
577
- const yank = await registry.handleRequest({
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/gems/rails-7.0.0.gem',
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 NPM token
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
- // Create OCI token with scopes
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, IRegistryConfig } from '@push.rocks/smartregistry';
419
+ import { SmartRegistry, StaticUpstreamProvider } from '@push.rocks/smartregistry';
668
420
 
669
- const config: IRegistryConfig = {
670
- storage: { /* S3 config */ },
671
- auth: { /* Auth config */ },
421
+ const upstreamProvider = new StaticUpstreamProvider({
672
422
  npm: {
673
423
  enabled: true,
674
- basePath: '/npm',
675
- upstream: {
676
- enabled: true,
677
- upstreams: [
678
- {
679
- id: 'company-private',
680
- name: 'Company Private NPM',
681
- url: 'https://npm.internal.company.com',
682
- priority: 1, // Lower = higher priority
683
- enabled: true,
684
- scopeRules: [
685
- { pattern: '@company/*', action: 'include' },
686
- { pattern: '@internal/*', action: 'include' },
687
- ],
688
- auth: { type: 'bearer', token: process.env.NPM_PRIVATE_TOKEN },
689
- },
690
- {
691
- id: 'npmjs',
692
- name: 'NPM Public Registry',
693
- url: 'https://registry.npmjs.org',
694
- priority: 10,
695
- enabled: true,
696
- scopeRules: [
697
- { pattern: '@company/*', action: 'exclude' },
698
- { pattern: '@internal/*', action: 'exclude' },
699
- ],
700
- auth: { type: 'none' },
701
- cache: { defaultTtlSeconds: 300 },
702
- resilience: { timeoutMs: 30000, maxRetries: 3 },
703
- },
704
- ],
705
- cache: { enabled: true, staleWhileRevalidate: true },
706
- },
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
- basePath: '/oci',
711
- upstream: {
712
- enabled: true,
713
- upstreams: [
714
- {
715
- id: 'dockerhub',
716
- name: 'Docker Hub',
717
- url: 'https://registry-1.docker.io',
718
- priority: 1,
719
- enabled: true,
720
- auth: { type: 'none' },
721
- },
722
- {
723
- id: 'ghcr',
724
- name: 'GitHub Container Registry',
725
- url: 'https://ghcr.io',
726
- priority: 2,
727
- enabled: true,
728
- scopeRules: [{ pattern: 'ghcr.io/*', action: 'include' }],
729
- auth: { type: 'bearer', token: process.env.GHCR_TOKEN },
730
- },
731
- ],
732
- },
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
- // Requests for @company/* packages go to private registry
740
- // Other packages proxy through to npmjs.org with caching
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, ICredentials, TRegistryProtocol } from '@push.rocks/smartregistry';
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
- constructor(private ldapClient: LdapClient) {}
466
+ async init() { /* connect to LDAP */ }
751
467
 
752
- async authenticate(credentials: ICredentials): Promise<string | null> {
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
- if (!session) return null;
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?: ITokenOptions): Promise<string> {
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): Promise<void> {
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): Promise<boolean> {
779
- if (!token) return action === 'read'; // Anonymous read-only
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(ldapClient),
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 through requests for audit/quota tracking
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
- The storage configuration extends `IS3Descriptor` from `@tsclass/tsclass` for standardized S3 configuration:
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 (e.g., 'us-east-1')
896
- bucketName: string; // Bucket name for this registry
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; // Secret for signing JWTs
578
+ jwtSecret: string;
905
579
  tokenStore: 'memory' | 'redis' | 'database';
906
- npmTokens: {
907
- enabled: boolean;
908
- defaultReadonly?: boolean;
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
- ```typescript
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
- npm?: {
591
+ ```typescript
592
+ {
931
593
  enabled: boolean;
932
- basePath: string; // Default: '/npm'
933
- features?: {
934
- publish?: boolean;
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 class that routes requests to appropriate protocol handlers.
948
-
949
- **Methods:**
950
- - `init()` - Initialize the registry
951
- - `handleRequest(context)` - Handle HTTP request
952
- - `getStorage()` - Get storage instance
953
- - `getAuthManager()` - Get auth manager
954
- - `getRegistry(protocol)` - Get protocol handler
955
-
956
- #### RegistryStorage
957
-
958
- Unified storage abstraction for both OCI and NPM content.
959
-
960
- **OCI Methods:**
961
- - `getOciBlob(digest)` - Get blob
962
- - `putOciBlob(digest, data)` - Store blob
963
- - `getOciManifest(repo, digest)` - Get manifest
964
- - `putOciManifest(repo, digest, data, type)` - Store manifest
965
-
966
- **NPM Methods:**
967
- - `getNpmPackument(name)` - Get package metadata
968
- - `putNpmPackument(name, data)` - Store package metadata
969
- - `getNpmTarball(name, version)` - Get tarball
970
- - `putNpmTarball(name, version, data)` - Store tarball
971
-
972
- **PyPI Methods:**
973
- - `getPypiPackageMetadata(name)` - Get package metadata
974
- - `putPypiPackageMetadata(name, data)` - Store package metadata
975
- - `getPypiPackageFile(name, filename)` - Get package file
976
- - `putPypiPackageFile(name, filename, data)` - Store package file
977
-
978
- **RubyGems Methods:**
979
- - `getRubyGemsVersions()` - Get versions index
980
- - `putRubyGemsVersions(data)` - Store versions index
981
- - `getRubyGemsInfo(gemName)` - Get gem info
982
- - `putRubyGemsInfo(gemName, data)` - Store gem info
983
- - `getRubyGem(gemName, version)` - Get .gem file
984
- - `putRubyGem(gemName, version, data)` - Store .gem file
985
-
986
- #### AuthManager
987
-
988
- Unified authentication manager supporting both NPM and OCI authentication schemes.
989
-
990
- **Methods:**
991
- - `authenticate(credentials)` - Validate user credentials
992
- - `createNpmToken(userId, readonly)` - Create NPM token
993
- - `createOciToken(userId, scopes, expiresIn)` - Create OCI JWT
994
- - `validateToken(token, protocol)` - Validate any token
995
- - `authorize(token, resource, action)` - Check permissions
996
-
997
- ### Protocol Handlers
998
-
999
- #### OciRegistry
1000
-
1001
- OCI Distribution Specification v1.1 compliant registry.
1002
-
1003
- **Endpoints:**
1004
- - `GET /v2/` - Version check
1005
- - `GET /v2/{name}/manifests/{ref}` - Get manifest
1006
- - `PUT /v2/{name}/manifests/{ref}` - Push manifest
1007
- - `GET /v2/{name}/blobs/{digest}` - Get blob
1008
- - `POST /v2/{name}/blobs/uploads/` - Initiate upload
1009
- - `PUT /v2/{name}/blobs/uploads/{uuid}` - Complete upload
1010
- - `GET /v2/{name}/tags/list` - List tags
1011
- - `GET /v2/{name}/referrers/{digest}` - Get referrers
1012
-
1013
- #### NpmRegistry
1014
-
1015
- NPM registry API compliant implementation.
1016
-
1017
- **Endpoints:**
1018
- - `GET /{package}` - Get package metadata
1019
- - `PUT /{package}` - Publish package
1020
- - `GET /{package}/-/{tarball}` - Download tarball
1021
- - `GET /-/v1/search` - Search packages
1022
- - `PUT /-/user/org.couchdb.user:{user}` - Login
1023
- - `GET /-/npm/v1/tokens` - List tokens
1024
- - `POST /-/npm/v1/tokens` - Create token
1025
- - `PUT /-/package/{pkg}/dist-tags/{tag}` - Update tag
1026
-
1027
- #### CargoRegistry
1028
-
1029
- Cargo/crates.io registry with sparse HTTP protocol support.
1030
-
1031
- **Endpoints:**
1032
- - `GET /config.json` - Registry configuration (sparse protocol)
1033
- - `GET /index/{path}` - Index files (hierarchical structure)
1034
- - `/1/{name}` - 1-character crate names
1035
- - `/2/{name}` - 2-character crate names
1036
- - `/3/{c}/{name}` - 3-character crate names
1037
- - `/{p1}/{p2}/{name}` - 4+ character crate names
1038
- - `PUT /api/v1/crates/new` - Publish crate (binary format)
1039
- - `GET /api/v1/crates/{crate}/{version}/download` - Download .crate file
1040
- - `DELETE /api/v1/crates/{crate}/{version}/yank` - Yank (deprecate) version
1041
- - `PUT /api/v1/crates/{crate}/{version}/unyank` - Unyank version
1042
- - `GET /api/v1/crates?q={query}` - Search crates
1043
-
1044
- **Index Format:**
1045
- - Newline-delimited JSON (one line per version)
1046
- - SHA256 checksums for .crate files
1047
- - Yanked flag (keep files, mark unavailable)
1048
-
1049
- #### ComposerRegistry
1050
-
1051
- Composer v2 repository API compliant implementation.
1052
-
1053
- **Endpoints:**
1054
- - `GET /packages.json` - Repository metadata and configuration
1055
- - `GET /p2/{vendor}/{package}.json` - Package version metadata
1056
- - `GET /p2/{vendor}/{package}~dev.json` - Dev versions metadata
1057
- - `GET /packages/list.json` - List all packages
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
- โ”‚ โ”‚ โ””โ”€โ”€ sha256/{hash}
1110
- โ”‚ โ”œโ”€โ”€ manifests/
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
- โ”‚ โ”œโ”€โ”€ packages/
1116
- โ”‚ โ”‚ โ”œโ”€โ”€ {name}/
1117
- โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.json # Packument
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
- โ”‚ โ”‚ โ””โ”€โ”€ {group-path}/{artifact}/{version}/
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 # Registry configuration (sparse protocol)
1134
- โ”‚ โ”œโ”€โ”€ index/ # Hierarchical index structure
1135
- โ”‚ โ”‚ โ”œโ”€โ”€ 1/{name} # 1-char crate names (e.g., "a")
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
- โ”‚ โ””โ”€โ”€ {vendor}/{package}/
1144
- โ”‚ โ”œโ”€โ”€ metadata.json # All versions metadata
1145
- โ”‚ โ””โ”€โ”€ {reference}.zip # Package ZIP files
740
+ โ”‚ โ””โ”€โ”€ packages/{vendor}/{package}/
741
+ โ”‚ โ”œโ”€โ”€ metadata.json
742
+ โ”‚ โ””โ”€โ”€ {reference}.zip
1146
743
  โ”œโ”€โ”€ pypi/
1147
- โ”‚ โ”œโ”€โ”€ simple/ # PEP 503 HTML files
1148
- โ”‚ โ”‚ โ”œโ”€โ”€ index.html # All packages list
1149
- โ”‚ โ”‚ โ””โ”€โ”€ {package}/index.html # Package versions list
1150
- โ”‚ โ”œโ”€โ”€ packages/
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 # Master versions file
1156
- โ”œโ”€โ”€ info/{gemname} # Per-gem info files
1157
- โ”œโ”€โ”€ names # All gem names
1158
- โ””โ”€โ”€ gems/{gemname}-{version}.gem # .gem files
749
+ โ”œโ”€โ”€ versions
750
+ โ”œโ”€โ”€ info/{gemname}
751
+ โ”œโ”€โ”€ names
752
+ โ””โ”€โ”€ gems/{gemname}-{version}.gem
1159
753
  ```
1160
754
 
1161
- ## ๐ŸŽฏ Scope Format
755
+ ## ๐ŸŒŠ Streaming Architecture
1162
756
 
1163
- Unified scope format across protocols:
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
- Examples:
1169
- npm:package:express:read # Read express package
1170
- npm:package:*:write # Write any package
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
- oci:repository:nginx:pull # Pull nginx image
1174
- oci:repository:*:push # Push any image
1175
- oci:*:*:* # Full OCI access
765
+ ### Stream Helpers
1176
766
 
1177
- maven:artifact:com.example:read # Read Maven artifact
1178
- maven:artifact:*:write # Write any artifact
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
- composer:package:vendor/package:read # Read Composer package
1186
- composer:package:*:write # Write any package
1187
- composer:*:*:* # Full Composer access
770
+ // Consume a stream into a Buffer
771
+ const buffer = await streamToBuffer(response.body);
1188
772
 
1189
- pypi:package:my-package:read # Read PyPI package
1190
- pypi:package:*:write # Write any package
1191
- pypi:*:*:* # Full PyPI access
773
+ // Consume a stream into parsed JSON
774
+ const data = await streamToJson(response.body);
1192
775
 
1193
- rubygems:gem:rails:read # Read RubyGems gem
1194
- rubygems:gem:*:write # Write any gem
1195
- rubygems:*:*:* # Full RubyGems access
776
+ // Create a ReadableStream from any data type
777
+ const stream = toReadableStream({ hello: 'world' });
1196
778
  ```
1197
779
 
1198
- ## ๐Ÿ”Œ Integration Examples
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
- ### Express Server
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).forEach(([key, value]) => {
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
- if (Buffer.isBuffer(response.body)) {
1226
- res.send(response.body);
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
- ## ๐Ÿ› ๏ธ Development
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
- ### Quick Start with smarts3
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 { Smarts3 } from '@push.rocks/smarts3';
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 Smarts3.createAndStart({
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
- // Manually create IS3Descriptor matching smarts3 configuration
1268
- // Note: smarts3 v5.1.0 doesn't properly expose getS3Descriptor() yet
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
- ...s3Descriptor,
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
- // Use registry...
1303
- // Your tests here
1304
-
1305
- // Cleanup
855
+ // ... run your tests ...
1306
856
  await s3Server.stop();
1307
857
  ```
1308
858
 
1309
- ### Benefits of Testing with smarts3
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
- # Run smarts3 integration test
1321
- pnpm exec tstest test/test.integration.smarts3.node.ts --verbose
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 that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
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. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
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 court Bremen HRB 35230 HB, Germany
882
+ Registered at District Court Bremen HRB 35230 HB, Germany
1341
883
 
1342
- For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
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.