2025-11-19 14:41:19 +00:00
# @push.rocks/smartregistry
2026-04-16 14:18:12 +00:00
> One TypeScript registry core for OCI, npm, Maven, Cargo, Composer, PyPI, and RubyGems.
`@push.rocks/smartregistry` is a composable library for building your own multi-protocol package registry. You hand it HTTP requests, it routes them to the right protocol handler, stores artifacts in S3-compatible object storage, enforces shared auth scopes, and can proxy/cache upstream registries when content is not local.
2025-11-21 17:13:06 +00:00
## Issue Reporting and Security
2025-11-27 21:11:04 +00:00
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/ ](https://community.foss.global/ ). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/ ](https://code.foss.global/ ) account to submit Pull Requests directly.
2025-11-19 14:41:19 +00:00
2026-04-16 14:18:12 +00:00
## Why It Hits
- One registry engine, seven ecosystems.
- Shared S3-backed storage instead of protocol-specific silos.
- Shared auth, token minting, and scope checks across protocols.
- Optional upstream proxying with retries, stale caching, negative caching, and circuit breakers.
- Clean public API: `handleRequest()` in, `IResponse` out.
## What It Actually Ships
| Protocol | Paths | What works | Auth style |
| --- | --- | --- | --- |
| OCI | `/oci/*` or `/v2/*` | version check, blobs, manifests, tags, referrers, deletes | Bearer JWT |
| npm | `/npm/*` | login, publish, packuments, version metadata, tarballs, dist-tags, search, token APIs, unpublish | Bearer token |
| Maven | `/maven/*` | POM/JAR/WAR upload and download, `maven-metadata.xml` , auto-generated checksums, delete | Bearer token or Basic auth with the token as password |
| Cargo | `/cargo/*` | sparse index, `config.json` , publish, download, search, yank, unyank | plain `Authorization` token |
| Composer | `/composer/*` | `packages.json` , `p2` metadata, ZIP dists, filtered package lists, upload, version delete, package delete | Bearer token, plus Basic auth for credential-backed reads |
| PyPI | `/simple/*` and `/pypi/*` | PEP 503 HTML, PEP 691 JSON, upload, JSON metadata API, downloads, package delete, version delete | Bearer token or Basic `__token__:<token>` |
| RubyGems | `/rubygems/*` | Compact Index, gem downloads, versions/dependencies JSON, specs endpoints, upload, yank, unyank | plain `Authorization` token |
Use `oci.basePath = '/v2'` if you want native Docker/OCI client compatibility. The default `/oci` path is fine for app-level routing, but Docker expects `/v2` .
Set `pypi.registryUrl` to the host root, not `/pypi` , because the Simple API lives at `/simple/*` while uploads and JSON endpoints live under `/pypi/*` .
## Install
2025-11-19 15:16:20 +00:00
``` bash
pnpm add @push.rocks/smartregistry
```
2026-04-16 14:18:12 +00:00
## Quick Start
``` ts
import { SmartRegistry , type IRegistryConfig } from '@push.rocks/smartregistry' ;
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
const publicUrl = 'https://registry.example.com' ;
2025-11-19 15:16:20 +00:00
const config : IRegistryConfig = {
storage : {
2026-04-16 14:18:12 +00:00
accessKey : process.env.S3_ACCESS_KEY ! ,
accessSecret : process.env.S3_ACCESS_SECRET ! ,
endpoint : 's3.example.com' ,
2025-11-19 15:16:20 +00:00
port : 443 ,
useSsl : true ,
2026-04-16 14:18:12 +00:00
region : 'eu-central-1' ,
bucketName : 'registry-artifacts' ,
2025-11-19 15:16:20 +00:00
} ,
2025-11-19 15:32:00 +00:00
auth : {
2026-04-16 14:18:12 +00:00
jwtSecret : process.env.REGISTRY_JWT_SECRET ! ,
2025-11-19 15:32:00 +00:00
tokenStore : 'memory' ,
npmTokens : { enabled : true } ,
ociTokens : {
enabled : true ,
2026-04-16 14:18:12 +00:00
realm : ` ${ publicUrl } /v2/token ` ,
service : 'smartregistry' ,
2025-11-19 15:32:00 +00:00
} ,
2026-04-16 14:18:12 +00:00
pypiTokens : { enabled : true } ,
rubygemsTokens : { enabled : true } ,
2025-11-19 15:32:00 +00:00
} ,
2026-04-16 14:18:12 +00:00
oci : { enabled : true , basePath : '/v2' } ,
npm : { enabled : true , basePath : '/npm' , registryUrl : ` ${ publicUrl } /npm ` } ,
maven : { enabled : true , basePath : '/maven' , registryUrl : ` ${ publicUrl } /maven ` } ,
cargo : { enabled : true , basePath : '/cargo' , registryUrl : ` ${ publicUrl } /cargo ` } ,
composer : { enabled : true , basePath : '/composer' , registryUrl : ` ${ publicUrl } /composer ` } ,
pypi : { enabled : true , basePath : '/pypi' , registryUrl : publicUrl } ,
rubygems : { enabled : true , basePath : '/rubygems' , registryUrl : ` ${ publicUrl } /rubygems ` } ,
2025-11-19 15:16:20 +00:00
} ;
const registry = new SmartRegistry ( config ) ;
await registry . init ( ) ;
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
const auth = registry . getAuthManager ( ) ;
const npmToken = await auth . createNpmToken ( 'ci-bot' ) ;
const cargoToken = await auth . createCargoToken ( 'ci-bot' ) ;
const ociToken = await auth . createOciToken ( 'ci-bot' , [ 'oci:repository:myorg/myapp:*' ] ) ;
2026-04-16 10:42:33 +00:00
2026-04-16 14:18:12 +00:00
console . log ( { npmToken , cargoToken , ociToken } ) ;
2025-11-21 09:13:02 +00:00
```
2026-04-16 14:18:12 +00:00
If you do not pass `authProvider` , the library uses `DefaultAuthProvider` , an in-memory reference implementation. That is perfect for tests and local dev, but you will usually want a real `IAuthProvider` in production.
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
## HTTP Integration
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
`SmartRegistry` is not a web framework. You own the HTTP server, request parsing, and response writing. The happy path is very small:
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
``` ts
import { createServer , type IncomingHttpHeaders } from 'node:http' ;
import { Readable } from 'node:stream' ;
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
function headersToRecord ( headers : IncomingHttpHeaders ) : Record < string , string > {
return Object . fromEntries (
Object . entries ( headers ) . map ( ( [ key , value ] ) = > [
key ,
Array . isArray ( value ) ? value . join ( ', ' ) : value ? ? '' ,
] )
) ;
2025-11-21 09:13:02 +00:00
}
2026-04-16 14:18:12 +00:00
createServer ( async ( req , res ) = > {
const url = new URL ( req . url ? ? '/' , 'http://localhost' ) ;
const headers = headersToRecord ( req . headers ) ;
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
const chunks : Buffer [ ] = [ ] ;
for await ( const chunk of req ) {
chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer.from ( chunk ) ) ;
}
const rawBody = Buffer . concat ( chunks ) ;
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
let body : unknown = rawBody . length ? rawBody : undefined ;
if ( ( headers [ 'content-type' ] ? ? '' ) . includes ( 'application/json' ) && rawBody . length ) {
body = JSON . parse ( rawBody . toString ( 'utf8' ) ) ;
}
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
const response = await registry . handleRequest ( {
method : req.method ? ? 'GET' ,
path : url.pathname ,
query : Object.fromEntries ( url . searchParams ) ,
headers ,
body ,
rawBody : rawBody.length ? rawBody : undefined ,
} ) ;
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
res . writeHead ( response . status , response . headers ) ;
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
if ( ! response . body ) {
res . end ( ) ;
return ;
}
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
Readable . fromWeb ( response . body ) . pipe ( res ) ;
} ) . listen ( 3000 ) ;
2025-11-21 17:13:06 +00:00
```
2026-04-16 14:18:12 +00:00
Keep `rawBody` for OCI manifests, OCI blobs, and any other digest-sensitive request where exact bytes matter.
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
For PyPI uploads, parse `multipart/form-data` before calling `handleRequest()` and pass the parsed fields in `context.body` . The library expects the upload form fields, not a raw multipart buffer.
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
At the public API boundary, `response.body` is always a `ReadableStream<Uint8Array>` .
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
## Core API
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
| API | Why you use it |
| --- | --- |
| `new SmartRegistry(config)` | build the registry orchestrator |
| `await registry.init()` | initialize storage, auth, and enabled protocols |
| `await registry.handleRequest(context)` | route one incoming HTTP request |
| `registry.getAuthManager()` | mint, validate, revoke, and authorize tokens |
| `registry.getStorage()` | reach the shared storage abstraction directly |
| `registry.getRegistry(protocol)` | access a specific protocol handler |
| `registry.destroy()` | clean up timers and protocol resources |
2025-11-21 17:13:06 +00:00
2026-04-16 14:18:12 +00:00
The package also exports the protocol-specific registry classes, upstream classes, `RegistryStorage` , `AuthManager` , `DefaultAuthProvider` , `StaticUpstreamProvider` , `UpstreamCache` , `CircuitBreaker` , and stream helpers such as `streamToBuffer()` and `streamToJson()` .
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
## Configuration Reference
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
| Key | Purpose |
| --- | --- |
| `storage` | S3-compatible backend config. This extends `IS3Descriptor` and adds `bucketName` . |
| `auth` | shared token settings across protocols |
| `authProvider` | plug in LDAP, OAuth, OIDC, custom DB-backed auth, or anything else implementing `IAuthProvider` |
| `storageHooks` | receive before/after put/get/delete callbacks with protocol, actor, package, and version context |
| `upstreamProvider` | decide per request which upstream registries to consult |
| `oci` / `npm` / `maven` / `cargo` / `composer` / `pypi` / `rubygems` | enable protocols, set base paths, and define the public registry URL they should emit |
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
## Upstream Proxying
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
If a package, image, crate, or artifact does not exist locally, a protocol handler can resolve an upstream config on the fly and fetch it from there.
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
``` ts
import {
StaticUpstreamProvider ,
} from '@push.rocks/smartregistry' ;
2025-11-27 21:11:04 +00:00
2026-03-24 22:59:37 +00:00
const upstreamProvider = new StaticUpstreamProvider ( {
2025-11-27 21:11:04 +00:00
npm : {
enabled : true ,
2026-03-24 22:59:37 +00:00
upstreams : [
{
id : 'npmjs' ,
2026-04-16 14:18:12 +00:00
name : 'npmjs' ,
2026-03-24 22:59:37 +00:00
url : 'https://registry.npmjs.org' ,
2026-04-16 14:18:12 +00:00
priority : 1 ,
2026-03-24 22:59:37 +00:00
enabled : true ,
2026-04-16 14:18:12 +00:00
auth : { type : 'none' } ,
2026-03-24 22:59:37 +00:00
} ,
] ,
2025-11-27 21:11:04 +00:00
} ,
oci : {
enabled : true ,
2026-03-24 22:59:37 +00:00
upstreams : [
2026-04-16 14:18:12 +00:00
{
id : 'dockerhub' ,
name : 'dockerhub' ,
url : 'https://registry-1.docker.io' ,
priority : 1 ,
enabled : true ,
auth : { type : 'none' } ,
} ,
2026-03-24 22:59:37 +00:00
] ,
2025-11-27 21:11:04 +00:00
} ,
2026-03-24 22:59:37 +00:00
} ) ;
2025-11-27 21:11:04 +00:00
```
2026-04-16 14:18:12 +00:00
Pass that provider as `upstreamProvider` in your `IRegistryConfig` .
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
The upstream layer supports scope rules, per-request routing, retries with backoff, circuit breakers, stale-while-revalidate caching, and negative caching for 404s.
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
## Custom Auth and Audit Hooks
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
Bring your own auth system by implementing `IAuthProvider` and passing it as `authProvider` .
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
``` ts
2025-11-27 21:11:04 +00:00
const registry = new SmartRegistry ( {
. . . config ,
2026-04-16 14:18:12 +00:00
authProvider : myAuthProvider ,
2025-11-27 21:11:04 +00:00
} ) ;
```
2026-04-16 14:18:12 +00:00
Use `storageHooks` when you need quota checks, audit logs, or side effects around artifact writes and deletes.
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
``` ts
const registry = new SmartRegistry ( {
. . . config ,
storageHooks : {
async beforePut ( context ) {
if ( ( context . metadata ? . size ? ? 0 ) > 500 * 1024 * 1024 ) {
return { allowed : false , reason : 'Artifact too large' } ;
2025-11-27 21:11:04 +00:00
}
2026-04-16 14:18:12 +00:00
return { allowed : true } ;
} ,
async afterPut ( context ) {
await auditLog ( 'storage.put' , context ) ;
} ,
2025-11-27 21:11:04 +00:00
} ,
2026-04-16 14:18:12 +00:00
} ) ;
2025-11-27 21:11:04 +00:00
```
2026-04-16 14:18:12 +00:00
`handleRequest()` also accepts an `actor` object. That extra context flows into storage hooks and upstream resolution, which is great for multi-tenant routing, org-aware policy checks, and audit trails.
2025-11-27 21:11:04 +00:00
2026-04-16 14:18:12 +00:00
``` ts
await registry . handleRequest ( {
method : 'GET' ,
path : '/npm/@acme/internal-lib' ,
headers : { authorization : ` Bearer ${ npmToken } ` } ,
2025-11-27 21:11:04 +00:00
query : { } ,
actor : {
2026-04-16 14:18:12 +00:00
orgId : 'acme' ,
sessionId : 'sess_123' ,
2025-11-27 21:11:04 +00:00
} ,
} ) ;
```
2026-04-16 14:18:12 +00:00
## Client Cheatsheet
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
### npm
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
``` ini
registry = https://registry.example.com/npm/
//registry.example.com/npm/:_authToken = <npm-token>
2025-11-19 15:16:20 +00:00
```
2026-04-16 14:18:12 +00:00
The npm handler also implements the npm-compatible login and token endpoints, including `PUT /-/user/org.couchdb.user:<name>` and `/-/npm/v1/tokens` .
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
### Docker / OCI
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
Set `oci.basePath` to `/v2` and expose a token endpoint that returns `token` , `access_token` , and `expires_in` .
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
``` ts
if ( url . pathname === '/v2/token' ) {
const token = await registry . getAuthManager ( ) . createOciToken (
'docker-user' ,
[ 'oci:repository:*:*' ] ,
3600 ,
) ;
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
res . setHeader ( 'Content-Type' , 'application/json' ) ;
res . end ( JSON . stringify ( {
token ,
access_token : token ,
expires_in : 3600 ,
} ) ) ;
return ;
2025-11-19 15:32:00 +00:00
}
2025-11-19 15:16:20 +00:00
```
2026-04-16 14:18:12 +00:00
``` bash
docker login registry.example.com
docker push registry.example.com/myorg/myimage:latest
docker pull registry.example.com/myorg/myimage:latest
2026-03-24 22:59:37 +00:00
```
2025-11-21 09:13:02 +00:00
2026-04-16 14:18:12 +00:00
### Maven
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
``` xml
<settings >
<servers >
<server >
<id > smartregistry</id>
<username > token</username>
<password > YOUR_MAVEN_TOKEN</password>
</server>
</servers>
</settings>
2025-11-19 15:32:00 +00:00
```
2026-04-16 14:18:12 +00:00
Point repositories or distribution management at `https://registry.example.com/maven` .
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
### Cargo
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
``` toml
# .cargo/config.toml
[ registries . smartregistry ]
index = "sparse+https://registry.example.com/cargo/"
2025-11-19 15:32:00 +00:00
```
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
``` toml
# .cargo/credentials.toml
[ registries . smartregistry ]
token = "YOUR_CARGO_TOKEN"
```
2026-03-24 22:59:37 +00:00
2026-04-16 14:18:12 +00:00
Cargo sends the token as a plain `Authorization` header, not `Bearer <token>` .
2026-03-24 22:59:37 +00:00
2026-04-16 14:18:12 +00:00
### Composer
2026-03-24 22:59:37 +00:00
2026-04-16 14:18:12 +00:00
``` json
{
"repositories" : [
{
"type" : "composer" ,
"url" : "https://registry.example.com/composer"
}
]
2026-03-24 22:59:37 +00:00
}
```
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
``` json
{
"http-basic" : {
"registry.example.com" : {
"username" : "my-user" ,
"password" : "my-password"
}
}
}
```
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
Composer installs can use Basic auth if your `authProvider.authenticate()` supports it. Programmatic writes use Bearer tokens cleanly.
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
### PyPI
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
``` ini
[distutils]
index-servers = smartregistry
2025-11-19 15:32:00 +00:00
2026-04-16 14:18:12 +00:00
[smartregistry]
repository = https://registry.example.com/pypi
username = __token__
password = YOUR_PYPI_TOKEN
2025-11-19 15:32:00 +00:00
```
2026-04-16 14:18:12 +00:00
``` bash
pip install --index-url https://registry.example.com/simple/ your-package
twine upload --repository smartregistry dist/*
```
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
### RubyGems
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
``` yaml
:rubygems_api_key : YOUR_RUBYGEMS_TOKEN
```
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
``` bash
gem push your-gem-1.0.0.gem --host https://registry.example.com/rubygems
bundle config set --global https://registry.example.com/rubygems YOUR_RUBYGEMS_TOKEN
```
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
## Testing and Compatibility
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
The repository contains protocol-level tests, cross-protocol integration tests, upstream provider tests, storage hook tests, and native-client test suites that exercise the library through real ecosystem tooling.
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
Native-client coverage exists for:
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
- Docker / OCI
- npm
- Maven (`mvn` )
- Cargo
- Composer
- PyPI (`pip` and `twine` )
- RubyGems (`gem` )
2025-11-24 00:15:29 +00:00
2026-04-16 14:18:12 +00:00
There are also integration tests for S3-compatible storage and `IS3Descriptor` -based configuration.
2025-11-24 00:15:29 +00:00
2025-11-20 19:46:34 +00:00
## License and Legal Information
2026-04-16 14:18:12 +00:00
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE ](./license ) file.
2025-11-20 19:46:34 +00:00
**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.
### Trademarks
2026-03-24 22:59:37 +00:00
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.
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.
2025-11-20 19:46:34 +00:00
### Company Information
2025-11-19 15:16:20 +00:00
2026-04-16 14:18:12 +00:00
Task Venture Capital GmbH
2026-03-24 22:59:37 +00:00
Registered at District Court Bremen HRB 35230 HB, Germany
2025-11-19 15:16:20 +00:00
2026-03-24 22:59:37 +00:00
For any legal inquiries or further information, please contact us via email at hello@task .vc.
2025-11-19 15:16:20 +00:00
2025-11-20 19:46:34 +00:00
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.