# 25.x → 26.0.0, entry by entry

**Six entries** — `CHANGELOG.md`'s 26.0.0 breaking entries in their order, grouped by
package, lowest tier first. No legacy path, no codemod, no shim. **Entry 1 can refuse sign-ins** on
a host whose egress cannot reach the provider's `jwks_uri` — check it before the deploy (upgrade
step 2). A later slice appends its rows below the last one and never renumbers; each row's
`CHANGELOG.md` line names it as `(#N)` after the dash.

### The upgrade, top to bottom

| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every `@ultimat3/*` to the one new version, `bun install` | nothing yet — a mixed install is untested | — |
| 2 | **before the deploy, an app with an OIDC sign-in:** from a production pod, `curl -sS -m 5 https://www.googleapis.com/oauth2/v3/certs` (or the `jwks_uri` of each provider you registered); where it cannot be reached, return `'token-endpoint-tls'` for that provider only from `oauthLogin(auth, { idTokenKeys })` (entry 1 shows the function) | `X_OAUTH_EXCHANGE_FAILED` (stage `jwks`) on every sign-in through that provider | 1 |
| 3 | `bun test` — a test that feeds `exchangeOAuthCode`, `completeOAuthLogin` or `oauthLogin` an unsigned id token: sign it and serve the key set at the provider's `jwksUri` from the injected `fetch`, or pass `keys` / `idTokenKeys: 'token-endpoint-tls'` | `X_OAUTH_TOKEN_INVALID` where the test signed in; an extra key-set URL in a recorded endpoint list | 1 |
| 4 | `bun run typecheck` for the removed exports: `APPLE_PROVIDER`, `ServerHandle.describe()`, `AgentRecovery`, `Recovery`, `ROUTE_FILENAME.api` | TS2305 / TS2339 at each site | 2, 3, 5 |
| 5 | `find apps -path '*/api/*' \( -name route.ts -o -name page.tsx \)` — move each into an action, or a plain route in `runtime.routes` | `X_ROUTE_FILE_INVALID` at boot | 4 |
| 6 | `grep -rnE "x (jobs (ls\|rm)\|(actions\|queries\|entities) describe\|db branch (ls\|drop))\b" .github scripts bin package.json docs` and swap each for `list`, `delete`, `show` | `X_CLI_UNKNOWN_COMMAND` naming the new verb | 6 |
| 7 | `x verify` | green, or a finding whose `fix:` is the edit | — |

### Entry by entry

Tier 2 — `@ultimat3/auth` (1–2), `@ultimat3/http` (3). Tier 4 — `@ultimat3/render` (4),
`@ultimat3/scraping` (5). Tier 5 — `@ultimat3/cli` (6).

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `exchangeOAuthCode`, `completeOAuthLogin`, `oauthLogin` — the id token's signature | your egress cannot reach a provider's `jwks_uri`, or a test hands the exchange an unsigned id token. 25.x accepted any id token read off the token endpoint over TLS; 26.0.0 checks its signature against the provider's published key set whenever the provider has a `jwksUri` (`providerJwks`, cached per provider). `X_OAUTH_TOKEN_INVALID` for a bad signature, `X_OAUTH_EXCHANGE_FAILED` (stage `jwks`) for an unreachable key set. Opt one provider out with `oauthLogin(auth, { idTokenKeys: (provider) => provider === 'bigco-sso' ? 'token-endpoint-tls' : undefined })` — `undefined` keeps the default; `providerJwks(providerFor(id))` there throws for GitHub, which publishes no `jwks_uri`; `completeOAuthLogin(auth, { idTokenKeys })` and `exchangeOAuthCode(…, { keys })` take the value directly |
| 2 | `APPLE_PROVIDER`, `BUILTIN_OAUTH_PROVIDERS`, `BUILTIN_OAUTH_PROVIDER_IDS` | you import `APPLE_PROVIDER` (TS2305) or list `'apple'` in `defineAuth({ providers })` (an unknown provider). Apple answers a `form_post` and the callback is a GET, so it never completed a login. Delete it, or `registerOAuthProvider` your own |
| 3 | `ServerHandle.describe()` | you call it. TS2339; read `x routes --json`, or call `describeRoutes(table)` from `@ultimat3/http` |
| 4 | `api/**/route.ts`, `ROUTE_FILENAME`, `RouteEntry.surface` | a route file lives under `api/`: `X_ROUTE_FILE_INVALID` (was `X_ROUTE_MODE_INVALID`). Make it an action, or a plain route in `runtime.routes`. `ROUTE_FILENAME` is `Record<RouteSurface, string>`; type a surface with the new `RouteSurface` export of `@ultimat3/render` |
| 5 | `recover: 'agent'`, `AgentRecovery`, `Recovery` | a scrape passes `'agent'` (refused at `scrape()`; 25.x threw `X_NOT_IMPLEMENTED` on the first moved selector) or imports a removed type. Write a `RecoveryHook` function and call your own `llm()` from it; `import type { Recovery }` becomes `RecoveryHook`. `@ultimat3/scraping` is tier 4 now — nothing to edit in an app |
| 6 | `x jobs ls`, `x jobs rm`, `x actions\|queries\|entities describe`, `x db branch ls`, `x db branch drop` | a script runs an old verb: `X_CLI_UNKNOWN_COMMAND` with the new one in its fix. `list`, `delete`, `show`, `list`, `delete`; a bare `x jobs` still lists, and `--json` is unchanged |
