Repository navigation
Commit 5d3018d
fix(runtime)!: an active ADR-0069 gate that cannot read the session answers 503 at the dispatcher's seam (#23363)
Fixes #23245
Clause-②: no (narrowing: an active gate's outage now refuses what it
served)
## What this changes
`HttpDispatcher.enforceAuthGate`
(`packages/runtime/src/http-dispatcher.ts`) read the session with
`.catch(() => undefined)` and wrapped the whole decision in `catch {
return null }`. So while the auth service could not answer, an active
ADR-0069 gate took the failed read for "no session" and admitted, on
every route the runtime dispatcher serves. REST's `computeExecCtx`
answers the same window `503` under #13906's decision 2 (option B, "an
*active* auth gate whose session re-read fails answers **503**"), and so
does the dispatcher plugin's declared-endpoint fallback since #23190.
Now, once `isAuthGateActive()` has answered `true`, a failure to read
the session throws `AuthzStoreUnavailableError('auth_gate', err)`. The
outer net re-raises that brand alone (`rethrowAuthzStoreUnavailable`,
the shape core prescribes) and keeps failing open on every other fault.
The call sits above `dispatch()`'s own `try`, so the error leaves
`dispatch()`, and the transport's existing error exit
(`errorResponseBase` in `dispatcher-plugin.ts`) answers it `503
SERVICE_UNAVAILABLE`. That is the exit the identity step's and the
membership gate's outages already take. No second rendering is added.
## The arms, case for case
| gate state | dispatcher before | dispatcher after | REST
`computeExecCtx` | declared-endpoint fallback |
|:--|:--|:--|:--|:--|
| no gate feature active (no read is made) | admitted | admitted,
unchanged | admitted | admitted |
| `isAuthGateActive()` throws | admitted | admitted, unchanged |
admitted | admitted |
| active, the session read rejects | **admitted** | **503** | 503 | 503
|
| active, the session API cannot be obtained, or the header bag cannot
be normalised | admitted | **503** | anonymous (see note) | 503 |
| active, the read resolves with no posture (no session, an un-held
user) | admitted | admitted, unchanged | admitted | admitted |
| active, the read carries a posture | 403 with the gate code | 403,
unchanged | 403 | 403 |
Note on the fourth row. On REST those steps run in the identity half of
`computeExecCtx`, before its gate block, and a fault there falls to the
method's outer net, so the caller becomes anonymous. The dispatcher's
gate runs after identity has been decided, so "anonymous" is not an
answer this seam can give: the choice is to admit or to refuse. This PR
refuses, as the in-package sibling `readGatedSessionUser` does for
exactly these steps. An active gate whose session read could not even be
made has failed to read, in the ruling's terms. The arm is named here so
the review can rule on it.
## Pin
`packages/runtime/src/http-dispatcher.auth-gate-outage.pin.test.ts`
drives the three cases through `dispatch()`:
1. **Outage:** an active gate whose session read rejects. `dispatch()`
rejects with the branded error (`code` `SERVICE_UNAVAILABLE`, `status`
503, `object` `auth_gate`), and the route's handler never runs.
2. **Gate off:** the same failing backend with no gate feature active is
served as before. The gate makes no read of its own, measured as a read
count (an active gate on the same request reads exactly once more than
an inactive one).
3. **Active and clear:** a read that resolves with an un-held user, or
with no session, is admitted.
A positive control rides beside them: an expired password on a route the
allow-list does not name gets the gate's 403, so a green "served" cannot
mean that the gate never ran. A second block mounts the real dispatcher
plugin on a recording server and reads the wire, one row per gate state:
the outage is answered `503` with an envelope that parses under
`BaseResponseSchema` and `ApiErrorSchema` and carries `code:
SERVICE_UNAVAILABLE`; gate off keeps its `401 UNAUTHENTICATED`; active
and clear keeps the route's own `501`; held keeps `403
PASSWORD_EXPIRED`.
**Negative control** (one shot, not kept as a test). With the fix
committed (`ebf5848f6`), `node scripts/ablation-replace.mjs` replaced
the outer net with `catch { return null }` (anchor 1 hit, x1 to x0, blob
`5b42c4c85e3b` to `db96181d7c71`) and ran the pin:
```text
× CASE 1 — OUTAGE: … → `dispatch()` RESOLVED — the active gate failed open on the outage: expected undefined to be defined
× OUTAGE on the wire: … → expected 401 to be 503 // Object.is equality
Tests 2 failed | 7 passed (9)
ablation-replace: ok restored: blob == HEAD (5b42c4c) and `git diff HEAD` is empty
```
Both outage cases went red and every control stayed green, which was the
direction predicted before the run. The wire reading also confirms the
"before" column: the outage used to answer `401` there, byte for byte
the gate-off answer.
## Full-repo pin sweep
No pin in the repository asserts the old fail-open at this seam, so
nothing needed flipping.
- Every test that wires `isAuthGateActive` (13 files) was checked. The
only ones that drive a failing read on an active gate pin other seams
(REST, the declared-endpoint fallback, the storage routes), and each
already expects 503.
- The error code and object (`'auth_gate'`) appear only at those seams.
- The fail-open wording (`fail-open`, `fails open`, `never break
dispatch`) in `packages/runtime/src` tests appears in prose only, never
in an assertion on this seam.
- The full `@objectstack/runtime` suite stays green with the fix.
## Verification (at `061c8f78e`, after merging `origin/main`
`228357ab6`)
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2`: `Test Files 372 passed (372)`, `Tests 5385 passed | 19
skipped (5404)`. The `repo` project: `Test Files 3 passed (3)`, `Tests
808 passed (808)`.
- `pnpm --filter @objectstack/runtime typecheck`: exit 0, and
`check:test-typecheck: OK` (the new test file adds no test-layer debt).
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths)
derived 62 commands at this head. All 62 ran, and `--ran` answered `62
derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED`. Every one
exited 0 at this head. On the pre-merge run, two had refused with
PREREQUISITE NOT MET: `check-plugin-teardown-shape --self-test` lacked a
fixture commit that is missing from a shallow clone, and
`check:dual-build-cjs-loads` needs an all-package build. Both were
re-run green after fetching that commit and building every package. Both
refusals are recorded as refusals, not as passes.
- Lint, as a narrowed proof at `061c8f78e`. ① Population, read from
eslint's own config: the two `.ts` files are linted, and the changeset
answers "File ignored because no matching configuration was supplied". ②
`--format json`: 2 files linted, 0 errors, 0 warnings. ③ Invariance:
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), and its only disk reads are two baseline JSON
files this diff does not touch, so no untouched file's verdict can move.
The full `pnpm lint` is CI's.
- Changeset: `.changeset/23245-dispatcher-auth-gate-outage.md`,
`@objectstack/runtime` `minor`, `fix(runtime)!:`, one ADR-0087 marker
(`not-required (no-migration-prescription)`), and the launch-window
BREAKING line, per PR #23133's precedent. `check-adr-0087-registration`
passes: `1 declared-breaking changeset(s), each carrying an ADR-0087
disposition`.
## Acceptance notes
- `packages/runtime/src/http-dispatcher.root-auth-gate.test.ts` (header
near line 44, and the comment near line 110) says `enforceAuthGate`
fails open on "any thrown error". After this PR that is no longer true
for an active gate's failed session read. The prose is not an assertion,
and the file is outside this PR's claimed surface. Carrier: the next PR
that touches that file.
- #23335 (in flight): its branch head `c12ba7bd0` changes only comments
in `packages/core/src/security/auth-gate.ts`. The pin's posture is an
expired password on a route the allow-list does not name, which that
change does not touch.
- PR #23346 has merged. The 503's rendering does not go through
`dispatcher-error-vocabulary.ts`, which is a gate's declaration file and
is not imported at runtime, so the two changes are not coupled. The pin
stays green on top of it.
- `CONTRACT_REVIEW_TIER` review of record: owed before the queue (the
seat runs it).
---
_Generated by [Claude
Code](https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 44cadd3 commit 5d3018d
3 files changed
Lines changed: 357 additions & 22 deletions
File tree
- .changeset
- packages/runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
0 commit comments