[AZ-510][AZ-511][AZ-512][AZ-513] Cycle 3 Steps 12-15 + admin prereq
ci/woodpecker/push/build-arm Pipeline failed

Wrap up cycle 3 across the autodev existing-code Phase B steps that
follow Implement (Steps 12-15), plus the cross-workspace prerequisite
ticket filed for AZ-512.

Step 12 - Test-Spec Sync:
- Un-quarantine FT-P-01 in traceability-matrix (closed by AZ-510)
- Add AZ-510 chained /users/me failure-path test reference under AC-23
- Note AZ-512 deferral status under O9 (P12 Phase B target)

Step 13 - Update Docs (task mode):
- Refresh src__auth__AuthContext module doc with AZ-510 wire shape
  (POST refresh + chained /users/me + bootstrapInflight guard)
- Add usersMe() to src__api__endpoints module doc + consumer note
- Rename src__features__annotations__classColors module doc to
  src__class-colors__classColors (matches AZ-511 git mv); refresh header
- Refresh src__components__DetectionClasses + src__features__annotations
  module group doc for the new class-colors barrel import path
- Update components/11_class-colors Module Inventory to point at the
  renamed module doc filename
- Rewrite system-flows.md Flow F2 (Bearer auto-refresh) with the AZ-510
  POST + chained /users/me sequence; close Finding B3 references
- Generate ripple_log_cycle3 documenting all changed source files,
  their reverse-dependency search results, and the docs touched

Step 14 - Security Audit (cycle-3 delta):
- Resume mode against cycle-2 baseline; cycle-2 artifacts untouched
- Re-run bun audit on both roots: clean (cycle-2 inline fix held)
- Re-rate OWASP A06: FAIL -> PASS; A07: PASS_WITH_KNOWN -> PASS (B3
  closed by AZ-510)
- New finding F-SAST-CY3-1 (LOW): __resetBootstrapInflightForTests
  exposed via src/auth public barrel; defer to hygiene cycle
- Verdict: FAIL -> PASS_WITH_WARNINGS; one HIGH (F-SAST-1
  mission-planner git-history key, unchanged) remains
- Add amendment banner to cycle-2 security_report.md

Step 15 - Performance Test:
- Static profile NFT-PERF-01 PASS (290 575 B gzipped vs 2 MB budget;
  ~14% of budget; no regression from AZ-510 surface additions)
- E2E profile SKIP (Playwright perf project still pending AZ-457..AZ-482);
  legitimate skip per test-run skill, gap acknowledged in report
- AZ-510 200ms p95 chain NFR verified at spec level only - no CI gate
  yet (covered by future AZ-457..AZ-482 work)

Cross-workspace prerequisite (AZ-513 just filed):
- Updated _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md
  to reflect AZ-513 filing on admin/ workspace (parent epic AZ-509,
  Blocks link to AZ-512). Companion task spec added in admin/ repo
  (separate commit there, owned by admin/ workspace).

State file: advanced to Step 16 (Deploy) per autodev existing-code flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 03:58:21 +03:00
parent 6c7e29722f
commit 09449bda2c
14 changed files with 448 additions and 41 deletions
+21 -12
View File
@@ -123,16 +123,18 @@ flowchart TD
---
## Flow F2: Bearer auto-refresh on 401 (TWO refresh paths exist in code)
## Flow F2: Bearer auto-refresh (bootstrap + 401-retry)
> **Cycle 3 / 2026-05-13 — AZ-510 consolidated the two refresh paths.** The historical "two divergent paths" wording below has been rewritten. The previous bug (finding B3 / Vision P3 violation) is now CLOSED.
### Description
There are **two distinct refresh code paths** in the source — the verification pass (Step 4) caught both:
There are two refresh trigger points in the source, but they now share a single wire shape:
1. **Bootstrap path**`AuthContext.tsx:24` calls `api.get('/api/admin/auth/refresh')` on app mount. This **does NOT have `credentials:'include'`** because `api/client.ts` doesn't add it on GET. Result: the cookie is not sent, the bootstrap silently fails, the user starts unauthenticated even when they have a valid refresh cookie.
2. **401-retry path**`api/client.ts:44` calls `fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })` automatically when any authenticated fetch returns 401. This path IS correct.
1. **Bootstrap path**`AuthContext.tsx` (`runBootstrap()` helper, guarded by a module-scoped `bootstrapInflight` promise to deduplicate React 18+ StrictMode dev double-mounts). On `<AuthProvider>` mount it calls `fetch(getApiBase() + endpoints.admin.authRefresh(), { method: 'POST', credentials: 'include' })`. On success it sets the bearer and **chains** `api.get<AuthUser>(endpoints.admin.usersMe())` (= `GET /api/admin/users/me`) to fetch the user record (the POST refresh response is `{ token }` only). On any failure path the bearer is cleared first, then `user: null` + `loading: false`.
2. **401-retry path**`api/client.ts:73` automatically calls `fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })` and replays the original request when any authenticated fetch returns 401.
The bootstrap path is the bug surfaced as finding B3 PRIORITY. The 401-retry path is the silent fallback that does work but only after the user has already hit a 401.
Both paths now POST with `credentials:'include'` and rely on the HttpOnly refresh cookie set on `/login`.
### Preconditions
@@ -157,7 +159,7 @@ sequenceDiagram
ApiClient-->>Page: response
```
### Sequence Diagram (Bootstrap path on app mount — broken)
### Sequence Diagram (Bootstrap path on app mount — POST refresh + chained `/users/me`, AZ-510)
```mermaid
sequenceDiagram
@@ -166,18 +168,25 @@ sequenceDiagram
participant AdminApi as admin/ service
App->>AuthCtx: <AuthProvider> mounts
AuthCtx->>AdminApi: GET /admin/auth/refresh (NO credentials:'include' — finding B3)
AdminApi-->>AuthCtx: 401 (no cookie sent)
AuthCtx->>AuthCtx: setLoading(false), user stays null
AuthCtx-->>App: ProtectedRoute sees null user → redirects to /login
AuthCtx->>AuthCtx: bootstrapInflight guard (StrictMode dedupe)
AuthCtx->>AdminApi: POST /admin/auth/refresh (credentials:'include')
AdminApi-->>AuthCtx: 200 {token} + Set-Cookie: refresh=...
AuthCtx->>AuthCtx: setToken(token)
AuthCtx->>AdminApi: GET /admin/users/me (Authorization: Bearer <token>)
AdminApi-->>AuthCtx: 200 {id, email, permissions}
AuthCtx->>AuthCtx: setUser(...), setLoading(false)
AuthCtx-->>App: ProtectedRoute sees user → renders gated route
```
### Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Bootstrap GET refresh missing `credentials:'include'` | `AuthContext.tsx:24` | server returns 401 because cookie was not sent | **Bug today** — finding B3 PRIORITY. Symptom: a user with a valid refresh cookie still gets bounced to `/login` on every fresh tab. Step 4 fix: change to POST with `credentials:'include'` (matching the 401-retry path), or just delete the bootstrap GET and let the first authenticated fetch's 401 trigger the retry path. |
| 401-retry path | `api/client.ts:44` | works | (no fix needed) |
| ~~Bootstrap GET refresh missing `credentials:'include'`~~ | — | — | **CLOSED 2026-05-13 by AZ-510.** Bootstrap now POSTs with `credentials:'include'`. Finding B3 / Vision P3 violation resolved. |
| Refresh 401 on bootstrap | `AuthContext.tsx` `runBootstrap()` | non-OK response from POST refresh | `setUser(null)` + `setLoading(false)``ProtectedRoute` redirects to `/login`. No console.error (expected on first visit / signed-out user). |
| Refresh network error on bootstrap | `AuthContext.tsx` `runBootstrap()` | outer `.catch` on the POST refresh fetch | `setToken(null)` + `setUser(null)` + `setLoading(false)` + `console.error('[AuthContext] Bootstrap failed:', err)`. UI redirects to `/login`. |
| Refresh 200 → `/users/me` failure (401, network, etc.) | `AuthContext.tsx` `runBootstrap()` | inner `try/catch` around `api.get(usersMe())` | `setToken(null)` first (Constraint #4 — bearer cleared before user state) + `console.error('[AuthContext] Refresh succeeded but /users/me failed:', err)` + return null → top-level then-handler sets `user: null` + `loading: false`. Covered by `AC-4 (AZ-510)` regression test. |
| 401-retry path inside `api/client.ts` | `api/client.ts:73` | works | (no fix needed) |
| Refresh cookie expired or revoked | refresh call | 401 | UI redirects to `/login`. |
| SSE subscription holds a stale bearer | active EventSource | server closes the SSE stream | No reconnect logic today (Step 8 hardening). |