Control plane for the Katena Portal Store: suite catalog, per-org entitlements, and entitlement-gated signed bundle downloads. Trusts tokens Better Auth issues (validated by userinfo introspection).
Axum service in api/, default port 7002. This page is generated from the live OpenAPI 3.1 document; the handlers’ #[utoipa::path] annotations are the source of truth.
Three independent mechanisms — a route uses one or none, never two:
Mechanism
Used by
How
Bearer token (Authorization: Bearer <token>)
Every route marked auth
Opaque OAuth access token from Better Auth (the server/ provider), validated by userinfo introspection; the caller’s org is derived from it.
HMAC signature
Routes marked HMAC
Signed over the exact raw body; no bearer. /webhooks/payments uses X-Katena-Signature, /webhooks/stripe uses Stripe-Signature.
Robot signature
Routes marked robot (/robot-agent/*)
ed25519 over METHOD\nPATH\nunix_ts\nnonce\nsha256_hex(body) with the robot’s enrolled key, in x-katena-robot, x-katena-ts (±60 s), x-katena-nonce (16 to 64 of [A-Za-z0-9_-], accepted once per key) and x-katena-sig. Enroll is signed with the key it registers.
Auth’d routes are org-scoped: you only see your own org’s data. Missing/invalid token → 401; valid token without an active org → 403.
Each auth’d route also needs a permission from the caller’s org role (owner, admin, technician, event_operator, viewer, legacy member), carried as org_role in userinfo; a role without it gets 403 forbidden: <resource>:<action>. /admin/* needs the katena_admin platform role (or an org in the legacy ADMIN_ORG_IDS), else 403 not_admin. The matrix lives in server/access.js, mirrored by api/src/authz.rs.
The ARC member and Open Call routes (/me/*, /open-call/*) are per person (the token’s sub), not per org, and need no active org. /admin/open-call/* and /admin/points need the open_call:review platform permission (open_call_staff or katena_admin). Their errors read <code>: <sentence>; match the code.
The caller’s own partner record (to show the join code). 404 if the org is not a partner.
POST
/partner/activations
auth
Activate a product for one of the partner’s customers: grants the entitlement and records the sale (and its event for ARC) in one transaction. 409 if the customer already owns it, so a double click never bills twice.
GET
/partner/customers
auth
The partner’s customers.
POST
/partner/join
auth
A customer org joins a partner with the partner’s join code.
GET
/partner/sales
auth
The partner’s sales, newest first (latest 500).
members — ARC member profile, member number (Founding 500) and points ledger; per person, no org needed
The caller’s own application with its status and uploads. 404 no_application if none.
PUT
/open-call/application
auth
Create the caller’s application, or edit it while it is still applied.
POST
/open-call/application/withdraw
auth
Withdraw the caller’s application (from any status).
GET
/open-call/booking
auth
The upcoming times with places taken, and the caller’s own booking.
POST
/open-call/booking
auth
Take one place.
DELETE
/open-call/booking
auth
Give the caller’s place back, any time before the session starts.
POST
/open-call/uploads
auth
Start an upload: checks the type, size and count limits and returns a signed URL to PUT the file to.
PUT
/open-call/uploads/{id}
signed
The file body for an upload, sent to the signed put_url (no bearer token). Content-Type must be the declared one and the body exactly the declared size.
DELETE
/open-call/uploads/{id}
auth
Delete one of the caller’s uploads (and its file) while the application is applied.
open-call-staff — ARC Open Call staff (open_call:review: open_call_staff or katena_admin): review, status moves with email, hand point awards, signed file links
Some wired routes are excluded on purpose — they carry no Katena Portal schema:
GET /checkout/done — Stripe post-payment browser landing (HTML, informational; the entitlement arrives via the webhook, not this redirect).
POST /webhooks/stripe — Stripe’s own webhook. The body is Stripe’s raw event JSON, not a Katena Portal type; HMAC-verified via Stripe-Signature like /webhooks/payments.
GET /dev/checkout/{order_id} + POST /dev/checkout/{order_id}/act — the fake payment page, mounted whenever ENV is not production.
The API crate’s unit tests (cargo test -p katenaos-api) cover the security-critical pure logic: HMAC + Stripe signature verify, the commerce & admin state-transition tables, and the fail-closed config guards. There are no HTTP/DB integration tests exercising the wired handlers end-to-end yet — that surface is verified manually against a live Postgres.