{"id":49767413,"url":"https://github.com/josunlp/publaryn","last_synced_at":"2026-05-11T11:01:39.681Z","repository":{"id":352255457,"uuid":"1211577819","full_name":"JosunLP/Publaryn","owner":"JosunLP","description":"A self-hostable, security-first package registry platform that speaks the native protocols of all major package managers and provides a unified management API.","archived":false,"fork":false,"pushed_at":"2026-05-05T10:18:31.000Z","size":1187,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T12:24:31.279Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://josunlp.github.io/Publaryn/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JosunLP.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":"SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"josunlp"}},"created_at":"2026-04-15T14:29:10.000Z","updated_at":"2026-05-05T10:18:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/JosunLP/Publaryn","commit_stats":null,"previous_names":["josunlp/publaryn"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/JosunLP/Publaryn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosunLP%2FPublaryn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosunLP%2FPublaryn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosunLP%2FPublaryn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosunLP%2FPublaryn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JosunLP","download_url":"https://codeload.github.com/JosunLP/Publaryn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosunLP%2FPublaryn/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32891966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-10T13:40:02.631Z","status":"online","status_checked_at":"2026-05-11T02:00:05.975Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-11T11:01:37.600Z","updated_at":"2026-05-11T11:01:39.673Z","avatar_url":"https://github.com/JosunLP.png","language":"Rust","funding_links":["https://github.com/sponsors/josunlp"],"categories":[],"sub_categories":[],"readme":"# Publaryn\n\n**Publaryn** — secure, independent package registry across ecosystems. Developed by AI Agents, built on Rust, and designed for security-conscious teams who want to host their own package registry without sacrificing the native experience of their ecosystem's tools.\n\nA self-hostable, security-first package registry platform that speaks the native protocols of all major package managers and provides a unified management API.\n\n---\n\n## Supported Ecosystems\n\n| Ecosystem    | Mount path                      | 1.0 baseline                                                                                        | Status                 |\n| ------------ | ------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------- |\n| npm / Bun    | `/npm`                          | packument reads, tarball download, search, publish, dist-tags                                       | ✅ Baseline implemented |\n| pip / PyPI   | `/pypi` plus `/_/oidc/*`        | Simple API, file download, legacy upload, trusted-publishing token exchange                         | ✅ Baseline implemented |\n| Rust Crates  | `/cargo/index`, `/cargo/api/v1` | sparse index, publish, download, search, yank, unyank, compatibility owner endpoints                | ✅ Baseline implemented |\n| NuGet        | `/nuget`                        | service index, flat container, registration, search, push, unlist, relist                           | ✅ Baseline implemented |\n| Apache Maven | `/maven`                        | repository reads, metadata generation, checksum reads, deploy-style PUT upload                      | ✅ Baseline implemented |\n| RubyGems     | `/rubygems`                     | metadata reads, version listing, gem download, push, yank, API key echo                             | ✅ Baseline implemented |\n| Composer     | `/composer`                     | packages index, package metadata, dist download, publish, yank                                      | ✅ Baseline implemented |\n| Containers   | `/oci`                          | probe, catalog, manifests, blobs, uploads, tags, referrers, delete semantics, orphaned blob cleanup | ✅ Baseline implemented |\n\n\u003e **Note:** Bun uses the npm adapter — no separate protocol implementation is required.\n\nThe current documented baseline includes native publish/read flows for every ecosystem above, shared quarantine → scan → publish lifecycle controls, ecosystem-aware package/release detail responses, bundle-style analysis summaries for package and release detail views, and a SvelteKit web portal that can browse package metadata, releases, security findings, trusted publishers, and OCI manifest references. The OCI adapter now also exposes native referrers discovery for subject-linked manifests (for example SBOMs or signatures already pushed through the registry) and inventory-backed background cleanup for unreferenced config/layer blobs, while broader attestation policy, signing UX, proxy/mirror/virtual repository lifecycle features, and richer discovery/ranking features remain intentionally separate follow-on work.\n\n---\n\n## 1.0 Product Contract\n\nPublaryn is now targeting **1.1.1** as the current stable release on top of the production-ready 1.0 baseline for:\n\n- self-hosted multi-ecosystem package hosting\n- native client compatibility across all currently mounted adapters\n- organization-centric governance and delegated team access\n- quarantine-first publication with background scanning and security findings\n- an integrated web portal for package discovery, package details, version details, settings, and organization workspaces\n\nThe detailed 1.0 contract lives in [docs/1.0.md](docs/1.0.md). It defines:\n\n- the **1.0 scope**\n- the **API and adapter matrix**\n- visibility, search, and security-flow behavior\n- the **release criteria**\n- the support and compatibility policy\n\nFor architecture decisions, use the indexed ADR catalog in [docs/adr/README.md](docs/adr/README.md).\n\n### 1.0 management surface\n\nThe documented 1.0 control-plane surface covers:\n\n- `/v1/auth/*`\n- `/v1/users/*`\n- `/v1/orgs/*`\n- `/v1/org-invitations/*`\n- `/v1/namespaces/*`\n- `/v1/repositories/*`\n- `/v1/packages/*`\n- `GET /v1/search`\n- `/v1/tokens*`\n- `/v1/audit`\n- `/v1/admin/jobs`\n- `/v1/stats`\n- `/health`\n- `/readiness`\n\n### In scope for 1.0\n\n- management API for auth, users, organizations, teams, repositories, packages, releases, tokens, audit, search, security findings, trusted publishers, and namespace claims\n- hosted repository kinds `public`, `private`, `staging`, and `release` for self-hosted package ownership and visibility boundaries\n- native protocol adapters for npm/Bun, PyPI, Cargo, NuGet, Maven, RubyGems, Composer, and OCI\n- actor-aware visibility enforcement for control-plane reads and package search\n- delegated package, repository, and namespace access for organization teams\n- background jobs for scanning, reindexing, cleanup, and operational queue handling\n- documented validation, CI, and container build pipeline\n\n### Explicitly out of scope for 1.0\n\n- proxy, mirror, and virtual repositories\n- Maven snapshot repositories and promotion workflows\n- enterprise SSO, SAML, and SCIM\n- billing, quotas as a product tiering system, and monetization features\n- full attestation policy, signing UX, and deep Sigstore integration\n- federated registries, regional replication, and air-gapped synchronization\n- operator-facing abuse and takedown consoles beyond the current quarantine and audit primitives\n\n### 1.0 release gate\n\nPublaryn should only call itself 1.0-ready when:\n\n- the README, [docs/concept.md](docs/concept.md), [docs/1.0.md](docs/1.0.md), and [docs/adr/README.md](docs/adr/README.md) agree on scope and deferrals\n- every mounted adapter has documented publish/read/auth behavior and targeted regression coverage for its baseline flows\n- the documented Rust and frontend CI checks pass\n- the Docker smoke build succeeds\n- release notes clearly separate supported, unsupported, and deferred features\n\n---\n\n## Architecture\n\n```text\n                     ┌────────────────────┐\n                     │     Web Portal /   │\n                     │     Admin UI       │\n                     └────────┬───────────┘\n                              │\n                              ▼\n┌──────────────┐    ┌──────────────────────┐    ┌──────────────────┐\n│ Native       │    │  Management REST API │    │ Auth / Identity  │\n│ Clients      ├───►│  /v1/*              ├───►│ OIDC / MFA / JWT │\n│ npm, pip,    │    │  OpenAPI / Swagger  │    │ Tokens / Sessions │\n│ cargo, etc.  │    └──────────┬──────────┘    └────────┬─────────┘\n└──────┬───────┘               │                        │\n       │                       ▼                        ▼\n       │          ┌─────────────────────────────────────────────┐\n       │          │            Rust Core Application             │\n       │          │─────────────────────────────────────────────│\n       │          │ Package Domain │ Org/Teams │ Policy Engine  │\n       │          │ Publish Pipeline │ Audit    │ Namespace Mgmt │\n       │          │ Security Findings │ Search  │ Provenance     │\n       └─────────►└──┬──────────────┬──────────┬───────────────┘\n                     │              │           │\n                     ▼              ▼           ▼\n              ┌─────────┐  ┌──────────────┐  ┌──────────────┐\n              │Protocol │  │   Background  │  │ Scan/Policy  │\n              │Adapters │  │   Workers     │  │ Workers      │\n              │npm/pip/ │  │   indexing    │  │ ClamAV/YARA  │\n              │cargo/   │  │   gc/events   │  │ Trivy/Grype  │\n              └────┬────┘  └──────┬────────┘  └──────┬───────┘\n                   │              │                   │\n                   └──────────────┴───────────────────┘\n                                  │\n          ┌───────────────────────┼──────────────────────────┐\n          ▼                       ▼                          ▼\n  ┌──────────────┐    ┌───────────────────────┐    ┌────────────────┐\n  │  PostgreSQL  │    │  S3 / MinIO Artifacts │    │   Meilisearch  │\n  │  metadata,   │    │  immutable blob store │    │   full-text    │\n  │  audit, auth │    └───────────────────────┘    │   search       │\n  └──────────────┘                                 └────────────────┘\n          │\n  ┌───────┴───────┐\n  │     Redis     │\n  │ cache / rate  │\n  │ limit / sess. │\n  └───────────────┘\n```\n\n### Crate Structure\n\n```text\ncrates/\n├── core/               # Domain models, errors, validation, policy\n├── api/                # HTTP server (axum) — REST + OpenAPI\n├── auth/               # Authentication: passwords, JWT, OIDC, MFA\n├── search/             # Search index (Meilisearch adapter)\n└── adapters/\n    ├── npm/            # npm / Bun registry adapter\n    ├── pypi/           # PyPI Simple Index adapter\n    ├── cargo-registry/ # Cargo sparse index adapter\n    ├── maven/          # Maven2 adapter\n    ├── nuget/          # NuGet v3 adapter\n    ├── rubygems/       # RubyGems compact index adapter\n    ├── composer/       # Composer repository adapter\n    └── oci/            # OCI Distribution API adapter\n```\n\n---\n\n## Local Development\n\n### Prerequisites\n\n- [Rust](https://rustup.rs/) 1.77+\n- [Docker](https://docs.docker.com/get-docker/) + Docker Compose\n- [Bun](https://bun.sh/) 1.3+ for the frontend toolchain\n\n### Quick Start\n\n```bash\n# 1. Start infrastructure (Postgres, Redis, MinIO, Meilisearch)\ndocker compose up -d postgres redis minio meilisearch\n\n# 2. Copy env config\ncp .env.example .env\n\n# Optional: allow a separate frontend origin during local development\n# SERVER__CORS_ALLOWED_ORIGINS=http://localhost:5173\n\n# 3. Run the API server\ncargo run --bin publaryn\n\n# 4. In a second terminal, run the frontend\ncd frontend\nbun install\nbun run dev\n```\n\nThe API is available at `http://localhost:3000`.\nThe frontend is available at `http://localhost:5173`.\nSwagger UI at `http://localhost:3000/swagger-ui`.\n\nThe web portal is a Bun-managed SvelteKit + Tailwind CSS application that builds to static assets under `frontend/dist` and is served by the API in containerized deployments.\n\n### Full Stack (includes API container)\n\n```bash\ndocker compose up --build\n```\n\n---\n\n## API Overview\n\n### Authentication\n\n```http\nPOST /v1/auth/register\nPOST /v1/auth/login\nPOST /v1/auth/logout\n```\n\n## Control-plane Authentication\n\nMutable management endpoints under `/v1/*` require an `Authorization: Bearer ...` header.\n\nSupported bearer credentials:\n\n- JWT access tokens returned by `POST /v1/auth/login`\n- Opaque API tokens created via `POST /v1/tokens`\n\nOwnership-sensitive fields are derived from the authenticated actor instead of trusting request payload values.\nFor example, user-owned namespaces and repositories are created for the authenticated user, and organization-owned mutations require owner or admin membership in the owning organization.\n\nInitial control-plane scopes:\n\n| Scope                   | Purpose                                                           |\n| ----------------------- | ----------------------------------------------------------------- |\n| `profile:write`         | Update the authenticated user's profile                           |\n| `tokens:read`           | List the authenticated user's API tokens                          |\n| `tokens:write`          | Create and revoke API tokens                                      |\n| `orgs:write`            | Create organizations and mutate organization governance data      |\n| `orgs:join`             | Review, accept, and decline invitations for the current user      |\n| `orgs:transfer`         | Transfer organization ownership to another active member          |\n| `namespaces:write`      | Create namespace claims                                           |\n| `namespaces:transfer`   | Transfer namespace claims into an organization you administer     |\n| `repositories:write`    | Create and update repositories                                    |\n| `repositories:transfer` | Transfer repository ownership into an organization you administer |\n| `packages:write`        | Update packages, releases, tags, and trusted publishers           |\n| `packages:transfer`     | Transfer package ownership into an organization you administer    |\n| `audit:read`            | Read the platform audit log (platform administrators only)        |\n\nJWT login sessions receive a default interactive scope set for standard self-service control-plane actions.\nOpaque API tokens must request one or more supported scopes, and unsupported scope strings are rejected.\n\nThe first invitation slice supports invitations for existing active user accounts. Invited users discover pending invitations through authenticated control-plane endpoints and can accept or decline them in product.\n\nThe first ownership-transfer slice allows a current organization owner to hand off their owner role to another existing active member. The transfer is applied atomically, the initiating owner is demoted to `admin`, and the action is written to the audit log.\n\nThe first repository-transfer slice allows a repository owner or delegated repository transfer maintainer to move a repository into an organization they already administer. This supports personal-to-organization handoff and organization-to-organization transfer when the authenticated actor controls both sides. Existing repository-scoped team grants are revoked during the move, while package ownership intentionally remains unchanged.\n\nThe first package-transfer slice allows a package owner to move a package into an organization they already administer. This supports personal-to-organization handoff and organization-to-organization transfer when the authenticated actor controls both sides. Direct transfer to another user account is intentionally deferred until an acceptance-based flow exists.\n\n### Users\n\n```http\nGET    /v1/users/:username\nPATCH  /v1/users/:username\nGET    /v1/users/:username/packages\n```\n\n### Organizations \u0026 Teams\n\n```http\nPOST   /v1/orgs\nGET    /v1/orgs/:slug\nPATCH  /v1/orgs/:slug\nGET    /v1/orgs/:slug/audit\nGET    /v1/orgs/:slug/audit/export\nGET    /v1/orgs/:slug/members\nPOST   /v1/orgs/:slug/members\nDELETE /v1/orgs/:slug/members/:username\nPOST   /v1/orgs/:slug/ownership-transfer\nGET    /v1/orgs/:slug/invitations\nPOST   /v1/orgs/:slug/invitations\nDELETE /v1/orgs/:slug/invitations/:id\nGET    /v1/orgs/:slug/teams\nPOST   /v1/orgs/:slug/teams\nPATCH  /v1/orgs/:slug/teams/:team_slug\nDELETE /v1/orgs/:slug/teams/:team_slug\nGET    /v1/orgs/:slug/teams/:team_slug/members\nPOST   /v1/orgs/:slug/teams/:team_slug/members\nDELETE /v1/orgs/:slug/teams/:team_slug/members/:username\nGET    /v1/orgs/:slug/teams/:team_slug/package-access\nPUT    /v1/orgs/:slug/teams/:team_slug/package-access/:ecosystem/:name\nDELETE /v1/orgs/:slug/teams/:team_slug/package-access/:ecosystem/:name\nGET    /v1/orgs/:slug/teams/:team_slug/repository-access\nPUT    /v1/orgs/:slug/teams/:team_slug/repository-access/:repository_slug\nDELETE /v1/orgs/:slug/teams/:team_slug/repository-access/:repository_slug\nGET    /v1/orgs/:slug/teams/:team_slug/namespace-access\nPUT    /v1/orgs/:slug/teams/:team_slug/namespace-access/:claim_id\nDELETE /v1/orgs/:slug/teams/:team_slug/namespace-access/:claim_id\nGET    /v1/orgs/:slug/repositories\nGET    /v1/orgs/:slug/security-findings\nGET    /v1/orgs/:slug/security-findings/export\nGET    /v1/orgs/:slug/packages\nGET    /v1/org-invitations\nPOST   /v1/org-invitations/:id/accept\nPOST   /v1/org-invitations/:id/decline\n```\n\nOrganization administrators can delegate package responsibilities to teams for organization-owned packages.\nMember and team directory reads are available to authenticated organization members by default, and organizations can mark the directory private to restrict those reads to owners and admins. Audit, invitation, delegated-access, and ownership-transfer routes keep their stricter role-based checks.\nCurrent package-scoped team permissions are `admin`, `publish`, `write_metadata`, `read_private`, `security_review`, and `transfer_ownership`.\nThese grants are stored in PostgreSQL, enforced by the management API, and automatically cleared when package ownership moves to a different organization.\nOrganization administrators can also delegate repository-scoped responsibilities to teams for organization-owned repositories.\nRepository-wide grants use the same permission vocabulary, apply across current and future packages in the selected repository, and the `admin` permission additionally allows repository configuration changes.\nThese repository grants are stored in PostgreSQL, enforced by the management API, and automatically cleared when the team or repository is removed.\nOrganization administrators can also delegate namespace-claim management to teams for organization-owned namespace claims.\nNamespace grants currently support `admin` for namespace-claim administration and `transfer_ownership` for ownership handoff flows; both are stored in PostgreSQL, enforced by the management API, and cleared when the claim changes owners or is deleted.\nRepository ownership can be transferred through `POST /v1/repositories/:slug/ownership-transfer` when the caller has `repositories:transfer`, currently controls the source repository, and is also an owner/admin in the target organization.\nCross-organization repository transfers revoke any repository-scoped team grants tied to the previous owner organization, but they do not automatically re-home packages that already belong to the repository.\nThe organization workspace also includes an aggregated security overview backed by `GET /v1/orgs/:slug/security-findings`, scoped to the packages currently visible to the requesting actor.\nThat endpoint and `GET /v1/orgs/:slug/security-findings/export` both accept the same unresolved-finding filters: repeated or comma-separated `severity` values, a single `ecosystem`, and a package-name substring through `package`.\nThe CSV export applies the same filters as the JSON view and remains visibility-aware, so anonymous actors only receive public package rows while authorized actors can export the broader package set they are allowed to see.\nOrganization audit reads now support action, actor, pagination, and UTC date-range filtering through the `occurred_from` and `occurred_until` query parameters.\nOrganization administrators can also export the full filtered audit view as CSV through `GET /v1/orgs/:slug/audit/export`; the export applies the same action, actor, and UTC date filters but ignores pagination.\n\n### Namespace Claims\n\n```http\nGET    /v1/namespaces\nPOST   /v1/namespaces\nDELETE /v1/namespaces/:id\nPOST   /v1/namespaces/:id/ownership-transfer\nGET    /v1/namespaces/lookup?ecosystem=\u003ceco\u003e\u0026namespace=\u003cclaim\u003e\n```\n\n### Repositories\n\n```http\nPOST   /v1/repositories\nGET    /v1/repositories/:slug\nPATCH  /v1/repositories/:slug\nPOST   /v1/repositories/:slug/ownership-transfer\nGET    /v1/repositories/:slug/packages\n```\n\n### Packages \u0026 Releases\n\n```http\nPOST   /v1/packages\nGET    /v1/packages/:ecosystem/:name\nPATCH  /v1/packages/:ecosystem/:name\nDELETE /v1/packages/:ecosystem/:name\nPOST   /v1/packages/:ecosystem/:name/ownership-transfer\nPOST   /v1/packages/:ecosystem/:name/releases\nGET    /v1/packages/:ecosystem/:name/releases\nGET    /v1/packages/:ecosystem/:name/releases/:version\nPOST   /v1/packages/:ecosystem/:name/releases/:version/publish\nGET    /v1/packages/:ecosystem/:name/releases/:version/artifacts\nPUT    /v1/packages/:ecosystem/:name/releases/:version/artifacts/:filename?kind=\u003ckind\u003e\nGET    /v1/packages/:ecosystem/:name/releases/:version/artifacts/:filename\nPUT    /v1/packages/:ecosystem/:name/releases/:version/yank\nPUT    /v1/packages/:ecosystem/:name/releases/:version/unyank\nPUT    /v1/packages/:ecosystem/:name/releases/:version/deprecate\nPUT    /v1/packages/:ecosystem/:name/releases/:version/undeprecate\nGET    /v1/packages/:ecosystem/:name/tags\nPUT    /v1/packages/:ecosystem/:name/tags/:tag\nDELETE /v1/packages/:ecosystem/:name/tags/:tag\nGET    /v1/packages/:ecosystem/:name/security-findings\nPATCH  /v1/packages/:ecosystem/:name/security-findings/:finding_id\nGET    /v1/packages/:ecosystem/:name/trusted-publishers\nPOST   /v1/packages/:ecosystem/:name/trusted-publishers\n```\n\nRelease history responses include published, deprecated, and yanked versions so maintainers and consumers can inspect full version state. Yanked releases can be restored with the dedicated unyank endpoint.\n\n`GET /v1/packages/:ecosystem/:name` and `GET /v1/packages/:ecosystem/:name/releases/:version` also return an `ecosystem_metadata` block. This nested payload preserves native package coordinates and release metadata such as Cargo dependencies/features, NuGet dependency groups, RubyGems runtime requirements, Maven and Composer provenance, and OCI manifest/blob references so API clients and the web portal can render ecosystem-correct details without reverse-engineering adapter storage.\n\nThe control-plane publish workflow is now explicit and quarantine-first:\n\n1. create the release in `quarantine`\n2. upload one or more immutable artifacts into shared object storage\n3. publish the release once artifact metadata and blobs are present consistently\n\nQuarantined and scanning releases are intentionally hidden from public direct reads and artifact downloads. They remain visible only to authorized maintainers and reviewers on the private management side.\n\nPackage maintainers (including team-delegated reviewers with the `security_review` package or repository permission) can resolve or reopen individual security findings through `PATCH /v1/packages/:ecosystem/:name/security-findings/:finding_id` with body `{ \"is_resolved\": bool, \"note\"?: string }`. Every state transition records a `security_finding_resolve` or `security_finding_reopen` audit event, including any supplied note in the audit metadata. For organization-owned packages, those events also appear in organization audit views and CSV exports. `GET /v1/packages/:ecosystem/:name` returns a `can_manage_security` flag that reflects whether the authenticated caller holds the `security_review` requirement, so dedicated reviewers without release-management permissions can see triage controls without being granted broader publish rights.\n\nArtifact uploads are idempotent by filename and content. Repeating the same upload for the same release and filename returns the existing artifact metadata instead of creating duplicates.\n\nPackage and repository read endpoints enforce explicit visibility semantics.\n`public` resources are readable and discoverable by everyone.\n`unlisted` resources remain readable through direct URLs but are intentionally excluded from search and package listing surfaces.\n`private`, `internal_org`, and `quarantined` resources require authenticated visibility through ownership, organization membership, or delegated team access.\nPackage administrators can update package visibility through the package settings API and web portal, but package visibility cannot be broader than the enclosing repository visibility.\n\nControl-plane package creation derives package ownership from the target repository instead of trusting caller-supplied owner fields.\nFor the current slice, package names are also enforced as globally unique within an ecosystem so the existing `/v1/packages/:ecosystem/:name` control-plane paths remain unambiguous.\nIf a matching namespace claim exists for an extracted namespace (currently npm/Bun scopes, Composer vendors, and Maven group IDs), the claim owner must match the repository owner.\n\n### Native protocol adapters\n\n#### PyPI / pip\n\nPublaryn currently exposes the following native PyPI-compatible routes:\n\n```http\nGET  /_/oidc/audience\nPOST /_/oidc/mint-token\nGET  /pypi/simple/\nGET  /pypi/simple/:project/\nGET  /pypi/files/:artifact_id/:filename\nPOST /pypi/legacy/\nPOST /pypi/legacy/:repository_slug/\n```\n\nThe read surface supports the PEP 503/691 Simple API with HTML and JSON responses.\n\nThe upload surface accepts Twine-compatible legacy uploads using `multipart/form-data` and a Publaryn credential:\n\n- Basic authentication with a Publaryn API token (for example username `__token__`, password `\u003cpub_...\u003e`)\n- Bearer JWTs or Bearer API tokens for non-Twine clients\n- Short-lived trusted-publishing tokens minted from `POST /_/oidc/mint-token`\n\nPublaryn also exposes the PyPI trusted-publishing exchange expected by modern PyPA tooling:\n\n- `GET /_/oidc/audience` returns the audience string external CI providers should request for their OIDC JWT\n- `POST /_/oidc/mint-token` exchanges that external OIDC JWT for a short-lived Publaryn `pub_...` token\n- the minted token currently lasts 15 minutes, carries `packages:write`, and is bound to exactly one existing PyPI package\n\nCurrent PyPI upload behavior:\n\n- the first uploaded file for a version auto-creates the release and publishes it once the artifact is durably stored\n- additional immutable files can be appended to the same published version to match PyPI's one-file-at-a-time upload flow\n- missing packages are auto-created in the publisher's first eligible user-owned repository when the default `/pypi/legacy/` endpoint is used, mirroring the current npm adapter ergonomics\n- publishers can target a specific Publaryn repository by posting to `/pypi/legacy/:repository_slug/`, which enables first-publish flows into organization-owned repositories for organization admins\n- trusted publishing reuses the existing per-package trusted publisher configuration and currently supports existing PyPI packages only\n- OIDC-derived tokens are intentionally confined to PyPI uploads for their matched package and are rejected on control-plane, npm, and PyPI read endpoints\n- detached signatures, upload attestations, and implicit organization selection without a repository-specific upload URL are intentionally deferred\n\n### Search\n\n```http\nGET /v1/search?q=\u003cquery\u003e\u0026ecosystem=\u003ceco\u003e\u0026org=\u003cslug\u003e\u0026repository=\u003cslug\u003e\u0026page=1\u0026per_page=20\n```\n\nThe current search endpoint is actor-aware:\n\n- anonymous callers only see publicly discoverable packages\n- authenticated callers can also see private and `internal_org` packages when they already have visibility through direct ownership, organization membership, or delegated team access\n- `org` and `repository` filters scope the visible search result set by owner organization slug and repository slug\n- `unlisted` and `quarantined` packages remain excluded from search\n\n### Tokens\n\n```http\nPOST   /v1/tokens\nGET    /v1/tokens\nDELETE /v1/tokens/:id\n```\n\n### Audit\n\n```http\nGET /v1/audit\n```\n\n### Platform stats \u0026 operator jobs\n\n```http\nGET /v1/stats\nGET /v1/admin/jobs\n```\n\n`/v1/stats` stays public in 1.0 and exposes top-level package, release,\norganization, security-finding, artifact, and pending-job counts for quick\ninstance-level visibility.\n\n`/v1/admin/jobs` is the intentionally narrow 1.0 operator queue surface. It\nrequires a platform-administrator identity with the `audit:read` scope,\nsupports `state`, `kind`, `page`, and `per_page` filters, and returns both a\nglobal queue summary and the matching jobs. Use\n[docs/operator/job-queue-recovery.md](docs/operator/job-queue-recovery.md) as\nthe baseline runbook for stale jobs, retry visibility, and recovery checks.\n\n### Health\n\n```http\nGET /health\nGET /readiness\n```\n\n`/health` is a liveness probe and returns `200 OK` while the process is running.\n`/readiness` is a readiness probe and returns `200 OK` only when the instance can reach PostgreSQL and, when Redis is configured, Redis. It returns `503 Service Unavailable` otherwise so orchestrators can stop routing new traffic to that replica.\n\nThe API server handles `SIGTERM` and `Ctrl+C` gracefully.\nDuring shutdown it stops accepting new work, lets in-flight requests drain within the orchestrator grace period, and then exits cleanly.\nThis is the expected lifecycle for rolling updates and horizontal scale-down events.\n\n---\n\n## Security Features\n\n- **Argon2id** password hashing\n- **JWT** access tokens with configurable TTL\n- **MFA/TOTP** ready (configurable per user and org)\n- **OIDC Trusted Publishing** — no long-lived CI secrets needed\n- **Immutable releases** — artifact content is never overwritten\n- **Append-only Audit Log** — enforced at database rule level\n- **Namespace claims** — prevent typosquatting, reserve namespaces\n- **Name similarity checks** — Levenshtein distance on new package names\n- **Reserved names** — block common abuse patterns\n- **Granular tokens** — personal, org, repo-scoped, package-scoped, CI\n- **Redis-backed rate limiting** — shared auth, write, read, and protocol throttles coordinated across replicas\n- **Publish pipeline** — quarantine → scan → publish (never skippable)\n- **Dependency confusion protection** — explicit namespace ownership\n\n---\n\n## Domain Model\n\n- `User`: A registered user account with MFA support\n- `Organization`: Group of users with teams, namespace claims, and policies\n- `Team`: Sub-group of an org with fine-grained permissions\n- `NamespaceClaim`: Ecosystem-specific namespace owned by a user or organization\n- `Repository`: Hosted packages; 1.0 supports `public`, `private`, `staging`, and `release` kinds, while proxy and virtual stay post-1.0\n- `Package`: Ecosystem-specific package identity\n- `Release`: Immutable versioned release\n- `Artifact`: A file associated with a release (tarball, wheel, jar, gem, …)\n- `ChannelRef`: Mutable tag/alias pointing to a release (npm dist-tag, OCI tag)\n- `Token`: Granular API token with expiry and scopes\n- `AuditLog`: Append-only record of all significant actions\n- `SecurityFinding`: CVE, malware, or policy violation found in a release\n- `TrustedPublisher`: OIDC trusted publishing configuration\n\n---\n\n## Configuration\n\nAll configuration is provided via environment variables (double-underscore separator).\nSee [`.env.example`](.env.example) for the full reference.\n\nKey variables:\n\n| Variable                       | Description                                                         | Default                              |\n| ------------------------------ | ------------------------------------------------------------------- | ------------------------------------ |\n| `DATABASE__URL`                | PostgreSQL connection string                                        | —                                    |\n| `AUTH__JWT_SECRET`             | JWT signing secret (min 32 chars)                                   | —                                    |\n| `AUTH__ISSUER`                 | JWT issuer URL                                                      | `http://localhost:3000`              |\n| `SERVER__CORS_ALLOWED_ORIGINS` | Comma-separated browser origins allowed for cross-origin API access | empty (deny cross-origin by default) |\n| `STORAGE__ENDPOINT`            | S3/MinIO endpoint                                                   | —                                    |\n| `STORAGE__BUCKET`              | Artifact storage bucket                                             | —                                    |\n| `SEARCH__URL`                  | Meilisearch base URL                                                | `http://localhost:7700`              |\n| `REDIS__URL`                   | Redis URL                                                           | `redis://localhost:6379`             |\n| `SERVER__BIND_ADDRESS`         | HTTP bind address                                                   | `0.0.0.0:3000`                       |\n\nThe API does not emit permissive CORS headers by default.\nIf the frontend runs on a different origin in development or production, configure an explicit allowlist with `SERVER__CORS_ALLOWED_ORIGINS`.\nWildcard origins are intentionally rejected so browser-based token usage cannot be exposed accidentally.\n\n---\n\n## Contributing\n\nContributions are welcome.\n\n- Read [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, validation steps, and contribution expectations.\n- Review [SUPPORT.md](SUPPORT.md) before opening a usage question or bug report.\n- Use [SECURITY.md](SECURITY.md) for responsible vulnerability disclosure instead of public issues.\n- Review [docs/1.0.md](docs/1.0.md) for the current release contract and [docs/adr/README.md](docs/adr/README.md) for the indexed decision record map.\n- Please open an issue first to discuss major changes before starting implementation work.\n\n---\n\n## License\n\nThis repository is licensed under both the Apache License 2.0 and the MIT License.\nSee [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosunlp%2Fpublaryn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjosunlp%2Fpublaryn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjosunlp%2Fpublaryn/lists"}