Auth provider abstraction: Supabase, Entra ID, and local HS256
Three commits convert Mike's hardwired Supabase auth check into a dispatching boundary. You set `AUTH_PROVIDER` to `supabase`, `entra`, or `local` and the middleware routes to the corresponding validator - no code change needed per deployment.
The first commit (bfee639b) is the mechanical extraction. AuthPrincipal and AuthValidationResult types land in lib/auth/types.ts. The existing Supabase validation moves to lib/auth/providers/supabase.ts unchanged. The middleware dispatches on AUTH_PROVIDER env var, returning 500 for unknown values. Zero behavior change in supabase mode.
The Entra provider (f9eb061c) validates JWTs against the tenant JWKS with a 5-minute in-memory cache. It accepts both v1.0 (https://sts.windows.net/<tid>/) and v2.0 (https://login.microsoftonline.com/<tid>/v2.0) issuer shapes, and both audience shapes (<clientId> bare and api://<clientId>). The JWT verification uses Node's node:crypto - createVerify("RSA-SHA256") - with no third-party JWT library. roles.ts maps Entra group OIDs to app roles via ENTRA_ADMIN_GROUP_IDS and ENTRA_MEMBER_GROUP_IDS env vars (comma-separated lists). tenantAccess middleware checks tenant lifecycle status (active / pending / suspended) and can auto-onboard when TENANT_ONBOARDING_MODE=auto.
The local provider (ec310e59) is HS256 using node:crypto only - createHmac("sha256", secret) with timingSafeEqual for the comparison. Requires JWT_SECRET in env. routes/auth.ts adds /local-login, /providers, /logout, and the Entra OIDC code-flow endpoints (/select-provider, /login-provider/:id, /openid-callback/:id) with HMAC-signed state cookies. The router is created in this commit but not yet mounted - that wiring lands in the later server commit.
One design note on group-to-role mapping via env vars: it works for simple cases but doesn't support per-tenant configuration. If you ever need different groups to have different roles across tenants, you'd need a mapping store.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?