# Contract: ctxpkg Registry v1 (GL #406) Status: ACTIVE Consumers: `lean-ctx pack publish/install` (CLI), ctxpkg.com storefront, leanctx.com account pages (token management), control plane (server) The hosted registry for `.ctxpkg` context packages, served via **ctxpkg.com**. v1 is deliberately minimal: publish, index, download, yank — no payouts, no search API, no WASM plugins (see Out of scope). ## Identity & trust model | Concern | Who enforces it | How | |---|---|---| | Authenticity | Registry (publish gate) | mandatory ed25519 signature, verified server-side against the engine's exact signing message | | Name binding | Registry (publish gate) | `manifest.name` must equal `@{namespace}/{name}` from the URL; version likewise | | Transport integrity | Client (install gate) | artifact SHA-256 from the index compared against downloaded bytes | | Content integrity | Client (install gate) | engine `verify_integrity` (content_hash, composite sha256, byte_size) + local ed25519 re-verification | | Immutability | Registry (DB constraint) | `(package, version)` unique; re-publish → 409; yank flips a flag, never deletes | The signing message is the engine's `ctxpkg-sign-v1` format (`rust/src/core/context_package/signing.rs`): ``` message = hex(sha256("ctxpkg-sign-v1:{name}:{version}:{integrity.sha256}")) signature = ed25519_sign(signing_key, message_bytes) ``` ## Namespaces - One namespace per account, claimed once, **permanent in v1**. - **Org-owned namespaces (GL #524)**: claim with `org_id` — the claimer stays the *anchor* (FK target for tokens/domains); org owners/admins manage (mint publish tokens, domains), members mint read tokens and install private packages. Requires owner/admin role in the org to claim. - Format: `[a-z0-9-]`, 2–32 chars, no leading/trailing dash. - Reserved (never self-serve): `leanctx`, `lean-ctx`, `ctxpkg`, `official`, `verified`, `registry`, `api`, `www`, `admin`, `internal`, `system`, `root`, `staff`, `support`. - Package names: `[a-z0-9-]`, 1–64 chars. Versions: SemVer `MAJOR.MINOR.PATCH` with optional pre-release/build suffix. ## Visibility (GL #524) - `manifest.visibility`: `public` (default) or `private`. - Private packages never appear in catalog/search/badge; index, manifest and download answer **404** unless the request carries a bearer token of the owning namespace (any scope). Authenticated private responses are `Cache-Control: private, no-store`. - Set at export: `lean-ctx pack export --sign --private`. ## Paid packs (GL #529, v0 first-party) - Price is **registry metadata** (`registry_packages.price_cents/currency`), *not* part of the signed manifest — repricing never forces a release. Set via `PUT /api/account/registry/price {name, price_cents}` (owner/admin; `0`/`null` clears; v0 cap 50 000 cents). - Catalog/index expose `price: {amount_cents, currency} | null`. Discovery of paid packs stays public — only the **download** is gated. - Download gate: no purchase and no owning-namespace token → **402** with an actionable `{"error": …}` (where to buy). Purchased or owner → 200 with `Cache-Control: private, no-store`. - Buy flow: `POST /api/account/registry/buy {namespace, name}` (session auth on leanctx.com) → Stripe Checkout (`mode=payment`, metadata `kind=ctxpkg_purchase`, `user_id`, `package_id`) → webhook `checkout.session.completed` records the purchase (idempotent on the session id). Purchases bind to the account; buyers mint **account install tokens** (`ctxr_…`, no namespace required) to install. - Refunds: manual in v0 (Stripe dashboard; delete the purchase row to revoke access). ## Public surface (ctxpkg.com → control plane `/registry/v1/*`) ctxpkg.com proxies `/api/*` → control-plane `/registry/*`. All bodies JSON unless noted. | Method & path (public form) | Auth | Returns | |---|---|---| | `GET /api/v1/index.json` | — | global catalog (public packages only): `{schema:"ctxpkg-registry-index-v1", packages:[{namespace,name,scoped_name,description,latest_version,versions,downloads,updated_at,tags,verified,visibility,quality}]}` (Cache-Control 300 s) | | `GET /api/v1/packages/{ns}/{name}/index.json` | token if private | `{schema:"ctxpkg-registry-package-v1", latest, visibility, versions:[{version,artifact_sha256,size_bytes,signer_public_key,yanked,downloads,published_at}]}` (Cache-Control 60 s public / no-store private) | | `GET /api/v1/packages/{ns}/{name}/{version}/download` | token if private | artifact bytes (`application/octet-stream`), `x-ctxpkg-sha256` header, `x-ctxpkg-yanked: true` when yanked. Yanked stays downloadable (reproducibility) but is excluded from `latest`. | | `PUT /api/v1/packages/{ns}/{name}/{version}` | Bearer `ctxp_…` (publish scope) | 201 `{published, artifact_sha256, size_bytes, trust_report}`; 400 invalid/unsigned/read-token; 401 bad token; 409 version exists; 413/400 too large | | `DELETE /api/v1/packages/{ns}/{name}/{version}` | Bearer `ctxp_…` (publish scope) | 200 `{yanked}` — yank, not delete; owner namespace only | Publish-time checks (`trust_report`, persisted per release): `schema:"ok"`, `signature:"verified"`, `name_version_match`, `size_within_limit` (default cap 8 MiB), and an explicit `skipped` list (`wasm_capability_audit`, `malware_heuristics`) — the report never claims more than was checked. ## Publisher self-service (leanctx.com account → edge → control plane) Edge routes (session auth, open `cloud_server`): | Edge route | Forwards to (internal-key auth) | |---|---| | `GET /api/account/registry` | `GET /api/billing/registry/{user_id}` | | `PUT /api/account/registry/namespace` | `PUT /api/billing/registry/{user_id}/namespace` | | `POST /api/account/registry/tokens` | `POST /api/billing/registry/{user_id}/tokens` | | `DELETE /api/account/registry/tokens/{id}` | `DELETE /api/billing/registry/{user_id}/tokens/{id}` | | `PUT /api/account/registry/price` | `PUT /api/billing/registry/{user_id}/price` | | `POST /api/account/registry/buy` | `POST /api/billing/registry/{user_id}/buy` | Tokens (GL #524 scopes): 256-bit, plaintext shown exactly once at mint; only the SHA-256 is stored. Max 10 active per publisher; revocation is immediate. | Scope | Prefix | May | Minted by | |---|---|---|---| | `publish` (default) | `ctxp_` | publish, yank, install (incl. private) | anchor / org owner / org admin | | `read` | `ctxr_` | install only (incl. private + purchased) — CI-safe | any org member; **any account** (buyer install tokens, GL #529) | `POST …/tokens` body: `{label?, scope?: "publish"\|"read"}`. The claim body accepts `{namespace, org_id?}`; org claims need owner/admin in that org. Accounts without a namespace may mint `read` tokens only (`GET /api/account/registry` then returns `namespace: null` plus their tokens and purchases). ## CLI surface ```bash lean-ctx pack export [@version] --sign # ed25519-signed bundle # key: ~/.lean-ctx/keys/ctxpkg-ed25519.key # (auto-generated, 0600 — back it up) lean-ctx pack export --sign --private # private on the hosted registry lean-ctx pack publish [--registry ] [--token ] # token also via CTXPKG_TOKEN # ctxr_ tokens are rejected locally lean-ctx pack install /[@version] [--registry ] # registry also via CTXPKG_REGISTRY # CTXPKG_TOKEN (ctxp_/ctxr_) unlocks # private packages ``` Install resolves `latest` (newest non-yanked) unless pinned; a pinned yanked version installs with a loud warning. Every remote install is recorded in the project's `.lean-ctx/ctxpkg.lock`: ```toml [[package]] name = "@acme/auth-context" version = "1.2.0" artifact_sha256 = "…" registry = "https://ctxpkg.com/api" ``` ## Out of scope for v1 (deliberate) - **WASM plugins / policy packs as registry artifacts** — blocked on the capability-audit pipeline (#403 signing story); v1 hosts signed `.ctxpkg` context packages only. - Publisher payouts (Stripe Connect rev-share, GL #532), namespace transfer, multiple keys per publisher (key rotation = new publisher identity in v1). - ~~Server-side search~~ → shipped (GL #514). ~~Org-owned namespaces, private packages, read tokens~~ → shipped (GL #524, P2). ~~First-party paid packs~~ → shipped (GL #529, P3 v0). ## Module map | Concern | Where | |---|---| | Server: routes + validation | control plane `src/routes/registry.rs`, `src/routes/registry_tokens.rs`, `src/routes/registry_purchases.rs` | | Server: queries / blobs / verify | control plane `src/registry_db.rs`, `src/registry_store.rs`, `src/registry_verify.rs` | | Client: remote calls | `rust/src/core/context_package/remote.rs` | | Client: signing keys | `rust/src/core/context_package/keys.rs` | | Client: lockfile | `rust/src/core/context_package/lockfile.rs` | | Edge proxies | `rust/src/cloud_server/billing_edge.rs` | | CLI | `rust/src/cli/pack_cmd.rs` |