9.0 KiB
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: SemVerMAJOR.MINOR.PATCHwith optional pre-release/build suffix.
Visibility (GL #524)
manifest.visibility:public(default) orprivate.- 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 <name> --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 viaPUT /api/account/registry/price {name, price_cents}(owner/admin;0/nullclears; 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 withCache-Control: private, no-store. - Buy flow:
POST /api/account/registry/buy {namespace, name}(session auth on leanctx.com) → Stripe Checkout (mode=payment, metadatakind=ctxpkg_purchase,user_id,package_id) → webhookcheckout.session.completedrecords 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
lean-ctx pack export <name>[@version] --sign # ed25519-signed bundle
# key: ~/.lean-ctx/keys/ctxpkg-ed25519.key
# (auto-generated, 0600 — back it up)
lean-ctx pack export <name> --sign --private # private on the hosted registry
lean-ctx pack publish <file.ctxpkg> [--registry <url>] [--token <ctxp_…>]
# token also via CTXPKG_TOKEN
# ctxr_ tokens are rejected locally
lean-ctx pack install <ns>/<name>[@version] [--registry <url>]
# 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:
[[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
.ctxpkgcontext 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 |