feat(release,build,tests): add automated multi-platform release pipeline and align runtime, model, and test updates

This commit is contained in:
2026-03-20 13:56:43 +00:00
parent 4d561b3874
commit b05c53f967
25 changed files with 3747 additions and 941 deletions

View File

@@ -0,0 +1,212 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-node:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable corepack
run: corepack enable
- name: Install root dependencies
run: pnpm install --ignore-scripts
- name: Install UI dependencies
run: cd ui && pnpm install
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Verify deno.json version matches tag
run: |
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
TAG_VERSION="${{ steps.version.outputs.version_number }}"
echo "deno.json version: $DENO_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
exit 1
fi
- name: Build Angular UI
run: cd ui && pnpm run build
- name: Bundle UI into TypeScript
run: deno run --allow-all scripts/bundle-ui.ts
- name: Compile binaries for all platforms
run: mkdir -p dist/binaries && npx tsdeno compile
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
cd ../..
- name: Extract changelog for this version
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
if [ ! -f CHANGELOG.md ] && [ ! -f changelog.md ]; then
echo "No changelog found, using default release notes"
cat > /tmp/release_notes.md << EOF
## Stack.Gallery Registry $VERSION
Pre-compiled binaries for multiple platforms.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash
\`\`\`
Or download the binary for your platform and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
### Checksums
SHA256 checksums are provided in SHA256SUMS.txt
EOF
else
CHANGELOG_FILE=$([ -f CHANGELOG.md ] && echo "CHANGELOG.md" || echo "changelog.md")
awk "/## \[$VERSION\]/,/## \[/" "$CHANGELOG_FILE" | sed '$d' > /tmp/release_notes.md || cat > /tmp/release_notes.md << EOF
## Stack.Gallery Registry $VERSION
See changelog.md for full details.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash
\`\`\`
EOF
fi
echo "Release notes:"
cat /tmp/release_notes.md
- name: Delete existing release if it exists
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Checking for existing release $VERSION..."
EXISTING_RELEASE_ID=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases/tags/$VERSION" \
| jq -r '.id // empty')
if [ -n "$EXISTING_RELEASE_ID" ]; then
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
curl -X DELETE -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases/$EXISTING_RELEASE_ID"
echo "Existing release deleted"
sleep 2
else
echo "No existing release found, proceeding with creation"
fi
- name: Create Gitea Release
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Creating release for $VERSION..."
RELEASE_ID=$(curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases" \
-d "{
\"tag_name\": \"$VERSION\",
\"name\": \"Stack.Gallery Registry $VERSION\",
\"body\": $(jq -Rs . /tmp/release_notes.md),
\"draft\": false,
\"prerelease\": false
}" | jq -r '.id')
echo "Release created with ID: $RELEASE_ID"
for binary in dist/binaries/*; do
filename=$(basename "$binary")
echo "Uploading $filename..."
curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$binary" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases/$RELEASE_ID/assets?name=$filename"
done
echo "All assets uploaded successfully"
- name: Clean up old releases
run: |
echo "Cleaning up old releases (keeping only last 3)..."
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases" | \
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
if [ -n "$RELEASES" ]; then
echo "Found releases to delete:"
for release_id in $RELEASES; do
echo " Deleting release ID: $release_id"
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/stack.gallery/registry/releases/$release_id"
done
echo "Old releases deleted successfully"
else
echo "No old releases to delete (less than 4 releases total)"
fi
echo ""
- name: Release Summary
run: |
echo "================================================"
echo " Release ${{ steps.version.outputs.version }} Complete!"
echo "================================================"
echo ""
echo "Binaries published:"
ls -lh dist/binaries/
echo ""
echo "Release URL:"
echo "https://code.foss.global/stack.gallery/registry/releases/tag/${{ steps.version.outputs.version }}"
echo ""
echo "Installation command:"
echo "curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash"
echo ""

8
.gitignore vendored
View File

@@ -4,9 +4,13 @@ node_modules/
# Build outputs # Build outputs
dist/ dist/
ui/dist/
.angular/ .angular/
out-tsc/ out-tsc/
# tsdeno temporary files
package.json.bak
# Generated files # Generated files
ts/embedded-ui.generated.ts ts/embedded-ui.generated.ts
@@ -45,11 +49,15 @@ coverage/
*.tmp *.tmp
*.temp *.temp
# Playwright MCP
.playwright-mcp/
# Debug # Debug
.nogit/ .nogit/
# Claude # Claude
CLAUDE.md CLAUDE.md
.claude/
stories/ stories/
# Package manager locks (keep pnpm-lock.yaml) # Package manager locks (keep pnpm-lock.yaml)

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 2026-03-20 - 1.4.0 - feat(release,build,tests)
add automated multi-platform release pipeline and align runtime, model, and test updates
- add a Gitea release workflow that builds the UI, bundles embedded assets, cross-compiles binaries for Linux and macOS, generates checksums, and publishes release assets from version tags
- switch compilation to tsdeno with compile targets defined in npmextra.json and simplify project scripts for check, lint, format, and compile tasks
- improve CLI startup error handling in mod.ts and guard execution with import.meta.main
- update test configuration to load MongoDB and S3 settings from qenv-based environment files and adjust tests for renamed model and token APIs
- rename package search usage to searchPackages, update audit event names, and align package version fields and model name overrides with newer dependency behavior
## 2025-12-03 - 1.3.0 - feat(auth) ## 2025-12-03 - 1.3.0 - feat(auth)
Add external authentication (OAuth/OIDC & LDAP) with admin management, UI, and encryption support Add external authentication (OAuth/OIDC & LDAP) with admin management, UI, and encryption support

View File

@@ -15,30 +15,29 @@
"build": "cd ui && pnpm run build", "build": "cd ui && pnpm run build",
"bundle-ui": "deno run --allow-all scripts/bundle-ui.ts", "bundle-ui": "deno run --allow-all scripts/bundle-ui.ts",
"bundle-ui:watch": "deno run --allow-all scripts/bundle-ui.ts --watch", "bundle-ui:watch": "deno run --allow-all scripts/bundle-ui.ts --watch",
"compile": "deno compile --allow-all --output dist/stack-gallery-registry mod.ts", "compile": "tsdeno compile",
"compile:linux-x64": "deno compile --allow-all --target x86_64-unknown-linux-gnu --output dist/stack-gallery-registry-linux-x64 mod.ts", "check": "deno check mod.ts",
"compile:linux-arm64": "deno compile --allow-all --target aarch64-unknown-linux-gnu --output dist/stack-gallery-registry-linux-arm64 mod.ts", "fmt": "deno fmt",
"compile:macos-x64": "deno compile --allow-all --target x86_64-apple-darwin --output dist/stack-gallery-registry-macos-x64 mod.ts", "lint": "deno lint"
"compile:macos-arm64": "deno compile --allow-all --target aarch64-apple-darwin --output dist/stack-gallery-registry-macos-arm64 mod.ts",
"release": "deno task bundle-ui && deno task compile:linux-x64 && deno task compile:linux-arm64 && deno task compile:macos-x64 && deno task compile:macos-arm64"
}, },
"imports": { "imports": {
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.5.0", "@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.6.0",
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.13", "@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.1.0",
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0", "@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.5.1",
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.1.0", "@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.2.1",
"@push.rocks/smartenv": "npm:@push.rocks/smartenv@^6.0.0", "@push.rocks/smartenv": "npm:@push.rocks/smartenv@^6.0.0",
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0", "@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0",
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.0", "@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.3",
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.1.0", "@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.1.0",
"@push.rocks/smartcrypto": "npm:@push.rocks/smartcrypto@^2.0.0", "@push.rocks/smartcrypto": "npm:@push.rocks/smartcrypto@^2.0.4",
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.0", "@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
"@push.rocks/smartunique": "npm:@push.rocks/smartunique@^3.0.0", "@push.rocks/smartunique": "npm:@push.rocks/smartunique@^3.0.9",
"@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.0", "@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.5",
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.0", "@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.0", "@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.20",
"@push.rocks/smartarchive": "npm:@push.rocks/smartarchive@^5.0.0", "@push.rocks/smartarchive": "npm:@push.rocks/smartarchive@^5.2.1",
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.3.0", "@push.rocks/qenv": "npm:@push.rocks/qenv@^6.1.3",
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.5.0",
"@std/path": "jsr:@std/path@^1.0.0", "@std/path": "jsr:@std/path@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0", "@std/fs": "jsr:@std/fs@^1.0.0",
"@std/http": "jsr:@std/http@^1.0.0" "@std/http": "jsr:@std/http@^1.0.0"

1456
deno.lock generated

File diff suppressed because it is too large Load Diff

15
mod.ts
View File

@@ -7,5 +7,16 @@
import { runCli } from './ts/cli.ts'; import { runCli } from './ts/cli.ts';
// Run CLI if (import.meta.main) {
await runCli(); try {
await runCli();
} catch (error) {
const debugMode = Deno.args.includes('--debug');
if (debugMode) {
console.error(error);
} else {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
Deno.exit(1);
}
}

68
npmextra.json Normal file
View File

@@ -0,0 +1,68 @@
{
"@git.zone/cli": {
"release": {
"registries": [
"https://verdaccio.lossless.digital"
],
"accessLevel": "public"
},
"projectType": "deno",
"module": {
"githost": "code.foss.global",
"gitscope": "stack.gallery",
"gitrepo": "registry",
"description": "Enterprise-grade multi-protocol package registry",
"npmPackagename": "@stack.gallery/registry",
"license": "MIT"
},
"services": [
"mongodb",
"minio"
]
},
"@git.zone/tsdeno": {
"compileTargets": [
{
"name": "stack-gallery-registry-linux-x64",
"entryPoint": "mod.ts",
"outDir": "dist/binaries",
"target": "x86_64-unknown-linux-gnu",
"permissions": [
"--allow-all"
],
"noCheck": true
},
{
"name": "stack-gallery-registry-linux-arm64",
"entryPoint": "mod.ts",
"outDir": "dist/binaries",
"target": "aarch64-unknown-linux-gnu",
"permissions": [
"--allow-all"
],
"noCheck": true
},
{
"name": "stack-gallery-registry-macos-x64",
"entryPoint": "mod.ts",
"outDir": "dist/binaries",
"target": "x86_64-apple-darwin",
"permissions": [
"--allow-all"
],
"noCheck": true
},
{
"name": "stack-gallery-registry-macos-arm64",
"entryPoint": "mod.ts",
"outDir": "dist/binaries",
"target": "aarch64-apple-darwin",
"permissions": [
"--allow-all"
],
"noCheck": true
}
]
},
"@ship.zone/szci": {}
}

View File

@@ -8,8 +8,10 @@
"start": "deno run --allow-all mod.ts server", "start": "deno run --allow-all mod.ts server",
"dev": "deno run --allow-all --watch mod.ts server --ephemeral", "dev": "deno run --allow-all --watch mod.ts server --ephemeral",
"watch": "concurrently --kill-others --names \"BACKEND,UI,BUNDLER\" --prefix-colors \"cyan,magenta,yellow\" \"deno run --allow-all --watch mod.ts server --ephemeral\" \"cd ui && pnpm run watch\" \"deno task bundle-ui:watch\"", "watch": "concurrently --kill-others --names \"BACKEND,UI,BUNDLER\" --prefix-colors \"cyan,magenta,yellow\" \"deno run --allow-all --watch mod.ts server --ephemeral\" \"cd ui && pnpm run watch\" \"deno task bundle-ui:watch\"",
"build": "cd ui && pnpm run build", "build": "deno task check",
"test": "deno test --allow-all" "test": "deno task test",
"lint": "deno task lint",
"format": "deno task fmt"
}, },
"keywords": [ "keywords": [
"registry", "registry",
@@ -25,10 +27,8 @@
"author": "Stack.Gallery", "author": "Stack.Gallery",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@git.zone/tsdeno": "^1.2.0",
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
}, },
"dependencies": {
"@push.rocks/smartdata": "link:../../push.rocks/smartdata"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
} }

2305
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
overrides:
'@push.rocks/smartdata': link:../../push.rocks/smartdata

438
readme.md
View File

@@ -1,6 +1,6 @@
# @stack.gallery/registry 📦 # @stack.gallery/registry 📦
**Enterprise-grade multi-protocol package registry** built with Deno and TypeScript. Host your own private NPM, Docker/OCI, Maven, Cargo, PyPI, Composer, and RubyGems registry with a unified, beautiful web interface. A self-hosted, multi-protocol package registry built with Deno and TypeScript. Run your own private **NPM**, **Docker/OCI**, **Maven**, **Cargo**, **PyPI**, **Composer**, and **RubyGems** registry — all behind a single binary with a modern web UI.
## Issue Reporting and Security ## Issue Reporting and Security
@@ -8,225 +8,357 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## ✨ Features ## ✨ Features
- 🔐 **Multi-Protocol Support** - NPM, OCI/Docker, Maven, Cargo, PyPI, Composer, RubyGems - 🔌 **7 Protocol Support** NPM, OCI/Docker, Maven, Cargo, PyPI, Composer, RubyGems via [`@push.rocks/smartregistry`](https://code.foss.global/push.rocks/smartregistry)
- 🏢 **Organizations & Teams** - Fine-grained access control with role-based permissions - 🏢 **Organizations & Teams** — Hierarchical access control: orgs → teams → repositories
- 🎫 **API Tokens** - Scoped tokens for CI/CD and programmatic access - 🔐 **Flexible Authentication** — Local JWT auth, OAuth/OIDC, and LDAP with JIT user provisioning
- 🔍 **Upstream Caching** - Proxy and cache packages from public registries - 🎫 **Scoped API Tokens** Per-protocol, per-scope tokens (`srg_` prefix) for CI/CD pipelines
- 📊 **Audit Logging** - Complete audit trail for compliance and security - 🛡️ **RBAC Permissions** — Reader → Developer → Maintainer → Admin per repository
- 🎨 **Modern Web UI** - Angular 19 dashboard for package management - 🔍 **Upstream Caching** — Transparently proxy and cache packages from public registries
- **Deno Runtime** - Fast, secure, TypeScript-first backend - 📊 **Audit Logging** Full audit trail on every action for compliance
- 🗄️ **MongoDB + S3** - Scalable storage with smartdata ORM - 🎨 **Modern Web UI** — Angular 19 dashboard with Tailwind CSS, embedded in the binary
-**Single Binary** — Cross-compiled with `deno compile` for Linux and macOS (x64 + ARM64)
- 🗄️ **MongoDB + S3** — Metadata in MongoDB, artifacts in any S3-compatible store
## 🚀 Quick Start ## 🚀 Quick Start
### Prerequisites ### Prerequisites
- **Deno** >= 1.40
- **MongoDB** >= 4.4 - **MongoDB** >= 4.4
- **S3-compatible storage** (MinIO, AWS S3, etc.) - **S3-compatible storage** (MinIO, AWS S3, Cloudflare R2, etc.)
- **Node.js** >= 18 (for UI development)
### Installation ### Install from Binary
```bash ```bash
# Clone the repository # One-liner install (latest version)
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash
# Install specific version
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --version v1.3.0
# Install + set up systemd service
curl -sSL https://code.foss.global/stack.gallery/registry/raw/branch/main/install.sh | sudo bash -s -- --setup-service
```
The installer:
- Detects your platform (Linux/macOS, x64/ARM64)
- Downloads the pre-compiled binary from Gitea releases
- Installs to `/opt/stack-gallery-registry/` with a symlink in `/usr/local/bin/`
- Optionally creates and enables a systemd service
### Run from Source
```bash
# Clone
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 UI dependencies # Development mode (hot reload, reads .nogit/env.json)
cd ui && pnpm install && cd .. deno task dev
# Build the UI # Production mode
pnpm run build deno task start
``` ```
### Configuration The registry is available at `http://localhost:3000`.
Create a `.nogit/env.json` file for local development: ## ⚙️ Configuration
Configuration is loaded from **environment variables** (production) or from **`.nogit/env.json`** when using the `--ephemeral` flag (development).
| Variable | Default | Description |
|----------|---------|-------------|
| `MONGODB_URL` | `mongodb://localhost:27017` | MongoDB connection string |
| `MONGODB_DB` | `stackgallery` | Database name |
| `S3_ENDPOINT` | `http://localhost:9000` | S3-compatible endpoint |
| `S3_ACCESS_KEY` | `minioadmin` | S3 access key |
| `S3_SECRET_KEY` | `minioadmin` | S3 secret key |
| `S3_BUCKET` | `registry` | S3 bucket name |
| `S3_REGION` | — | S3 region |
| `HOST` | `0.0.0.0` | Server bind address |
| `PORT` | `3000` | Server port |
| `JWT_SECRET` | `change-me-in-production` | JWT signing secret |
| `AUTH_ENCRYPTION_KEY` | *(ephemeral)* | 64-char hex for AES-256-GCM encryption of OAuth/LDAP secrets |
| `STORAGE_PATH` | `packages` | Base path in S3 for artifacts |
| `ENABLE_UPSTREAM_CACHE` | `true` | Cache packages from upstream registries |
| `UPSTREAM_CACHE_EXPIRY` | `24` | Cache TTL in hours |
**Example `.nogit/env.json`:**
```json ```json
{ {
"MONGODB_URL": "mongodb://localhost:27017", "MONGODB_URL": "mongodb://admin:pass@localhost:27017/stackregistry?authSource=admin",
"MONGODB_NAME": "stackregistry", "MONGODB_NAME": "stackregistry",
"S3_HOST": "localhost", "S3_HOST": "localhost",
"S3_PORT": "9000", "S3_PORT": "9000",
"S3_ACCESSKEY": "minioadmin", "S3_ACCESSKEY": "minioadmin",
"S3_SECRETKEY": "minioadmin", "S3_SECRETKEY": "minioadmin",
"S3_BUCKET": "registry", "S3_BUCKET": "registry",
"S3_USESSL": false, "S3_USESSL": false
"JWT_SECRET": "your-secure-secret-key",
"ADMIN_EMAIL": "admin@example.com",
"ADMIN_PASSWORD": "your-admin-password"
} }
``` ```
Or use environment variables: ## 🔌 Protocol Endpoints
| Variable | Description | Default | Each protocol is handled natively via [`@push.rocks/smartregistry`](https://code.foss.global/push.rocks/smartregistry). Point your package manager at the registry:
|----------|-------------|---------|
| `MONGODB_URL` | MongoDB connection string | `mongodb://localhost:27017` |
| `MONGODB_DB` | Database name | `stackgallery` |
| `S3_ENDPOINT` | S3 endpoint URL | `http://localhost:9000` |
| `S3_ACCESS_KEY` | S3 access key | `minioadmin` |
| `S3_SECRET_KEY` | S3 secret key | `minioadmin` |
| `S3_BUCKET` | S3 bucket name | `registry` |
| `JWT_SECRET` | JWT signing secret | `change-me-in-production` |
| `ADMIN_EMAIL` | Default admin email | `admin@stack.gallery` |
| `ADMIN_PASSWORD` | Default admin password | `admin` |
| `PORT` | HTTP server port | `3000` |
### Running | Protocol | Paths | Client Config Example |
|----------|-------|-----------------------|
| **NPM** | `/-/*`, `/@scope/*` | `npm config set registry http://registry:3000` |
| **OCI/Docker** | `/v2/*` | `docker login registry:3000` |
| **Maven** | `/maven2/*` | Add repository URL in `pom.xml` |
| **Cargo** | `/api/v1/crates/*` | Configure in `.cargo/config.toml` |
| **PyPI** | `/simple/*`, `/pypi/*` | `pip install --index-url http://registry:3000/simple/` |
| **Composer** | `/packages.json`, `/p/*` | Add repository in `composer.json` |
| **RubyGems** | `/api/v1/gems/*`, `/gems/*` | `gem sources -a http://registry:3000` |
```bash Authentication works with **Bearer tokens** (API tokens prefixed `srg_`) and **Basic auth** (email:password or username:token).
# Development mode (with hot reload)
pnpm run watch
# Production mode ## 🔐 Authentication & Security
deno run --allow-all mod.ts server
# Or with Deno tasks ### Local Auth
deno task start - JWT-based with **15-minute access tokens** and **7-day refresh tokens** (HS256)
- Session tracking — each login creates a session, tokens embed session IDs
- Password hashing with PBKDF2 (10,000 rounds SHA-256 + random salt)
### External Auth (OAuth/OIDC & LDAP)
- **OAuth/OIDC** — Connect to any OIDC-compliant provider (Keycloak, Okta, Auth0, Azure AD, etc.)
- **LDAP** — Bind + search authentication against Active Directory or OpenLDAP
- **JIT Provisioning** — Users are auto-created on first external login
- **Auto-linking** — External identities are linked to existing users by email match
- **Encrypted secrets** — Provider client secrets and bind passwords are stored AES-256-GCM encrypted
### RBAC Permissions
Access is resolved through a hierarchy:
```
Platform Admin (full access)
└─ Organization Owner/Admin
└─ Team Maintainer (read + write + delete on team repos)
└─ Team Member (read + write on team repos)
└─ Direct Repo Permission (reader / developer / maintainer / admin)
└─ Public Repository (read for everyone)
``` ```
The registry will be available at `http://localhost:3000` ### Scoped API Tokens
Tokens are prefixed with `srg_` and can be scoped to:
- Specific **protocols** (e.g., npm + oci only)
- Specific **actions** (read / write / delete)
- Specific **organizations**
- Custom **expiration** dates
## 📡 REST API
All management endpoints live under `/api/v1/`. Authenticated via `Authorization: Bearer <jwt_or_api_token>`.
### Auth
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/auth/login` | Login (email + password) |
| `POST` | `/api/v1/auth/refresh` | Refresh access token |
| `POST` | `/api/v1/auth/logout` | Logout (invalidate session) |
| `GET` | `/api/v1/auth/me` | Current user info |
| `GET` | `/api/v1/auth/providers` | List active external auth providers |
| `GET` | `/api/v1/auth/oauth/:id/authorize` | Initiate OAuth flow |
| `GET` | `/api/v1/auth/oauth/:id/callback` | OAuth callback |
| `POST` | `/api/v1/auth/ldap/:id/login` | LDAP login |
### Users
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/users` | List users |
| `POST` | `/api/v1/users` | Create user |
| `GET` | `/api/v1/users/:id` | Get user |
| `PUT` | `/api/v1/users/:id` | Update user |
| `DELETE` | `/api/v1/users/:id` | Delete user |
### Organizations
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/organizations` | List organizations |
| `POST` | `/api/v1/organizations` | Create organization |
| `GET` | `/api/v1/organizations/:id` | Get organization |
| `PUT` | `/api/v1/organizations/:id` | Update organization |
| `DELETE` | `/api/v1/organizations/:id` | Delete organization |
| `GET` | `/api/v1/organizations/:id/members` | List members |
| `POST` | `/api/v1/organizations/:id/members` | Add member |
| `PUT` | `/api/v1/organizations/:id/members/:userId` | Update member role |
| `DELETE` | `/api/v1/organizations/:id/members/:userId` | Remove member |
### Repositories
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/organizations/:orgId/repositories` | List org repos |
| `POST` | `/api/v1/organizations/:orgId/repositories` | Create repo |
| `GET` | `/api/v1/repositories/:id` | Get repo |
| `PUT` | `/api/v1/repositories/:id` | Update repo |
| `DELETE` | `/api/v1/repositories/:id` | Delete repo |
### Packages
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/packages` | Search packages |
| `GET` | `/api/v1/packages/:id` | Get package details |
| `GET` | `/api/v1/packages/:id/versions` | List versions |
| `DELETE` | `/api/v1/packages/:id` | Delete package |
| `DELETE` | `/api/v1/packages/:id/versions/:version` | Delete version |
### Tokens
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/tokens` | List your tokens |
| `POST` | `/api/v1/tokens` | Create token |
| `DELETE` | `/api/v1/tokens/:id` | Revoke token |
### Audit
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/audit` | Query audit logs |
### Admin (Platform Admins Only)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/auth/providers` | List all auth providers |
| `POST` | `/api/v1/admin/auth/providers` | Create auth provider |
| `GET` | `/api/v1/admin/auth/providers/:id` | Get provider details |
| `PUT` | `/api/v1/admin/auth/providers/:id` | Update provider |
| `DELETE` | `/api/v1/admin/auth/providers/:id` | Disable provider |
| `POST` | `/api/v1/admin/auth/providers/:id/test` | Test provider connection |
| `GET` | `/api/v1/admin/auth/settings` | Get platform settings |
| `PUT` | `/api/v1/admin/auth/settings` | Update platform settings |
### Health Check
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/health` or `/healthz` | Returns JSON status of MongoDB, S3, and registry |
## 🏗️ Architecture ## 🏗️ Architecture
``` ```
registry/ registry/
├── mod.ts # Entry point ├── mod.ts # Deno entry point
├── deno.json # Deno config, tasks, imports
├── npmextra.json # tsdeno compile targets & gitzone config
├── install.sh # Binary installer script
├── .gitea/workflows/ # CI release pipeline
├── scripts/
│ └── bundle-ui.ts # Embeds Angular build as base64 TypeScript
├── ts/ ├── ts/
│ ├── registry.ts # Main StackGalleryRegistry class │ ├── registry.ts # StackGalleryRegistry — main orchestrator
│ ├── cli.ts # CLI command handler │ ├── cli.ts # CLI commands (smartcli)
│ ├── plugins.ts # Centralized dependencies │ ├── plugins.ts # Centralized dependency imports
│ ├── api/ │ ├── api/
│ │ ├── router.ts # REST API router with JWT auth │ │ ├── router.ts # REST API router with JWT/token auth
│ │ └── handlers/ # API endpoint handlers │ │ └── handlers/ # auth, user, org, repo, package, token, audit, oauth, admin
│ ├── models/ # MongoDB models (smartdata) │ ├── models/ # MongoDB models via @push.rocks/smartdata
│ │ ├── user.ts │ │ ├── user.ts, organization.ts, team.ts
│ │ ├── organization.ts │ │ ├── repository.ts, package.ts
│ │ ├── repository.ts │ │ ├── apitoken.ts, session.ts, auditlog.ts
│ │ ├── package.ts │ │ ├── auth.provider.ts, external.identity.ts, platform.settings.ts
│ │ ── session.ts │ │ ── *.member.ts, *.permission.ts
│ └── ... ├── services/ # Business logic
│ ├── services/ # Business logic │ ├── auth.service.ts # JWT login/refresh/logout
│ │ ├── auth.service.ts │ │ ├── external.auth.service.ts # OAuth/OIDC & LDAP flows
│ │ ├── permission.service.ts │ │ ├── crypto.service.ts # AES-256-GCM encryption
│ │ ├── token.service.ts │ │ ├── token.service.ts # API token CRUD
│ │ ── audit.service.ts │ │ ── permission.service.ts # RBAC resolution
├── providers/ # Registry protocol integrations │ └── audit.service.ts # Audit logging
│ ├── auth.provider.ts │ ├── providers/ # smartregistry integration
│ │ ── storage.provider.ts │ │ ── auth.provider.ts # IAuthProvider implementation
│ └── interfaces/ # TypeScript types │ └── storage.provider.ts # IStorageHooks for quota/audit
└── ui/ # Angular 19 web interface │ └── interfaces/ # TypeScript interfaces & types
└── ui/ # Angular 19 + Tailwind CSS frontend
└── src/app/
├── features/ # Login, dashboard, orgs, repos, packages, tokens, admin
├── core/ # Services, guards, interceptors
└── shared/ # Layout, UI components
``` ```
## 📡 API Endpoints
### Authentication
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/auth/login` | Login with email/password |
| `POST` | `/api/v1/auth/refresh` | Refresh access token |
| `POST` | `/api/v1/auth/logout` | Logout (invalidate session) |
| `GET` | `/api/v1/auth/me` | Get current user |
### Organizations
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/organizations` | List organizations |
| `POST` | `/api/v1/organizations` | Create organization |
| `GET` | `/api/v1/organizations/:id` | Get organization details |
| `PUT` | `/api/v1/organizations/:id` | Update organization |
| `DELETE` | `/api/v1/organizations/:id` | Delete organization |
### Repositories
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/organizations/:orgId/repositories` | List repositories |
| `POST` | `/api/v1/organizations/:orgId/repositories` | Create repository |
| `GET` | `/api/v1/repositories/:id` | Get repository details |
### Packages
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/packages` | List packages |
| `GET` | `/api/v1/packages/:id` | Get package details |
| `GET` | `/api/v1/packages/:id/versions` | List package versions |
### API Tokens
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/tokens` | List user's tokens |
| `POST` | `/api/v1/tokens` | Create new token |
| `DELETE` | `/api/v1/tokens/:id` | Revoke token |
## 🔌 Protocol Endpoints
The registry handles protocol-specific endpoints automatically via `@push.rocks/smartregistry`:
- **NPM**: `/-/*`, `/@scope/*`
- **OCI/Docker**: `/v2/*`
- **Maven**: `/maven2/*`
- **PyPI**: `/simple/*`, `/pypi/*`
- **Cargo**: `/api/v1/crates/*`
- **Composer**: `/packages.json`, `/p/*`
- **RubyGems**: `/api/v1/gems/*`, `/gems/*`
## 🔧 Technology Stack ## 🔧 Technology Stack
| Component | Technology | | Component | Technology |
|-----------|------------| |-----------|------------|
| Runtime | Deno | | **Runtime** | Deno 2.x |
| Language | TypeScript | | **Language** | TypeScript (strict mode) |
| Database | MongoDB via `@push.rocks/smartdata` | | **Database** | MongoDB via [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) |
| Storage | S3 via `@push.rocks/smartbucket` | | **Storage** | S3 via [`@push.rocks/smartbucket`](https://code.foss.global/push.rocks/smartbucket) |
| Registry | `@push.rocks/smartregistry` | | **Registry Core** | [`@push.rocks/smartregistry`](https://code.foss.global/push.rocks/smartregistry) |
| Frontend | Angular 19 | | **Frontend** | Angular 19 (Signals, Zoneless) + Tailwind CSS |
| Auth | JWT with session management | | **Auth** | JWT (HS256) + OAuth/OIDC + LDAP |
| **Build** | [`@git.zone/tsdeno`](https://code.foss.global/git.zone/tsdeno) cross-compilation |
| **CI/CD** | Gitea Actions → binary releases |
## 🛡 Security Features ## 🛠 Development
- **JWT Authentication** - Short-lived access tokens with refresh flow ### Commands
- **Session Management** - Track and invalidate active sessions
- **Scoped API Tokens** - Fine-grained permissions per token
- **RBAC** - Organization-level role-based access control
- **Audit Logging** - Comprehensive action logging
- **Password Hashing** - PBKDF2-style hashing with salts
## 📜 CLI Commands
```bash ```bash
# Start the server # Start dev server with hot reload (reads .nogit/env.json)
deno run --allow-all mod.ts server [--ephemeral] deno task dev
# Show help # Watch mode: backend + UI + bundler concurrently
deno run --allow-all mod.ts help pnpm run watch
# Build Angular UI
deno task build
# Bundle UI into embedded TypeScript
deno task bundle-ui
# Cross-compile binaries for all platforms
deno task compile
# Type check / format / lint
deno task check
deno task fmt
deno task lint
# Run tests
deno task test # All tests
deno task test:unit # Unit tests only
deno task test:integration # Integration tests (requires running server)
deno task test:e2e # E2E tests (requires running server + services)
``` ```
Options: ### Build & Release
- `--ephemeral` / `-e` - Load config from `.nogit/env.json` instead of environment variables
Releases are automated via Gitea Actions (`.gitea/workflows/release.yml`):
1. Push a `v*` tag
2. CI builds the Angular UI and bundles it into TypeScript
3. `tsdeno compile` produces binaries for 4 platforms (linux-x64, linux-arm64, macos-x64, macos-arm64)
4. Binaries + SHA256 checksums are uploaded as Gitea release assets
Compile targets are configured in `npmextra.json` under `@git.zone/tsdeno`.
### Storage Layout
Artifacts are stored in S3 at:
```
{storagePath}/{protocol}/{orgName}/{packageName}/{version}/{filename}
```
For example: `packages/npm/myorg/mypackage/1.0.0/mypackage-1.0.0.tgz`
## License and Legal Information ## License and Legal Information
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. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**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. **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 ### Trademarks
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. 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.
### Company Information ### Company Information
Task Venture Capital GmbH Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require 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 terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. 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.

View File

@@ -1,17 +1,37 @@
/** /**
* Test configuration for Stack.Gallery Registry tests * Test configuration for Stack.Gallery Registry tests
* Uses @push.rocks/qenv to read from .nogit/env.json
*/ */
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', '.nogit/', false);
const mongoUrl = await testQenv.getEnvVarOnDemand('MONGODB_URL')
|| 'mongodb://testadmin:testpass@localhost:27117/test-registry?authSource=admin';
const mongoName = await testQenv.getEnvVarOnDemand('MONGODB_NAME')
|| 'test-registry';
const s3Endpoint = await testQenv.getEnvVarOnDemand('S3_ENDPOINT') || 'localhost';
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT') || '9100';
const s3AccessKey = await testQenv.getEnvVarOnDemand('S3_ACCESSKEY') || 'testadmin';
const s3SecretKey = await testQenv.getEnvVarOnDemand('S3_SECRETKEY') || 'testpassword';
const s3Bucket = await testQenv.getEnvVarOnDemand('S3_BUCKET') || 'test-registry';
const s3UseSsl = await testQenv.getEnvVarOnDemand('S3_USESSL');
const s3Protocol = s3UseSsl === 'true' ? 'https' : 'http';
const s3EndpointUrl = `${s3Protocol}://${s3Endpoint}:${s3Port}`;
export const testConfig = { export const testConfig = {
mongodb: { mongodb: {
url: 'mongodb://testadmin:testpass@localhost:27117/test-registry?authSource=admin', url: mongoUrl,
name: 'test-registry', name: mongoName,
}, },
s3: { s3: {
endpoint: 'http://localhost:9100', endpoint: s3EndpointUrl,
accessKey: 'testadmin', accessKey: s3AccessKey,
secretKey: 'testpassword', secretKey: s3SecretKey,
bucket: 'test-registry', bucket: s3Bucket,
region: 'us-east-1', region: 'us-east-1',
}, },
jwt: { jwt: {
@@ -35,26 +55,8 @@ export const testConfig = {
}; };
/** /**
* Get test config with environment variable overrides * Get test config (kept for backward compatibility)
*/ */
export function getTestConfig() { export function getTestConfig() {
return { return testConfig;
...testConfig,
mongodb: {
...testConfig.mongodb,
url: Deno.env.get('TEST_MONGODB_URL') || testConfig.mongodb.url,
name: Deno.env.get('TEST_MONGODB_NAME') || testConfig.mongodb.name,
},
s3: {
...testConfig.s3,
endpoint: Deno.env.get('TEST_S3_ENDPOINT') || testConfig.s3.endpoint,
accessKey: Deno.env.get('TEST_S3_ACCESS_KEY') || testConfig.s3.accessKey,
secretKey: Deno.env.get('TEST_S3_SECRET_KEY') || testConfig.s3.secretKey,
bucket: Deno.env.get('TEST_S3_BUCKET') || testConfig.s3.bucket,
},
registry: {
...testConfig.registry,
url: Deno.env.get('TEST_REGISTRY_URL') || testConfig.registry.url,
},
};
} }

View File

@@ -46,10 +46,9 @@ describe('Package Model', () => {
return { return {
version, version,
publishedAt: new Date(), publishedAt: new Date(),
publishedBy: testUserId, publishedById: testUserId,
size: 1024, size: 1024,
checksum: `sha256-${crypto.randomUUID()}`, digest: `sha256:${crypto.randomUUID()}`,
checksumAlgorithm: 'sha256',
downloads: 0, downloads: 0,
metadata: {}, metadata: {},
}; };
@@ -124,7 +123,7 @@ describe('Package Model', () => {
await createPackage('find-this'); await createPackage('find-this');
await createPackage('other'); await createPackage('other');
const results = await Package.search('search'); const results = await Package.searchPackages('search');
assertEquals(results.length, 1); assertEquals(results.length, 1);
assertEquals(results[0].name, 'search-me'); assertEquals(results[0].name, 'search-me');
}); });
@@ -134,14 +133,14 @@ describe('Package Model', () => {
pkg.description = 'A unique description for testing'; pkg.description = 'A unique description for testing';
await pkg.save(); await pkg.save();
const results = await Package.search('unique description'); const results = await Package.searchPackages('unique description');
assertEquals(results.length, 1); assertEquals(results.length, 1);
}); });
it('should filter by protocol', async () => { it('should filter by protocol', async () => {
await createPackage('npm-pkg'); await createPackage('npm-pkg');
const results = await Package.search('npm', { protocol: 'oci' }); const results = await Package.searchPackages('npm', { protocol: 'oci' });
assertEquals(results.length, 0); assertEquals(results.length, 0);
}); });
@@ -150,10 +149,10 @@ describe('Package Model', () => {
await createPackage('page2'); await createPackage('page2');
await createPackage('page3'); await createPackage('page3');
const firstPage = await Package.search('page', { limit: 2, offset: 0 }); const firstPage = await Package.searchPackages('page', { limit: 2, offset: 0 });
assertEquals(firstPage.length, 2); assertEquals(firstPage.length, 2);
const secondPage = await Package.search('page', { limit: 2, offset: 2 }); const secondPage = await Package.searchPackages('page', { limit: 2, offset: 2 });
assertEquals(secondPage.length, 1); assertEquals(secondPage.length, 1);
}); });
}); });

View File

@@ -104,8 +104,8 @@ describe('TokenService', () => {
const validation = await tokenService.validateToken(rawToken, '127.0.0.1'); const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertExists(validation); assertExists(validation);
assertEquals(validation.userId, testUserId); assertEquals(validation.token!.userId, testUserId);
assertEquals(validation.protocols.includes('npm'), true); assertEquals(validation.token!.protocols.includes('npm'), true);
}); });
it('should reject invalid token format', async () => { it('should reject invalid token format', async () => {

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@stack.gallery/registry', name: '@stack.gallery/registry',
version: '1.3.0', version: '1.4.0',
description: 'Enterprise-grade multi-protocol package registry' description: 'Enterprise-grade multi-protocol package registry'
} }

View File

@@ -29,7 +29,7 @@ export class PackageApi {
// For anonymous users, only search public packages // For anonymous users, only search public packages
const isPrivate = ctx.actor?.userId ? undefined : false; const isPrivate = ctx.actor?.userId ? undefined : false;
const packages = await Package.search(query, { const packages = await Package.searchPackages(query, {
protocol, protocol,
organizationId, organizationId,
isPrivate, isPrivate,

View File

@@ -23,7 +23,7 @@ export class ApiToken extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToke
public createdById?: string; // Who created the token (for audit) public createdById?: string; // Who created the token (for audit)
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
public name: string = ''; public override name: string = '';
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.index({ unique: true }) @plugins.smartdata.index({ unique: true })

View File

@@ -36,7 +36,7 @@ export class AuthProvider
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.index({ unique: true }) @plugins.smartdata.index({ unique: true })
public name: string = ''; // URL-safe slug identifier public override name: string = ''; // URL-safe slug identifier
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()

View File

@@ -25,7 +25,7 @@ export class Organization extends plugins.smartdata.SmartDataDbDoc<Organization,
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()
@plugins.smartdata.index({ unique: true }) @plugins.smartdata.index({ unique: true })
public name: string = ''; // URL-safe slug public override name: string = ''; // URL-safe slug
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()

View File

@@ -31,7 +31,7 @@ export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package>
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()
@plugins.smartdata.index() @plugins.smartdata.index()
public name: string = ''; public override name: string = '';
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()
@@ -110,7 +110,7 @@ export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package>
/** /**
* Search packages * Search packages
*/ */
public static async search( public static async searchPackages(
query: string, query: string,
options?: { options?: {
protocol?: TRegistryProtocol; protocol?: TRegistryProtocol;

View File

@@ -17,7 +17,7 @@ export class Repository extends plugins.smartdata.SmartDataDbDoc<Repository, Rep
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()
public name: string = ''; public override name: string = '';
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
public description?: string; public description?: string;

View File

@@ -17,7 +17,7 @@ export class Team extends plugins.smartdata.SmartDataDbDoc<Team, Team> implement
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@plugins.smartdata.searchable() @plugins.smartdata.searchable()
public name: string = ''; public override name: string = '';
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
public description?: string; public description?: string;

View File

@@ -109,13 +109,13 @@ export class AuditService {
public async logUserLogin(userId: string, success: boolean, errorMessage?: string): Promise<AuditLog> { public async logUserLogin(userId: string, success: boolean, errorMessage?: string): Promise<AuditLog> {
if (success) { if (success) {
return await this.logSuccess('USER_LOGIN', 'user', userId); return await this.logSuccess('AUTH_LOGIN', 'user', userId);
} }
return await this.logFailure('USER_LOGIN', 'user', 'LOGIN_FAILED', errorMessage || 'Login failed', userId); return await this.logFailure('AUTH_LOGIN', 'user', 'LOGIN_FAILED', errorMessage || 'Login failed', userId);
} }
public async logUserLogout(userId: string): Promise<AuditLog> { public async logUserLogout(userId: string): Promise<AuditLog> {
return await this.logSuccess('USER_LOGOUT', 'user', userId); return await this.logSuccess('AUTH_LOGOUT', 'user', userId);
} }
public async logTokenCreated(tokenId: string, tokenName: string): Promise<AuditLog> { public async logTokenCreated(tokenId: string, tokenName: string): Promise<AuditLog> {
@@ -133,7 +133,7 @@ export class AuditService {
organizationId: string, organizationId: string,
repositoryId: string repositoryId: string
): Promise<AuditLog> { ): Promise<AuditLog> {
return await this.log('PACKAGE_PUBLISHED', 'package', { return await this.log('PACKAGE_PUSHED', 'package', {
resourceId: packageId, resourceId: packageId,
resourceName: packageName, resourceName: packageName,
organizationId, organizationId,
@@ -150,7 +150,7 @@ export class AuditService {
organizationId: string, organizationId: string,
repositoryId: string repositoryId: string
): Promise<AuditLog> { ): Promise<AuditLog> {
return await this.log('PACKAGE_DOWNLOADED', 'package', { return await this.log('PACKAGE_PULLED', 'package', {
resourceId: packageId, resourceId: packageId,
resourceName: packageName, resourceName: packageName,
organizationId, organizationId,
@@ -161,7 +161,7 @@ export class AuditService {
} }
public async logOrganizationCreated(orgId: string, orgName: string): Promise<AuditLog> { public async logOrganizationCreated(orgId: string, orgName: string): Promise<AuditLog> {
return await this.logSuccess('ORGANIZATION_CREATED', 'organization', orgId, orgName); return await this.logSuccess('ORG_CREATED', 'organization', orgId, orgName);
} }
public async logRepositoryCreated( public async logRepositoryCreated(
@@ -169,7 +169,7 @@ export class AuditService {
repoName: string, repoName: string,
organizationId: string organizationId: string
): Promise<AuditLog> { ): Promise<AuditLog> {
return await this.log('REPOSITORY_CREATED', 'repository', { return await this.log('REPO_CREATED', 'repository', {
resourceId: repoId, resourceId: repoId,
resourceName: repoName, resourceName: repoName,
organizationId, organizationId,
@@ -184,7 +184,7 @@ export class AuditService {
oldRole: string | null, oldRole: string | null,
newRole: string | null newRole: string | null
): Promise<AuditLog> { ): Promise<AuditLog> {
return await this.log('PERMISSION_CHANGED', resourceType, { return await this.log('ORG_MEMBER_ROLE_CHANGED', resourceType, {
resourceId, resourceId,
metadata: { metadata: {
targetUserId, targetUserId,

View File

@@ -226,7 +226,7 @@ export class AuthService {
actorId: userId, actorId: userId,
actorType: 'user', actorType: 'user',
actorIp: options.ipAddress, actorIp: options.ipAddress,
}).log('USER_LOGOUT', 'user', { }).log('AUTH_LOGOUT', 'user', {
resourceId: userId, resourceId: userId,
metadata: { sessionsInvalidated: count }, metadata: { sessionsInvalidated: count },
success: true, success: true,

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"lib": ["ES2022"],
"types": ["node"],
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["ts/**/*", "mod.ts"],
"exclude": ["node_modules", "dist", "ui"]
}