cloud/C5: Lumina Cloud HTTP client + LuminaCloudError#221
cloud/C5: Lumina Cloud HTTP client + LuminaCloudError#221blueberrycongee wants to merge 2 commits intoloop/cloud-C1from
Conversation
Implements the four endpoints from CONTRACT.md §2: - verifyLicenseOnline (§2.1, no auth, body-based) - getModels (§2.3, Bearer auth) - getUsage (§2.4, Bearer auth) - getRevocations (§2.5, no auth, optional ?since) LuminaCloudError class consolidates §6 server-defined codes plus two client-side labels: 'network' (fetch threw before reaching the wire) and 'unknown' (unmapped HTTP status with no parseable body). Server codes win when the response body parses; status fallback is used otherwise. Retry-After header is captured on 429 (and any other status that supplies it). Base URL is configurable via VITE_LUMINA_CLOUD_BASE_URL; getBaseUrl() reads import.meta.env first and falls back to process.env so vitest's vi.stubEnv works in the test harness. Default https://api.lumina-note.com per the contract. 21 tests cover: env override + default + whitespace fallback; endpoint URLs / methods / auth headers / body shapes for all four; status→code mapping for 8 HTTP codes; server body code overrides status mapping; Retry-After parsing; fetch rejection → 'network'; unmapped 4xx → 'unknown'. No new runtime deps — manual fetch mock instead of msw, as the task allows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ff60f14dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| throw await readError(response); | ||
| } | ||
| // Tolerate empty 204 etc., though the contract has no 2xx-without-body cases. | ||
| return (await response.json()) as T; |
There was a problem hiding this comment.
Normalize malformed 2xx bodies into LuminaCloudError
fetchJson unconditionally parses every successful response with response.json(), so a 204 No Content or any non-JSON 2xx payload will throw a raw SyntaxError instead of a LuminaCloudError. This breaks the client’s typed error contract and can bypass caller logic that only handles LuminaCloudError (e.g., graceful offline/error UX), turning recoverable API anomalies into untyped runtime failures.
Useful? React with 👍 / 👎.
|
Closing — re-issued cleanly by integrator. |
What
Fills in the C1 client.ts stubs with a typed fetch-based client for the four endpoints in CONTRACT.md §2 (verify, models, usage, revocations) plus a
LuminaCloudErrorclass that consolidates §6 server codes with client-side categories.Stacked on #217 (C1). No dependency on C2/C4, so this can ship in either order.
LuminaCloudErrorCodeextendsCloudErrorCode(server-defined per §6) with two client-side labels:'network'— fetch rejected before reaching the wire (DNS, offline, TLS).'unknown'— unmapped HTTP status with no parseable error body.When the response body parses as a §6 envelope, the server code wins. Otherwise we fall back to a status-code mapping (400 →
bad_request, 401 →invalid_license, 402 →quota_exceeded, 403 →feature_disabled, 404 →not_found, 429 →rate_limit, ≥500 →internal, 502 →upstream_unavailable).Retry-Afteris captured on 429 and any other status that sets it.getBaseUrl()readsVITE_LUMINA_CLOUD_BASE_URLfromimport.meta.envfirst, then falls back toprocess.env. The fallback exists because vitest'svi.stubEnvmirrors values intoprocess.envbut not always intoimport.meta.env— without the fallback the env-override test fails.Acceptance criteria
verifyLicenseOnline(license)→§2.1shape.getModels(license)→§2.3shape.getUsage(license)→§2.4shape.getRevocations(since?)→§2.5shape.CONTRACT.md §6to a typedLuminaCloudErrorclass.https://api.lumina-note.com).mswadded.How I tested
npm run typecheck: pass.npm test -- --run src/services/luminaCloud/client.test.ts: 21/21 pass.Test coverage: env override + default + whitespace fallback (3); per-endpoint URL / method / auth header / body shape (5); status→code mapping for 8 HTTP codes (8); server-body code overrides status fallback (1); Retry-After parsing (1); fetch rejection →
network(1); unmapped 4xx →unknown(1); ?since serialization (1) — total 21.Touched files outside src/services/luminaCloud/
cloud/TASKS.md— marked C5[x]and appended Done-log entry. No other files outsidecloud/PRD.md§3 surfaces touched.package.jsonchanges — task allowed manual fetch mock ormsw; chose manual to avoid the dep.Notes for Lead
CloudErrorCoderather than narrowing it. The contract enumerates server codes only, but callers (C9 CloudUsagePanel especially) need to distinguish "we never reached the server" from "the server said no". The two extra labels are explicit on the type so a switch-on-code that handles only contract codes will still get a TS exhaustiveness warning fornetwork/unknown.verifyLicenseOnlinereturns the{ valid: false, reason }body as a regular value (not a thrown error). Per CONTRACT.md §2.1 the server returns 200 either way, so a thrown error would be wrong. Callers should treatvalid: falseas "license rejected", not "API failed".getRevocations(since?)URL-encodes thesinceparameter viaURL.searchParams; the test asserts2026-04-28T00:00:00Zround-trips as2026-04-28T00%3A00%3A00Z(colons are reserved in the query string).