agents agents/D-worker-auth.md
Owns: worker/ (created 2026-08-21), including worker/migrations/ — the Pigsty schema. Must not touch: RRABBIT/, studbook/, root *.md, cloud.computedriven.com/.

Lane D — the credential surface, and now the control plane

Owns: worker/ (created 2026-08-21), including worker/migrations/ — the Pigsty schema. Must not touch: RRABBIT/, studbook/, root *.md, cloud.computedriven.com/.

## ⚠️ PARTIALLY UNBLOCKED — 2026-08-21, R7/R8 What is no longer blocked: the schema, the RLS battery, the tenant-context contract and the token-verification unit tests. All of it runs against local PostgreSQL and a locally generated keypair. None of it needs a cloud account. That is most of M1. What is still blocked on lane C: integration against a real realm, a real Hyperdrive binding, and a real R2 bucket. Those stay NOT DEPLOYED and must be named so, never mocked into a production claim. Not needed for v0 public tracks. Public tracks are world-readable and need no credential at all, which is why lane A ships without this. This lane exists for private data.

What changed on 2026-08-21

A proposed Cloud brief dropped this lane entirely — R2 objects and a Pigsty storage ref appeared in its architecture with no credential minter anywhere. R8 keeps the lane and widens it.

This is now two jobs in one Worker, not one:

  1. the control-plane API — the portal BFF and api.computedriven.com, reaching Pigsty over

Hyperdrive → Workers VPC → Cloudflare Tunnel (R7);

  1. the credential minter — unchanged in purpose, better in mechanism (below).

It stops being "small and stateless". Re-read the free-tier arithmetic below with that in mind: the 100k req/day ceiling now has to cover portal traffic, not just credential mints.

Read first

STACK_HUB.md §0.7 (the shape) and §0.4.3 (the hole this fills) · CONFIDENTIALITY.md (what this does not solve).

The one job

Keycloak (Fly)      issues an OIDC JWT — who you are, which plan you hold
      ↓  JWKS
Worker (Cloudflare) verify signature · check entitlement ·
                    mint a SCOPED, SHORT-LIVED credential
      ↓
R2                  encrypted blobs — never sees a plaintext byte

This closes the only hole in the compute-in-the-OS design: an object store cannot decide who deserves a credential. Everything else moved into the user's machine; this could not, because a customer who could mint their own credential could mint their own subscription.

Keep it small. It is a stateless request/response handler and it fits inside the Workers free plan — 100,000 requests/day, 10 ms CPU per invocation. If it grows past that, something has gone wrong with the design rather than with the plan.

Amended 2026-08-21 (R8). "Small" now means the bytes never come through here, not the request count is tiny. The rule that matters: `` Super / client │ authenticate ────────────► Worker │ ├── resolve principal + organization │ ├── consult Pigsty (Hyperdrive) │ ├── authorize the requested object scope │ └── mint a scoped, short-lived R2 credential │ ◄───────────────────────────────┘ │ └──────────── chunks ────────────► R2 DIRECT. Never via the Worker. ` **Not** client → Worker → 400 GB of machine backup → R2`. A Worker in the byte path would blow the CPU limit, the request duration and the cost model simultaneously, and it buys nothing: the credential already carries the authorization. Mechanism: use R2's temporary-credentials API for a chunk session — short-lived S3 credentials scoped to a bucket, a set of operations, and an exact prefix or object path. Use a presigned URL where the operation is genuinely one object. The chunker does batches, so temporary credentials are the normal case and presigned URLs are the exception. Verify both against Cloudflare's own docs at the point of use. This tree has quoted stale provider facts three times; §0.2's rule applies to API shapes as much as to prices.

What it must do

  1. Verify the JWT against the realm's JWKS. Workers cannot use Node-native libraries — use

jose or cloudflare-worker-jwt. Cache the JWKS briefly; rotate.

  1. Check `iss`, `aud`, `exp` and the claims. A valid signature on a token for a different

audience is not authorization.

  1. Check entitlement — is this subscription current, and for which tier.

SUPERSEDED 2026-08-21 (R7). This said subscriber state lives in D1. It does not. R7 makes Pigsty the single owner of ComputeDriven truth, and entitlements are that — they are joined against organizations, worlds and usage, which is exactly the shape D1 is wrong for. entitlements is a Pigsty table reached over Hyperdrive. D1 keeps no application state under this ruling. If it earns a job later it will be a cache with a named invalidation rule, not a second source of truth.

  1. Mint a scoped, short-lived credential — a presigned URL or scoped token, narrowed to that

tenant's prefix. Never hand out a broad credential because it is simpler.

  1. Fail closed. Anything unverifiable is a refusal, by name, with no fallback path.

What this does NOT solve — do not let it drift

This is authorization, not confidentiality. They are different questions with different mechanisms, and collapsing them is the same category error as conflating a sem- world with an OS world:

QuestionAnswered by
Who are you, and are you entitled?this lane
Why can the storage provider not read your data?client-side keys — nothing here touches it

Keycloak controls who may ask for a credential. It has nothing to say about why knowing a content hash does not reveal content. Never describe this as making anything end-to-end encrypted.

Done means

  • Unit tests against a locally generated keypair: valid token accepted, expired refused, wrong

audience refused, bad signature refused, alg: none refused, a token for tenant A unable to obtain a credential for tenant B.

  • Integration against a real Keycloak realm once lane C lands.

  • A measured request cost showing it fits the free tier.

Traps

  • `alg: none` and algorithm confusion. Pin the expected algorithm; never take it from the token.

  • Clock skew — allow a small leeway on exp/nbf, and say how much.

  • A JWKS fetch that fails must refuse, not fall back to a cached key indefinitely.

  • Scope every credential to a tenant prefix. The most likely real bug in this lane is a

credential that works for more than it should, and it will not show up in a happy-path test.

  • Do not build auth by hand. Verify tokens someone else issued; issue none yourself.

Open in the interactive atlas