Files
henryor af5535dfcc docs(openspec): archiva el change console-tech-debt-cleanup
Persiste proposal, specs delta (auth, api-client, access-requests,
platform-shell, testing), design con 8 ADRs, tasks (27 items en 10 fases),
verify-report (28 tests, 0 críticos, 0 warnings tras el cierre) y
archive-report final.

Cierra el ciclo SDD del cleanup de deuda técnica del console.
2026-05-07 21:14:56 -05:00

3.3 KiB

API Client Specification

Delta for: API Client — console-tech-debt-cleanup

ADDED Requirements

Requirement: No null-as-T Casts; Typed 204 Path

apiClient<T> MUST NOT contain null as T. For endpoints that legitimately return HTTP 204 (no body), callers MUST use a dedicated apiClientVoid (or equivalent) that returns Promise<void>. apiClient<T> MUST throw or return a typed error if invoked against an endpoint that responds 204. TypeScript MUST be able to statically distinguish body-returning calls from void calls.

Scenario: 200 response with body

  • GIVEN a call apiClient<User>('/users/1', { method: 'GET' })
  • WHEN the server responds HTTP 200 with a JSON body { id: "...", email: "..." }
  • THEN the function resolves with User typed data
  • AND no cast is required at the call site

Scenario: 204 response via apiClientVoid

  • GIVEN a call apiClientVoid('/access-requests/123/approve', { method: 'PUT' })
  • WHEN the server responds HTTP 204
  • THEN the function resolves with void (no body)
  • AND the TypeScript return type is Promise<void> — not Promise<null> or Promise<undefined>

Scenario: Caller cannot pass 204-returning endpoint to apiClient

  • GIVEN a developer writes apiClient<void>('/approve', ...)
  • WHEN TypeScript compiles
  • THEN a compile-time error surfaces (or the function signature prevents the void generic)

Requirement: 401 Triggers Single Refresh-and-Retry

When apiClient receives HTTP 401 on the first attempt (non-bypassed endpoint, non-retry call), it MUST invoke SessionCoordinator.refresh() exactly once, then replay the original request with the new token. If the retried request succeeds, apiClient MUST resolve with the response body. If refresh fails or the retry also fails, apiClient MUST call logout(), redirect to /login, and throw.

Scenario: 401 with refresh success, retry succeeds with body

  • GIVEN an authenticated call returns HTTP 401
  • AND SessionCoordinator.refresh() returns a new access token
  • WHEN the retried request returns HTTP 200 with body
  • THEN apiClient resolves with the parsed response body
  • AND only one refresh call was made

Scenario: 401 with refresh success, retry returns 204

  • GIVEN an authenticated call returns HTTP 401
  • AND SessionCoordinator.refresh() returns a new access token
  • WHEN the retried request returns HTTP 204
  • THEN apiClientVoid resolves with void
  • AND the caller is NOT handed a null value

Scenario: 401 with refresh failure

  • GIVEN an authenticated call returns HTTP 401
  • AND SessionCoordinator.refresh() returns null
  • THEN logout() is called
  • AND the browser is redirected to /login
  • AND apiClient throws with message "Sesion expirada"

Requirement: SessionCoordinator Deduplicates Concurrent Refresh Calls

If multiple in-flight requests trigger a 401 simultaneously, SessionCoordinator.refresh() MUST execute the underlying HTTP refresh request exactly once. All concurrent callers MUST await the same Promise and receive the same new token.

Scenario: Two concurrent 401s produce one refresh HTTP call

  • GIVEN two apiClient calls are in flight simultaneously
  • WHEN both receive HTTP 401
  • THEN SessionCoordinator.performRefresh is called exactly once (one HTTP POST to /auth/refresh)
  • AND both original requests are retried with the same new token