fix(registry): restore protocol routing and test coverage for npm, oci, and api flows
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-22 - 1.8.5 - fix(registry)
|
||||||
|
restore protocol routing and test coverage for npm, oci, and api flows
|
||||||
|
|
||||||
|
- initialize and route REST API requests through ApiRouter alongside smartregistry
|
||||||
|
- add org-aware npm path handling and OCI bearer token endpoint support in the registry server
|
||||||
|
- enforce API token scopes in the auth provider instead of allowing all authenticated writes
|
||||||
|
- start the test server in integration and e2e suites and update assertions to match current API responses
|
||||||
|
- fix npm unpublish and OCI image test flows, including docker build loading and storage cleanup
|
||||||
|
|
||||||
## 2026-03-21 - 1.8.4 - fix(deps)
|
## 2026-03-21 - 1.8.4 - fix(deps)
|
||||||
bump @stack.gallery/catalog to ^1.0.2 and remove committed test fixture auth token
|
bump @stack.gallery/catalog to ^1.0.2 and remove committed test fixture auth token
|
||||||
|
|
||||||
|
|||||||
10
deno.lock
generated
10
deno.lock
generated
@@ -43,7 +43,7 @@
|
|||||||
"npm:@push.rocks/smartrx@^3.0.10": "3.0.10",
|
"npm:@push.rocks/smartrx@^3.0.10": "3.0.10",
|
||||||
"npm:@push.rocks/smartstring@^4.1.0": "4.1.0",
|
"npm:@push.rocks/smartstring@^4.1.0": "4.1.0",
|
||||||
"npm:@push.rocks/smartunique@^3.0.9": "3.0.9",
|
"npm:@push.rocks/smartunique@^3.0.9": "3.0.9",
|
||||||
"npm:@stack.gallery/catalog@^1.0.1": "1.0.1",
|
"npm:@stack.gallery/catalog@^1.0.2": "1.0.2",
|
||||||
"npm:@tsclass/tsclass@^9.5.0": "9.5.0"
|
"npm:@tsclass/tsclass@^9.5.0": "9.5.0"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
@@ -3037,15 +3037,15 @@
|
|||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"tarball": "https://verdaccio.lossless.digital/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz"
|
||||||
},
|
},
|
||||||
"@stack.gallery/catalog@1.0.1": {
|
"@stack.gallery/catalog@1.0.2": {
|
||||||
"integrity": "sha512-9wlSACeahEVWKTqcLKQq/kbjOz7p0v5l5NQ0UbhFMYcpFtcGx2mZMHGdSUNrHUHQh7zUDiU5qW6E8GlLJBP43A==",
|
"integrity": "sha512-alPyu2YwpIwaM0hYcLnW05PcCYishWDitYVWDkk7+HDcy3q32LGizkS0eUu/l7Za6w/to06OXGgEmZMhZY4nuQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@design.estate/dees-catalog",
|
"@design.estate/dees-catalog",
|
||||||
"@design.estate/dees-domtools",
|
"@design.estate/dees-domtools",
|
||||||
"@design.estate/dees-element",
|
"@design.estate/dees-element",
|
||||||
"@design.estate/dees-wcctools"
|
"@design.estate/dees-wcctools"
|
||||||
],
|
],
|
||||||
"tarball": "https://verdaccio.lossless.digital/@stack.gallery/catalog/-/catalog-1.0.1.tgz"
|
"tarball": "https://verdaccio.lossless.digital/@stack.gallery/catalog/-/catalog-1.0.2.tgz"
|
||||||
},
|
},
|
||||||
"@tempfix/idb@8.0.3": {
|
"@tempfix/idb@8.0.3": {
|
||||||
"integrity": "sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==",
|
"integrity": "sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==",
|
||||||
@@ -6808,7 +6808,7 @@
|
|||||||
"npm:@git.zone/tsdeno@^1.2.0",
|
"npm:@git.zone/tsdeno@^1.2.0",
|
||||||
"npm:@git.zone/tswatch@^3.1.0",
|
"npm:@git.zone/tswatch@^3.1.0",
|
||||||
"npm:@push.rocks/smartguard@^3.1.0",
|
"npm:@push.rocks/smartguard@^3.1.0",
|
||||||
"npm:@stack.gallery/catalog@^1.0.1"
|
"npm:@stack.gallery/catalog@^1.0.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
readme.md
118
readme.md
@@ -6,11 +6,7 @@ all behind a single binary with a modern web UI.
|
|||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit
|
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.
|
||||||
[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.
|
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@@ -22,7 +18,7 @@ Requests directly.
|
|||||||
- 🛡️ **RBAC Permissions** — Reader → Developer → Maintainer → Admin per repository
|
- 🛡️ **RBAC Permissions** — Reader → Developer → Maintainer → Admin per repository
|
||||||
- 🔍 **Upstream Caching** — Transparently proxy and cache packages from public registries
|
- 🔍 **Upstream Caching** — Transparently proxy and cache packages from public registries
|
||||||
- 📊 **Audit Logging** — Full audit trail on every action for compliance
|
- 📊 **Audit Logging** — Full audit trail on every action for compliance
|
||||||
- 🎨 **Modern Web UI** — Angular 19 dashboard with Tailwind CSS, embedded in the binary
|
- 🎨 **Modern Web UI** — Web Components dashboard built with [`@design.estate/dees-catalog`](https://code.foss.global/design.estate/dees-catalog), bundled into the binary
|
||||||
- ⚡ **Single Binary** — Cross-compiled with `deno compile` for Linux and macOS (x64 + ARM64)
|
- ⚡ **Single Binary** — Cross-compiled with `deno compile` for Linux and macOS (x64 + ARM64)
|
||||||
- 🗄️ **MongoDB + S3** — Metadata in MongoDB, artifacts in any S3-compatible store
|
- 🗄️ **MongoDB + S3** — Metadata in MongoDB, artifacts in any S3-compatible store
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ Requests directly.
|
|||||||
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash
|
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash
|
||||||
|
|
||||||
# Install specific version
|
# Install specific version
|
||||||
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --version v1.4.0
|
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --version v1.8.0
|
||||||
|
|
||||||
# Install + set up systemd service
|
# Install + set up systemd service
|
||||||
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --setup-service
|
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --setup-service
|
||||||
@@ -60,6 +56,9 @@ The installer:
|
|||||||
git clone https://code.foss.global/stack.gallery/registry.git
|
git clone https://code.foss.global/stack.gallery/registry.git
|
||||||
cd registry
|
cd registry
|
||||||
|
|
||||||
|
# Install Node dependencies (for tsbundle/tsdeno build tools)
|
||||||
|
pnpm install
|
||||||
|
|
||||||
# Development mode (hot reload, reads .nogit/env.json)
|
# Development mode (hot reload, reads .nogit/env.json)
|
||||||
deno task dev
|
deno task dev
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ manager at the registry:
|
|||||||
|
|
||||||
| Protocol | Paths | Client Config Example |
|
| Protocol | Paths | Client Config Example |
|
||||||
| -------------- | --------------------------- | ------------------------------------------------------ |
|
| -------------- | --------------------------- | ------------------------------------------------------ |
|
||||||
| **NPM** | `/-/*`, `/@scope/*` | `npm config set registry http://registry:3000` |
|
| **NPM** | `/-/npm/{org}/*` | `npm config set registry http://registry:3000/-/npm/myorg/` |
|
||||||
| **OCI/Docker** | `/v2/*` | `docker login registry:3000` |
|
| **OCI/Docker** | `/v2/*` | `docker login registry:3000` |
|
||||||
| **Maven** | `/maven2/*` | Add repository URL in `pom.xml` |
|
| **Maven** | `/maven2/*` | Add repository URL in `pom.xml` |
|
||||||
| **Cargo** | `/api/v1/crates/*` | Configure in `.cargo/config.toml` |
|
| **Cargo** | `/api/v1/crates/*` | Configure in `.cargo/config.toml` |
|
||||||
@@ -125,6 +124,34 @@ manager at the registry:
|
|||||||
Authentication works with **Bearer tokens** (API tokens prefixed `srg_`) and **Basic auth**
|
Authentication works with **Bearer tokens** (API tokens prefixed `srg_`) and **Basic auth**
|
||||||
(email:password or username:token).
|
(email:password or username:token).
|
||||||
|
|
||||||
|
### NPM Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure npm to use your org's registry
|
||||||
|
npm config set @myorg:registry http://localhost:3000/-/npm/myorg/
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
echo "//localhost:3000/-/npm/myorg/:_authToken=srg_YOUR_TOKEN" >> ~/.npmrc
|
||||||
|
|
||||||
|
# Publish & install as usual
|
||||||
|
npm publish
|
||||||
|
npm install @myorg/my-package
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker/OCI Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login
|
||||||
|
docker login localhost:3000
|
||||||
|
|
||||||
|
# Tag and push
|
||||||
|
docker tag myimage:latest localhost:3000/myorg/myimage:1.0.0
|
||||||
|
docker push localhost:3000/myorg/myimage:1.0.0
|
||||||
|
|
||||||
|
# Pull
|
||||||
|
docker pull localhost:3000/myorg/myimage:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
## 🔐 Authentication & Security
|
## 🔐 Authentication & Security
|
||||||
|
|
||||||
### Local Auth
|
### Local Auth
|
||||||
@@ -265,11 +292,10 @@ All management endpoints live under `/api/v1/`. Authenticated via
|
|||||||
registry/
|
registry/
|
||||||
├── mod.ts # Deno entry point
|
├── mod.ts # Deno entry point
|
||||||
├── deno.json # Deno config, tasks, imports
|
├── deno.json # Deno config, tasks, imports
|
||||||
|
├── package.json # Node deps (tsbundle, tsdeno, tswatch)
|
||||||
├── npmextra.json # tsdeno compile targets & gitzone config
|
├── npmextra.json # tsdeno compile targets & gitzone config
|
||||||
├── install.sh # Binary installer script
|
├── install.sh # Binary installer script
|
||||||
├── .gitea/workflows/ # CI release pipeline
|
├── .gitea/workflows/ # CI release pipeline
|
||||||
├── scripts/
|
|
||||||
│ └── bundle-ui.ts # Embeds Angular build as base64 TypeScript
|
|
||||||
├── ts/
|
├── ts/
|
||||||
│ ├── registry.ts # StackGalleryRegistry — main orchestrator
|
│ ├── registry.ts # StackGalleryRegistry — main orchestrator
|
||||||
│ ├── cli.ts # CLI commands (smartcli)
|
│ ├── cli.ts # CLI commands (smartcli)
|
||||||
@@ -277,6 +303,7 @@ registry/
|
|||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── router.ts # REST API router with JWT/token auth
|
│ │ ├── router.ts # REST API router with JWT/token auth
|
||||||
│ │ └── handlers/ # auth, user, org, repo, package, token, audit, oauth, admin
|
│ │ └── handlers/ # auth, user, org, repo, package, token, audit, oauth, admin
|
||||||
|
│ ├── opsserver/ # TypedRequest RPC handlers
|
||||||
│ ├── models/ # MongoDB models via @push.rocks/smartdata
|
│ ├── models/ # MongoDB models via @push.rocks/smartdata
|
||||||
│ │ ├── user.ts, organization.ts, team.ts
|
│ │ ├── user.ts, organization.ts, team.ts
|
||||||
│ │ ├── repository.ts, package.ts
|
│ │ ├── repository.ts, package.ts
|
||||||
@@ -294,29 +321,25 @@ registry/
|
|||||||
│ │ ├── auth.provider.ts # IAuthProvider implementation
|
│ │ ├── auth.provider.ts # IAuthProvider implementation
|
||||||
│ │ └── storage.provider.ts # IStorageHooks for quota/audit
|
│ │ └── storage.provider.ts # IStorageHooks for quota/audit
|
||||||
│ └── interfaces/ # TypeScript interfaces & types
|
│ └── interfaces/ # TypeScript interfaces & types
|
||||||
├── ts_interfaces/ # Shared API contract (TypedRequest interfaces)
|
└── ts_interfaces/ # Shared API contract (TypedRequest interfaces)
|
||||||
│ ├── data/ # Data types (auth, org, repo, package, token, audit, admin)
|
├── data/ # Data types (auth, org, repo, package, token, audit, admin)
|
||||||
│ └── requests/ # Request/response interfaces for all API endpoints
|
└── requests/ # Request/response interfaces for all API endpoints
|
||||||
└── ui/ # Angular 19 + Tailwind CSS frontend
|
|
||||||
└── src/app/
|
|
||||||
├── features/ # Login, dashboard, orgs, repos, packages, tokens, admin
|
|
||||||
├── core/ # Services, guards, interceptors
|
|
||||||
└── shared/ # Layout, UI components
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Technology Stack
|
## 🔧 Technology Stack
|
||||||
|
|
||||||
| Component | Technology |
|
| Component | Technology |
|
||||||
| ----------------- | ------------------------------------------------------------------------------------ |
|
| ----------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| **Runtime** | Deno 2.x |
|
| **Runtime** | Deno 2.x |
|
||||||
| **Language** | TypeScript (strict mode) |
|
| **Language** | TypeScript (strict mode) |
|
||||||
| **Database** | MongoDB via [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) |
|
| **Database** | MongoDB via [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) |
|
||||||
| **Storage** | S3 via [`@push.rocks/smartbucket`](https://code.foss.global/push.rocks/smartbucket) |
|
| **Storage** | S3 via [`@push.rocks/smartbucket`](https://code.foss.global/push.rocks/smartbucket) |
|
||||||
| **Registry Core** | [`@push.rocks/smartregistry`](https://code.foss.global/push.rocks/smartregistry) |
|
| **Registry Core** | [`@push.rocks/smartregistry`](https://code.foss.global/push.rocks/smartregistry) |
|
||||||
| **Frontend** | Angular 19 (Signals, Zoneless) + Tailwind CSS |
|
| **Frontend** | Web Components via [`@design.estate/dees-element`](https://code.foss.global/design.estate/dees-element) + [`@design.estate/dees-catalog`](https://code.foss.global/design.estate/dees-catalog) |
|
||||||
| **Auth** | JWT (HS256) + OAuth/OIDC + LDAP |
|
| **UI Build** | [`@git.zone/tsbundle`](https://code.foss.global/git.zone/tsbundle) |
|
||||||
| **Build** | [`@git.zone/tsdeno`](https://code.foss.global/git.zone/tsdeno) cross-compilation |
|
| **Auth** | JWT (HS256) + OAuth/OIDC + LDAP |
|
||||||
| **CI/CD** | Gitea Actions → binary releases |
|
| **Build** | [`@git.zone/tsdeno`](https://code.foss.global/git.zone/tsdeno) cross-compilation |
|
||||||
|
| **CI/CD** | Gitea Actions → binary releases |
|
||||||
|
|
||||||
## 🛠️ Development
|
## 🛠️ Development
|
||||||
|
|
||||||
@@ -329,11 +352,8 @@ deno task dev
|
|||||||
# Watch mode: backend + UI + bundler concurrently
|
# Watch mode: backend + UI + bundler concurrently
|
||||||
pnpm run watch
|
pnpm run watch
|
||||||
|
|
||||||
# Build Angular UI
|
# Build UI (web components via tsbundle)
|
||||||
deno task build
|
deno task build-ui
|
||||||
|
|
||||||
# Bundle UI into embedded TypeScript
|
|
||||||
deno task bundle-ui
|
|
||||||
|
|
||||||
# Cross-compile binaries for all platforms
|
# Cross-compile binaries for all platforms
|
||||||
deno task compile
|
deno task compile
|
||||||
@@ -355,7 +375,7 @@ deno task test:e2e # E2E tests (requires running server + services)
|
|||||||
Releases are automated via Gitea Actions (`.gitea/workflows/release.yml`):
|
Releases are automated via Gitea Actions (`.gitea/workflows/release.yml`):
|
||||||
|
|
||||||
1. Push a `v*` tag
|
1. Push a `v*` tag
|
||||||
2. CI builds the Angular UI and bundles it into TypeScript
|
2. CI builds the Web Components UI via `tsbundle`
|
||||||
3. `tsdeno compile` produces binaries for 4 platforms (linux-x64, linux-arm64, macos-x64,
|
3. `tsdeno compile` produces binaries for 4 platforms (linux-x64, linux-arm64, macos-x64,
|
||||||
macos-arm64)
|
macos-arm64)
|
||||||
4. Binaries + SHA256 checksums are uploaded as Gitea release assets
|
4. Binaries + SHA256 checksums are uploaded as Gitea release assets
|
||||||
@@ -367,38 +387,28 @@ Compile targets are configured in `npmextra.json` under `@git.zone/tsdeno`.
|
|||||||
Artifacts are stored in S3 at:
|
Artifacts are stored in S3 at:
|
||||||
|
|
||||||
```
|
```
|
||||||
{storagePath}/{protocol}/{orgName}/{packageName}/{version}/{filename}
|
{storagePath}/{protocol}/packages/{packageName}/{version}/{filename}
|
||||||
```
|
```
|
||||||
|
|
||||||
For example: `packages/npm/myorg/mypackage/1.0.0/mypackage-1.0.0.tgz`
|
For example: `packages/npm/packages/@myorg/mypackage/mypackage-1.0.0.tgz`
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
be found in the [LICENSE](./LICENSE) file.
|
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
|
**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.
|
||||||
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
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
|
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.
|
||||||
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
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its
|
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.
|
||||||
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
|
|
||||||
Capital GmbH of any derivative works.
|
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ import {
|
|||||||
createTestApiToken,
|
createTestApiToken,
|
||||||
createTestRepository,
|
createTestRepository,
|
||||||
createTestUser,
|
createTestUser,
|
||||||
|
getTestRegistry,
|
||||||
runCommand,
|
runCommand,
|
||||||
setupTestDb,
|
setupTestDb,
|
||||||
skipIfMissing,
|
skipIfMissing,
|
||||||
|
startTestServer,
|
||||||
|
stopTestServer,
|
||||||
teardownTestDb,
|
teardownTestDb,
|
||||||
testConfig,
|
testConfig,
|
||||||
} from '../helpers/index.ts';
|
} from '../helpers/index.ts';
|
||||||
@@ -27,7 +30,7 @@ const FIXTURE_DIR = path.join(
|
|||||||
'../fixtures/npm/@stack-test/demo-package',
|
'../fixtures/npm/@stack-test/demo-package',
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('NPM E2E: Full lifecycle', () => {
|
describe('NPM E2E: Full lifecycle', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||||
let testUserId: string;
|
let testUserId: string;
|
||||||
let testOrgName: string;
|
let testOrgName: string;
|
||||||
let apiToken: string;
|
let apiToken: string;
|
||||||
@@ -41,11 +44,13 @@ describe('NPM E2E: Full lifecycle', () => {
|
|||||||
|
|
||||||
await setupTestDb();
|
await setupTestDb();
|
||||||
registryUrl = testConfig.registry.url;
|
registryUrl = testConfig.registry.url;
|
||||||
|
await startTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (!shouldSkip) {
|
if (!shouldSkip) {
|
||||||
await teardownTestDb();
|
await teardownTestDb();
|
||||||
|
await stopTestServer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,6 +59,24 @@ describe('NPM E2E: Full lifecycle', () => {
|
|||||||
|
|
||||||
await cleanupTestDb();
|
await cleanupTestDb();
|
||||||
|
|
||||||
|
// Clean up S3 test packages from previous runs
|
||||||
|
try {
|
||||||
|
const bucket = getTestRegistry()?.getSmartBucket();
|
||||||
|
if (bucket) {
|
||||||
|
const b = await bucket.getBucketByName(testConfig.s3.bucket);
|
||||||
|
if (b) {
|
||||||
|
for (const key of [
|
||||||
|
'npm/packages/@stack-test/demo-package/index.json',
|
||||||
|
'npm/packages/@stack-test/demo-package/stack-test-demo-package-1.0.0.tgz',
|
||||||
|
]) {
|
||||||
|
await b.fastRemove({ path: key }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore S3 cleanup errors
|
||||||
|
}
|
||||||
|
|
||||||
// Create test user and org
|
// Create test user and org
|
||||||
const { user } = await createTestUser({ status: 'active' });
|
const { user } = await createTestUser({ status: 'active' });
|
||||||
testUserId = user.id;
|
testUserId = user.id;
|
||||||
@@ -237,11 +260,17 @@ describe('NPM E2E: Full lifecycle', () => {
|
|||||||
apiToken,
|
apiToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unpublish
|
// Unpublish (run from FIXTURE_DIR so .npmrc auth is picked up)
|
||||||
const unpublishResult = await clients.npm.unpublish(
|
const unpublishResult = await runCommand(
|
||||||
'@stack-test/demo-package@1.0.0',
|
[
|
||||||
`${registryUrl}/-/npm/${testOrgName}/`,
|
'npm',
|
||||||
apiToken,
|
'unpublish',
|
||||||
|
'@stack-test/demo-package@1.0.0',
|
||||||
|
'--registry',
|
||||||
|
`${registryUrl}/-/npm/${testOrgName}/`,
|
||||||
|
'--force',
|
||||||
|
],
|
||||||
|
{ cwd: FIXTURE_DIR },
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
createTestUser,
|
createTestUser,
|
||||||
setupTestDb,
|
setupTestDb,
|
||||||
skipIfMissing,
|
skipIfMissing,
|
||||||
|
startTestServer,
|
||||||
|
stopTestServer,
|
||||||
teardownTestDb,
|
teardownTestDb,
|
||||||
testConfig,
|
testConfig,
|
||||||
} from '../helpers/index.ts';
|
} from '../helpers/index.ts';
|
||||||
@@ -26,7 +28,7 @@ const FIXTURE_DIR = path.join(
|
|||||||
'../fixtures/oci',
|
'../fixtures/oci',
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('OCI E2E: Full lifecycle', () => {
|
describe('OCI E2E: Full lifecycle', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||||
let testUserId: string;
|
let testUserId: string;
|
||||||
let testOrgName: string;
|
let testOrgName: string;
|
||||||
let apiToken: string;
|
let apiToken: string;
|
||||||
@@ -41,11 +43,13 @@ describe('OCI E2E: Full lifecycle', () => {
|
|||||||
await setupTestDb();
|
await setupTestDb();
|
||||||
const url = new URL(testConfig.registry.url);
|
const url = new URL(testConfig.registry.url);
|
||||||
registryHost = url.host;
|
registryHost = url.host;
|
||||||
|
await startTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (!shouldSkip) {
|
if (!shouldSkip) {
|
||||||
await teardownTestDb();
|
await teardownTestDb();
|
||||||
|
await stopTestServer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ describe('OCI E2E: Full lifecycle', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageName = `${registryHost}/v2/${testOrgName}/demo:1.0.0`;
|
const imageName = `${registryHost}/${testOrgName}/demo:1.0.0`;
|
||||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +116,7 @@ describe('OCI E2E: Full lifecycle', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageName = `${registryHost}/v2/${testOrgName}/demo:1.0.0`;
|
const imageName = `${registryHost}/${testOrgName}/demo:1.0.0`;
|
||||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -138,7 +142,7 @@ describe('OCI E2E: Full lifecycle', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageName = `${registryHost}/v2/${testOrgName}/multi:1.0.0`;
|
const imageName = `${registryHost}/${testOrgName}/multi:1.0.0`;
|
||||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.multi-layer');
|
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.multi-layer');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -81,5 +81,8 @@ export {
|
|||||||
setupTestStorage,
|
setupTestStorage,
|
||||||
} from './storage.helper.ts';
|
} from './storage.helper.ts';
|
||||||
|
|
||||||
|
// Server helpers
|
||||||
|
export { getTestRegistry, startTestServer, stopTestServer } from './server.helper.ts';
|
||||||
|
|
||||||
// Re-export test config
|
// Re-export test config
|
||||||
export { getTestConfig, testConfig } from '../test.config.ts';
|
export { getTestConfig, testConfig } from '../test.config.ts';
|
||||||
|
|||||||
49
test/helpers/server.helper.ts
Normal file
49
test/helpers/server.helper.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Server helper - starts/stops the registry server for integration and E2E tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StackGalleryRegistry } from '../../ts/registry.ts';
|
||||||
|
import { testConfig } from '../test.config.ts';
|
||||||
|
|
||||||
|
let registry: StackGalleryRegistry | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the registry server for testing
|
||||||
|
*/
|
||||||
|
export async function startTestServer(): Promise<StackGalleryRegistry> {
|
||||||
|
if (registry) return registry;
|
||||||
|
|
||||||
|
// Set JWT_SECRET env var so ApiRouter's AuthService uses the same secret
|
||||||
|
Deno.env.set('JWT_SECRET', testConfig.jwt.secret);
|
||||||
|
|
||||||
|
registry = new StackGalleryRegistry({
|
||||||
|
mongoUrl: testConfig.mongodb.url,
|
||||||
|
mongoDb: testConfig.mongodb.name,
|
||||||
|
s3Endpoint: testConfig.s3.endpoint,
|
||||||
|
s3AccessKey: testConfig.s3.accessKey,
|
||||||
|
s3SecretKey: testConfig.s3.secretKey,
|
||||||
|
s3Bucket: testConfig.s3.bucket,
|
||||||
|
s3Region: testConfig.s3.region,
|
||||||
|
port: testConfig.registry.port,
|
||||||
|
jwtSecret: testConfig.jwt.secret,
|
||||||
|
});
|
||||||
|
await registry.start();
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the registry server
|
||||||
|
*/
|
||||||
|
export async function stopTestServer(): Promise<void> {
|
||||||
|
if (registry) {
|
||||||
|
await registry.stop();
|
||||||
|
registry = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current registry instance
|
||||||
|
*/
|
||||||
|
export function getTestRegistry(): StackGalleryRegistry | null {
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ export const clients = {
|
|||||||
docker: {
|
docker: {
|
||||||
check: () => commandExists('docker'),
|
check: () => commandExists('docker'),
|
||||||
build: (dockerfile: string, tag: string, context: string) =>
|
build: (dockerfile: string, tag: string, context: string) =>
|
||||||
runCommand(['docker', 'build', '-f', dockerfile, '-t', tag, context]),
|
runCommand(['docker', 'build', '--load', '-f', dockerfile, '-t', tag, context]),
|
||||||
push: (image: string) => runCommand(['docker', 'push', image]),
|
push: (image: string) => runCommand(['docker', 'push', image]),
|
||||||
pull: (image: string) => runCommand(['docker', 'pull', image]),
|
pull: (image: string) => runCommand(['docker', 'pull', image]),
|
||||||
rmi: (image: string, force = false) =>
|
rmi: (image: string, force = false) =>
|
||||||
|
|||||||
@@ -13,16 +13,20 @@ import {
|
|||||||
get,
|
get,
|
||||||
post,
|
post,
|
||||||
setupTestDb,
|
setupTestDb,
|
||||||
|
startTestServer,
|
||||||
|
stopTestServer,
|
||||||
teardownTestDb,
|
teardownTestDb,
|
||||||
} from '../helpers/index.ts';
|
} from '../helpers/index.ts';
|
||||||
|
|
||||||
describe('Auth API Integration', () => {
|
describe('Auth API Integration', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupTestDb();
|
await setupTestDb();
|
||||||
|
await startTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await teardownTestDb();
|
await teardownTestDb();
|
||||||
|
await stopTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -56,7 +60,7 @@ describe('Auth API Integration', () => {
|
|||||||
|
|
||||||
assertStatus(response, 401);
|
assertStatus(response, 401);
|
||||||
const body = response.body as Record<string, unknown>;
|
const body = response.body as Record<string, unknown>;
|
||||||
assertEquals(body.error, 'INVALID_CREDENTIALS');
|
assertEquals(body.code, 'INVALID_CREDENTIALS');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 401 for inactive user', async () => {
|
it('should return 401 for inactive user', async () => {
|
||||||
@@ -72,7 +76,7 @@ describe('Auth API Integration', () => {
|
|||||||
|
|
||||||
assertStatus(response, 401);
|
assertStatus(response, 401);
|
||||||
const body = response.body as Record<string, unknown>;
|
const body = response.body as Record<string, unknown>;
|
||||||
assertEquals(body.error, 'ACCOUNT_INACTIVE');
|
assertEquals(body.code, 'ACCOUNT_INACTIVE');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,9 +159,14 @@ describe('Auth API Integration', () => {
|
|||||||
});
|
});
|
||||||
const loginBody = loginResponse.body as Record<string, unknown>;
|
const loginBody = loginResponse.body as Record<string, unknown>;
|
||||||
const token = loginBody.accessToken as string;
|
const token = loginBody.accessToken as string;
|
||||||
|
const sessionId = loginBody.sessionId as string;
|
||||||
|
|
||||||
// Logout
|
// Logout with sessionId
|
||||||
const logoutResponse = await post('/api/v1/auth/logout', {}, createAuthHeader(token));
|
const logoutResponse = await post(
|
||||||
|
'/api/v1/auth/logout',
|
||||||
|
{ sessionId },
|
||||||
|
createAuthHeader(token),
|
||||||
|
);
|
||||||
|
|
||||||
assertStatus(logoutResponse, 200);
|
assertStatus(logoutResponse, 200);
|
||||||
|
|
||||||
|
|||||||
@@ -16,19 +16,23 @@ import {
|
|||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
setupTestDb,
|
setupTestDb,
|
||||||
|
startTestServer,
|
||||||
|
stopTestServer,
|
||||||
teardownTestDb,
|
teardownTestDb,
|
||||||
} from '../helpers/index.ts';
|
} from '../helpers/index.ts';
|
||||||
|
|
||||||
describe('Organization API Integration', () => {
|
describe('Organization API Integration', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||||
let accessToken: string;
|
let accessToken: string;
|
||||||
let testUserId: string;
|
let testUserId: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupTestDb();
|
await setupTestDb();
|
||||||
|
await startTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await teardownTestDb();
|
await teardownTestDb();
|
||||||
|
await stopTestServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -116,8 +120,8 @@ describe('Organization API Integration', () => {
|
|||||||
const response = await get('/api/v1/organizations', createAuthHeader(accessToken));
|
const response = await get('/api/v1/organizations', createAuthHeader(accessToken));
|
||||||
|
|
||||||
assertStatus(response, 200);
|
assertStatus(response, 200);
|
||||||
const body = response.body as Record<string, unknown>[];
|
const body = response.body as { organizations: Record<string, unknown>[] };
|
||||||
assertEquals(body.length >= 2, true);
|
assertEquals(body.organizations.length >= 2, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,8 +206,8 @@ describe('Organization API Integration', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assertStatus(response, 200);
|
assertStatus(response, 200);
|
||||||
const body = response.body as Record<string, unknown>[];
|
const body = response.body as { members: Record<string, unknown>[] };
|
||||||
assertEquals(body.length >= 1, true); // At least the creator
|
assertEquals(body.members.length >= 1, true); // At least the creator
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add member to organization', async () => {
|
it('should add member to organization', async () => {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@stack.gallery/registry',
|
name: '@stack.gallery/registry',
|
||||||
version: '1.8.4',
|
version: '1.8.5',
|
||||||
description: 'Enterprise-grade multi-protocol package registry'
|
description: 'Enterprise-grade multi-protocol package registry'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,15 +144,30 @@ export class StackGalleryAuthProvider implements plugins.smartregistry.IAuthProv
|
|||||||
// Map action
|
// Map action
|
||||||
const mappedAction = this.mapAction(action);
|
const mappedAction = this.mapAction(action);
|
||||||
|
|
||||||
// For simple authorization without specific resource context,
|
// Check if user is active
|
||||||
// check if user is active
|
|
||||||
const user = await User.findById(userId);
|
const user = await User.findById(userId);
|
||||||
if (!user || !user.isActive) return false;
|
if (!user || !user.isActive) return false;
|
||||||
|
|
||||||
// System admins bypass all checks
|
// System admins bypass all checks
|
||||||
if (user.isSystemAdmin) return true;
|
if (user.isSystemAdmin) return true;
|
||||||
|
|
||||||
return mappedAction === 'read'; // Default: authenticated users can read
|
// Check token scopes for the requested action
|
||||||
|
if (token.scopes) {
|
||||||
|
for (const scope of token.scopes) {
|
||||||
|
// Scope format: "protocol:action1,action2" or "*"
|
||||||
|
if (scope === '*') return true;
|
||||||
|
const [, actions] = scope.split(':');
|
||||||
|
if (actions) {
|
||||||
|
const actionList = actions.split(',');
|
||||||
|
if (actionList.includes(mappedAction) || actionList.includes('*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: authenticated users can read
|
||||||
|
return mappedAction === 'read';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
119
ts/registry.ts
119
ts/registry.ts
@@ -8,6 +8,7 @@ import { closeDb, initDb, isDbConnected } from './models/db.ts';
|
|||||||
import { StackGalleryAuthProvider } from './providers/auth.provider.ts';
|
import { StackGalleryAuthProvider } from './providers/auth.provider.ts';
|
||||||
import { StackGalleryStorageHooks } from './providers/storage.provider.ts';
|
import { StackGalleryStorageHooks } from './providers/storage.provider.ts';
|
||||||
import { OpsServer } from './opsserver/classes.opsserver.ts';
|
import { OpsServer } from './opsserver/classes.opsserver.ts';
|
||||||
|
import { ApiRouter } from './api/router.ts';
|
||||||
|
|
||||||
// Bundled UI files (generated by tsbundle with base64ts output mode)
|
// Bundled UI files (generated by tsbundle with base64ts output mode)
|
||||||
let bundledFileMap: Map<string, { data: Uint8Array; contentType: string }> | null = null;
|
let bundledFileMap: Map<string, { data: Uint8Array; contentType: string }> | null = null;
|
||||||
@@ -77,6 +78,7 @@ export class StackGalleryRegistry {
|
|||||||
private authProvider: StackGalleryAuthProvider | null = null;
|
private authProvider: StackGalleryAuthProvider | null = null;
|
||||||
private storageHooks: StackGalleryStorageHooks | null = null;
|
private storageHooks: StackGalleryStorageHooks | null = null;
|
||||||
private opsServer: OpsServer | null = null;
|
private opsServer: OpsServer | null = null;
|
||||||
|
private apiRouter: ApiRouter | null = null;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
|
||||||
constructor(config: IRegistryConfig) {
|
constructor(config: IRegistryConfig) {
|
||||||
@@ -141,13 +143,21 @@ export class StackGalleryRegistry {
|
|||||||
npmTokens: { enabled: true },
|
npmTokens: { enabled: true },
|
||||||
ociTokens: {
|
ociTokens: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
realm: 'stack.gallery',
|
realm: `http://${this.config.host === '0.0.0.0' ? 'localhost' : this.config.host}:${this.config.port}/v2/token`,
|
||||||
service: 'registry',
|
service: 'registry',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
npm: { enabled: true, basePath: '/-/npm' },
|
||||||
|
oci: { enabled: true, basePath: '/v2' },
|
||||||
});
|
});
|
||||||
|
await this.smartRegistry.init();
|
||||||
console.log('[StackGalleryRegistry] smartregistry initialized');
|
console.log('[StackGalleryRegistry] smartregistry initialized');
|
||||||
|
|
||||||
|
// Initialize REST API router
|
||||||
|
console.log('[StackGalleryRegistry] Initializing API router...');
|
||||||
|
this.apiRouter = new ApiRouter();
|
||||||
|
console.log('[StackGalleryRegistry] API router initialized');
|
||||||
|
|
||||||
// Initialize OpsServer (TypedRequest handlers)
|
// Initialize OpsServer (TypedRequest handlers)
|
||||||
console.log('[StackGalleryRegistry] Initializing OpsServer...');
|
console.log('[StackGalleryRegistry] Initializing OpsServer...');
|
||||||
this.opsServer = new OpsServer(this);
|
this.opsServer = new OpsServer(this);
|
||||||
@@ -198,31 +208,40 @@ export class StackGalleryRegistry {
|
|||||||
return await this.handleTypedRequest(request);
|
return await this.handleTypedRequest(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy REST API endpoints (keep for backwards compatibility during migration)
|
|
||||||
// TODO: Remove once frontend is fully migrated to TypedRequest
|
|
||||||
|
|
||||||
// Registry protocol endpoints (handled by smartregistry)
|
// Registry protocol endpoints (handled by smartregistry)
|
||||||
const registryPaths = [
|
// NPM: /-/npm/{orgName}/... -> strip orgName, forward as /-/npm/...
|
||||||
'/-/',
|
// OCI: /v2/{orgName}/... -> forward as /v2/{orgName}/... (OCI uses name segments natively)
|
||||||
'/v2/',
|
if (this.smartRegistry) {
|
||||||
'/maven2/',
|
|
||||||
'/simple/',
|
|
||||||
'/pypi/',
|
|
||||||
'/api/v1/crates/',
|
|
||||||
'/packages.json',
|
|
||||||
'/p/',
|
|
||||||
'/api/v1/gems/',
|
|
||||||
'/gems/',
|
|
||||||
];
|
|
||||||
const isRegistryPath = registryPaths.some((p) => path.startsWith(p)) ||
|
|
||||||
(path.startsWith('/@') && !path.startsWith('/@stack'));
|
|
||||||
|
|
||||||
if (this.smartRegistry && isRegistryPath) {
|
|
||||||
try {
|
try {
|
||||||
// Convert Request to IRequestContext
|
// NPM protocol: extract org from /-/npm/{orgName}/...
|
||||||
const requestContext = await this.requestToContext(request);
|
if (path.startsWith('/-/npm/')) {
|
||||||
const response = await this.smartRegistry.handleRequest(requestContext);
|
const orgMatch = path.match(/^\/-\/npm\/([^\/]+)(\/.*)?$/);
|
||||||
if (response) return this.contextResponseToResponse(response);
|
if (orgMatch) {
|
||||||
|
const orgName = decodeURIComponent(orgMatch[1]);
|
||||||
|
const remainder = orgMatch[2] || '/';
|
||||||
|
const requestContext = await this.requestToContext(request);
|
||||||
|
requestContext.path = `/-/npm${remainder}`;
|
||||||
|
if (!requestContext.actor) {
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
requestContext.actor = {} as any;
|
||||||
|
}
|
||||||
|
requestContext.actor!.orgId = orgName;
|
||||||
|
const response = await this.smartRegistry.handleRequest(requestContext);
|
||||||
|
if (response) return this.contextResponseToResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCI token endpoint: /v2/token (Docker Bearer auth flow)
|
||||||
|
if (path === '/v2/token') {
|
||||||
|
return this.handleOciTokenRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCI protocol: /v2/... or /v2
|
||||||
|
if (path.startsWith('/v2/') || path === '/v2') {
|
||||||
|
const requestContext = await this.requestToContext(request);
|
||||||
|
const response = await this.smartRegistry.handleRequest(requestContext);
|
||||||
|
if (response) return this.contextResponseToResponse(response);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[StackGalleryRegistry] Request error:', error);
|
console.error('[StackGalleryRegistry] Request error:', error);
|
||||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||||
@@ -232,6 +251,11 @@ export class StackGalleryRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REST API endpoints
|
||||||
|
if (this.apiRouter && path.startsWith('/api/')) {
|
||||||
|
return this.apiRouter.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
// Serve static UI files
|
// Serve static UI files
|
||||||
return this.serveStaticFile(path);
|
return this.serveStaticFile(path);
|
||||||
}
|
}
|
||||||
@@ -374,6 +398,53 @@ export class StackGalleryRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OCI token requests (Docker Bearer auth flow)
|
||||||
|
* Docker sends GET /v2/token?service=...&scope=... to obtain a Bearer token
|
||||||
|
*/
|
||||||
|
private async handleOciTokenRequest(request: Request): Promise<Response> {
|
||||||
|
const authHeader = request.headers.get('authorization');
|
||||||
|
let apiToken: string | undefined;
|
||||||
|
|
||||||
|
// Extract token from Basic auth (Docker sends username:password)
|
||||||
|
if (authHeader?.startsWith('Basic ')) {
|
||||||
|
const credentials = atob(authHeader.substring(6));
|
||||||
|
const [_username, password] = credentials.split(':');
|
||||||
|
if (password) {
|
||||||
|
apiToken = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from Bearer auth
|
||||||
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
|
apiToken = authHeader.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiToken) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
token: apiToken,
|
||||||
|
access_token: apiToken,
|
||||||
|
expires_in: 3600,
|
||||||
|
issued_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No auth provided — return 401
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'authentication required' }),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Health check endpoint
|
* Health check endpoint
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@stack.gallery/registry',
|
name: '@stack.gallery/registry',
|
||||||
version: '1.8.4',
|
version: '1.8.5',
|
||||||
description: 'Enterprise-grade multi-protocol package registry'
|
description: 'Enterprise-grade multi-protocol package registry'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user