Worker auth
API keys (pn_…), fail-closed D1 without KV, admin token, Bearer, ALLOW_OPEN_KEYS, and /api/run gating.
This page
Worker auth
Abstract
AXIS Worker auth is API-key based, not session cookies. Keys are created by admins (X-Admin-Token), validated via KV when bound, and used as Authorization: Bearer on the script library and (when gated) on /api/run.
Core helpers: worker/src/auth.ts. Key CRUD: worker/src/keys.ts.
Conceptual model
Rendering…
Interface surface
AuthContext
{ key: string; userId: string; tier: string }
userId is a truncated SHA-256 of the key (stable partition id).
extractBearer
Authorization: Bearer <token>- Else query
?key=
requireApiKey
Returns either { ok: true, ctx } or { ok: false, status, code, message }.
| Code | HTTP | When |
|---|---|---|
NO_KEY | 401 | Empty Bearer / ?key= |
INVALID_KEY | 401 | Unknown in KV / malformed shape |
API_KEYS_REQUIRED | 503 | Fail-closed: D1 (DB) bound without API_KEYS KV and ALLOW_OPEN_KEYS off |
Fail-closed with durable storage (2.0.1+)
When D1 is active but API_KEYS KV is not bound, inventable shape-only keys would partition real script data by attacker-chosen tokens with no mint/revoke path. In that configuration:
- Only explicit
ALLOW_OPEN_KEYS=1accepts any non-empty Bearer (local demos). - Otherwise respond 503
API_KEYS_REQUIRED— bindAPI_KEYSand mint via/api/keys.
/api/run auth gate
From worker/src/runtime.ts:
| Condition | Auth on POST /api/run |
|---|---|
API_KEYS bound | Required |
REQUIRE_RUN_AUTH=1 (or true / yes) | Required |
| Neither | Optional (Bearer still meters when present) |
Always rate-limited (30/min) regardless of auth — see Runtime.
Admin keys API (/api/keys)
| Action | Auth | Behavior |
|---|---|---|
create (POST or ?action=create) | X-Admin-Token === env.ADMIN_TOKEN | Mint pn_ + 48 hex, store key:{key} in KV (1y TTL) with tier |
validate (GET or ?action=validate) | Bearer or ?key= | Tier + created_at if known |
Tiers: free | hobby | pro | team | enterprise.
Without ADMIN_TOKEN set, create always fails (isAdmin false).
Do not leave ADMIN_TOKEN = "" in [vars]. That empty assignment is still a Worker binding, and Cloudflare error 10053 rejects wrangler secret put ADMIN_TOKEN (Binding name already in use). axis secret put ADMIN_TOKEN comments the stub, deploys to drop it, then sets the secret.
CLI:
axis setup kv # create + bind API_KEYS
axis secret put ADMIN_TOKEN # comments empty [vars] stub if present
axis keys create --tier hobby # prompts, or --admin-token / AXIS_ADMIN_TOKEN
axis keys validate --key pn_…
Without KV on validate: accepts well-formed pn_[a-f0-9]{48} as hobby (dev).
ALLOW_OPEN_KEYS
wrangler.toml defaults local demos to "1":
accept any non-empty Bearer key for
/api/scripts
Never leave open in production. Prefer bound API_KEYS + real admin-minted keys.
Internals
| Path | Role |
|---|---|
worker/src/auth.ts | Bearer, hash, requireApiKey (fail-closed) |
worker/src/keys.ts | Admin create / validate |
worker/src/runtime.ts | Run gate + rate limit + usage meter |
worker/src/scripts.ts | requireApiKey on library CRUD |
worker/src/git-oauth.ts | Env GITHUB_OAUTH_CLIENT_ID / GITLAB_OAUTH_CLIENT_ID wins over body clientId |
Key format
'pn_' + 24 random bytes as hex // 48 hex chars
Invariants & edge cases
- Admin token is a shared secret — rotate via wrangler secrets in production, not committed vars.
- KV record shape — JSON
{ key, tier, createdAt }underkey:${apiKey}. - No OAuth / cookies — PWA stores the key in plugin config (
storage:cloud). - Stream DO unauthenticated — public market data only.
Worked examples
Create a key (admin)
curl -sS -X POST 'http://127.0.0.1:8787/api/keys?action=create' \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"tier":"hobby"}'
Validate
curl -sS 'http://127.0.0.1:8787/api/keys?action=validate' \
-H "Authorization: Bearer pn_…"
Failure modes
| Symptom | Cause |
|---|---|
| 403 create | Wrong/missing admin token |
| 401 scripts | Open keys off + no KV + bad shape |
503 API_KEYS_REQUIRED | D1 without API_KEYS KV and open keys off — bind KV + mint keys |
401 on /api/run | API_KEYS or REQUIRE_RUN_AUTH set; missing Bearer |
| OAuth start uses attacker client | Env client id not set — set GITHUB_OAUTH_CLIENT_ID (env always wins over body) |
| Cross-user leakage tests fail | Partition hash regression |
See also
- Data plane
- Bindings
- Runtime
- Security tests
- AXIS CLI —
axis secret put·axis setup oauth