Skip to content

cloud/C5: Lumina Cloud HTTP client + LuminaCloudError#221

Closed
blueberrycongee wants to merge 2 commits intoloop/cloud-C1from
loop/cloud-C5
Closed

cloud/C5: Lumina Cloud HTTP client + LuminaCloudError#221
blueberrycongee wants to merge 2 commits intoloop/cloud-C1from
loop/cloud-C5

Conversation

@blueberrycongee
Copy link
Copy Markdown
Owner

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 LuminaCloudError class 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.

LuminaCloudErrorCode extends CloudErrorCode (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-After is captured on 429 and any other status that sets it.

getBaseUrl() reads VITE_LUMINA_CLOUD_BASE_URL from import.meta.env first, then falls back to process.env. The fallback exists because vitest's vi.stubEnv mirrors values into process.env but not always into import.meta.env — without the fallback the env-override test fails.

Acceptance criteria

  • verifyLicenseOnline(license)§2.1 shape.
  • getModels(license)§2.3 shape.
  • getUsage(license)§2.4 shape.
  • getRevocations(since?)§2.5 shape.
  • All errors mapped from CONTRACT.md §6 to a typed LuminaCloudError class.
  • Base URL configurable via env (default https://api.lumina-note.com).
  • Tests use a manual fetch mock — no msw added.

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 outside cloud/PRD.md §3 surfaces touched.
  • No package.json changes — task allowed manual fetch mock or msw; chose manual to avoid the dep.

Notes for Lead

  • I extended CloudErrorCode rather 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 for network / unknown.
  • verifyLicenseOnline returns 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 treat valid: false as "license rejected", not "API failed".
  • getRevocations(since?) URL-encodes the since parameter via URL.searchParams; the test asserts 2026-04-28T00:00:00Z round-trips as 2026-04-28T00%3A00%3A00Z (colons are reserved in the query string).

blueberrycongee and others added 2 commits April 28, 2026 12:43
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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@blueberrycongee
Copy link
Copy Markdown
Owner Author

Closing — re-issued cleanly by integrator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant