Skip to content

[N10a] Route auth: jwt(), oidc(), basic() in an ordered list, principal per run - #307

Merged
LinuxDevil merged 6 commits into
mainfrom
lou-n10a-route-auth
Oct 2, 2026
Merged

LinuxDevil merged 6 commits into
mainfrom
lou-n10a-route-auth

Conversation

@LinuxDevil

@LinuxDevil LinuxDevil commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

N10a: route auth (jwt(), oidc(), basic()) in an ordered list, principal per run

Closes #249

What

  • New subpath @lousho/build-ai-agent/auth (src/auth/: index.ts, jwt.ts, oidc.ts, basic.ts, routeAuth.ts, types.ts, encoding.ts). It uses Web Crypto and fetch only: no node: import, no new dependency. package.json exports["./auth"] and tsup.config.ts 'auth/index'. Principal is also exported from the root.
  • routeAuth(request, list): the first Principal wins, null skips, AuthError(401|403) stops, any other throw is a logged 500 with a generic body, an empty list or all entries skipping gives a generic 401 with one WWW-Authenticate per distinct challenge. Responses carry cache-control: no-store.
  • createRouteHandler({ auth }), serveFetch(request, ctx, auth) and createDeployedServer(agent, { auth }) take the list. The string and boolean-function forms behave as before. The node server appends apiToken(LOUSHO_API_TOKEN). An agent directory's auth.ts is loaded by resolveAgentDir() (auth, manifest.auth), bundled by node-server-dir.ts, and passed by the generated server. The sdk runtime plugin resolves @lousho/build-ai-agent/auth in bundles.
  • The principal reaches the run through SendOptions.principal, session send()/stream(), SessionTurnCall, RunConfigContext and MemoryScopeContext. Slack and Discord set the sender as the principal. mountChannels passes ChannelInbound.principal along.
  • New error code LOUSHO_AUTH_CONFIG_INVALID. In production, createRouteHandler without auth logs a warning once.

Security checks and their tests

Requirement Test
Web Crypto only, no new dependency, no node: import src/auth/routeAuth.test.ts "bundles for the browser platform ... no node: import"
alg: none rejected (any casing) jwt.test.ts "rejects alg 'none' in any casing"
An alg that is not configured is rejected jwt.test.ts "rejects an alg outside the configured algorithms"; construction cases
No HMAC/asymmetric switch from the token header (algorithm confusion) jwt.test.ts "refuses algorithm confusion"; construction cases (publicKey + HS256, jwksUrl + HS256); "never takes an HMAC key ... from a key set"
Bad signature jwt.test.ts "rejects a bad signature, and a token signed by another key"
exp (required), nbf, iat with a bounded skew (60 s default, 300 s max) jwt.test.ts "checks exp (required), nbf and iat with the clock tolerance"; construction case clockToleranceSec: 3600
iss / aud jwt.test.ts "checks the issuer exactly and the audience"; oidc.test.ts issuer mismatch and audience
Constant-time Basic and token comparison basic.test.ts "compares in constant time ..." and "accepts the bearer token ... in constant time"
Bounded JWKS cache and refresh jwt.test.ts "fetches once, caches for 10 minutes, and refetches an unknown kid at most once per 30 s", "a failing endpoint is retried at most every 30 s", "concurrent first requests share one fetch"; oidc.test.ts "retries a failed discovery at most every 30 s"
No key-set URL taken from the token jwt.test.ts "never fetches a key-set URL named by the token (jku, x5u, jwk ...)"; oidc.test.ts discovery issuer mismatch never fetches the JWKS
Generic 401 that does not say which check failed routeAuth.test.ts "gives the same generic 401 whatever check failed"; "an unexpected throw is a 500 whose body does not carry the error"
Tokens and credentials never logged by construction (no log line contains a token or a password); routeAuth.test.ts 500-body test

Remote sub-agents and remote evals against a server that uses an auth list: remoteAgent.test.ts "works against a server whose auth is a list ... approvals included" and "adds the remote run's tokens behind an auth list too ..." (usage reporting, principal api-token). remoteTarget.test.ts "passes against a server whose auth is a list".

For the docs site (G9)

  • New page auth (docs/auth.md, "Route auth and principals"). It needs an English and an Arabic page, navigation in docs.json for both languages, and an entry in PAGES in scripts/sync-sdk-docs.mjs. Headings: ## The auth list, ## Helpers (### jwt(), ### oidc(), ### basic(), ### apiToken() and anonymous(), ### Your own entry), ## 401, 403 and WWW-Authenticate, ## Where it runs, ## createRouteHandler, ## The node server and auth.ts, ## Reading the principal in the run, ## Security notes.
  • New section ## Auth (### LOUSHO_AUTH_CONFIG_INVALID), appended at the end of errors.
  • Edited only, no heading changes: deployment (a paragraph under ### Auth), nextjs (a paragraph under ## Routes), memory (a paragraph under ## Scopes), agent-directories (a layout line and the deploy paragraph), api-overview (a bullet), cloudflare-workers (a "Route auth" row in the limits table). The README docs table has a new row.

Out of scope / notes

Verification (after merging origin/main at 996e4a0; then 5fe0b2f, a CHANGELOG-only tidy, after which docs:verify-snippets, docs:llms:check and the scripts tests were rerun)

  • npx tsc --noEmit: ok
  • npm run lint: ok, 0 warnings
  • npm run build: ok. npm run build --workspace=packages/create-lousho-agent: ok
  • npm run test:types: 68 passed, no type errors
  • npm run docs:verify-snippets -- --skip-build: all 220 snippets type-check; 8 also run
  • npm run docs:llms:check: ok
  • npm run test:coverage: 241 files passed, 1 skipped; 3517 tests passed, 6 skipped. An earlier run, before the second sync, hit a 5 s timeout in NodeWorkspace.test.ts "runs in the root by default" under machine load. That test passes alone and in every later run.
  • npm run fallow: no issues, 0 above threshold
  • Agent Forge: typecheck ok, typecheck:server ok, test 119 passed, test:server 130 passed
  • npm run pack-smoke: all checks passed (esm 17/17 and cjs 17/17 entries load, including ./auth)
  • Live test spend: none (no live calls)

🤖 Generated with Claude Code

LinuxDevil and others added 6 commits October 2, 2026 20:32
…al per run

New subpath @lousho/build-ai-agent/auth (Web Crypto and fetch only): jwt(),
oidc(), basic(), apiToken(), anonymous() and routeAuth(). The first entry
that returns a Principal accepts; null skips; AuthError stops; everything
skipping is a generic 401 with merged WWW-Authenticate challenges.
createRouteHandler, serveFetch and createDeployedServer take the list; an
agent directory's auth.ts guards the built node server. The principal
reaches send()/stream()/session turns, RunConfigContext and
MemoryScopeContext; Slack and Discord set the sender as principal.
New error code LOUSHO_AUTH_CONFIG_INVALID; docs/auth.md.

Closes #249

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llow)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil merged commit 3d2da03 into main Oct 2, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[N10a] Route auth: jwt(), oidc(), basic() in an ordered list, principal per run

1 participant