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

Diagram

Rendering…

Interface surface

AuthContext

{ key: string; userId: string; tier: string }

userId is a truncated SHA-256 of the key (stable partition id).

extractBearer

  1. Authorization: Bearer <token>
  2. Else query ?key=

requireApiKey

Returns either { ok: true, ctx } or { ok: false, status, code, message }.

CodeHTTPWhen
NO_KEY401Empty Bearer / ?key=
INVALID_KEY401Unknown in KV / malformed shape
API_KEYS_REQUIRED503Fail-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:

  1. Only explicit ALLOW_OPEN_KEYS=1 accepts any non-empty Bearer (local demos).
  2. Otherwise respond 503 API_KEYS_REQUIRED — bind API_KEYS and mint via /api/keys.

/api/run auth gate

From worker/src/runtime.ts:

ConditionAuth on POST /api/run
API_KEYS boundRequired
REQUIRE_RUN_AUTH=1 (or true / yes)Required
NeitherOptional (Bearer still meters when present)

Always rate-limited (30/min) regardless of auth — see Runtime.

Admin keys API (/api/keys)

ActionAuthBehavior
create (POST or ?action=create)X-Admin-Token === env.ADMIN_TOKENMint 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

PathRole
worker/src/auth.tsBearer, hash, requireApiKey (fail-closed)
worker/src/keys.tsAdmin create / validate
worker/src/runtime.tsRun gate + rate limit + usage meter
worker/src/scripts.tsrequireApiKey on library CRUD
worker/src/git-oauth.tsEnv 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

  1. Admin token is a shared secret — rotate via wrangler secrets in production, not committed vars.
  2. KV record shape — JSON { key, tier, createdAt } under key:${apiKey}.
  3. No OAuth / cookies — PWA stores the key in plugin config (storage:cloud).
  4. 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

SymptomCause
403 createWrong/missing admin token
401 scriptsOpen keys off + no KV + bad shape
503 API_KEYS_REQUIREDD1 without API_KEYS KV and open keys off — bind KV + mint keys
401 on /api/runAPI_KEYS or REQUIRE_RUN_AUTH set; missing Bearer
OAuth start uses attacker clientEnv client id not set — set GITHUB_OAUTH_CLIENT_ID (env always wins over body)
Cross-user leakage tests failPartition hash regression

See also