mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-01 07:02:35 +00:00
* feat(web): send the deploy access token as an Authorization header The public edge added in #8 answers 401 to every /api/* request that arrives without `Authorization: Bearer <token>`, so the deployed UI is currently locked out of its own backend. This is the browser half. `fetchWithTimeout` is the one chokepoint every REST call passes through, so the header is merged there, once. The merge uses `Headers` rather than an object spread because callers pass their own `headers` — `runQuery` and `search` send `Content-Type: application/json` — and a spread would silently drop one side depending on ordering. `streamSSE` gains the same header plus the hooks the heartbeat migration needs next: `onOpen`, `onReconnecting`, and a retry policy (`maxRetries`/`capDelayMs`/`retryOnHttpError`). Defaults are unchanged, so both existing callers keep their 3-retry behavior. The token lives in sessionStorage, following the precedent `core/llm/settings-service.ts` set when it migrated provider API keys off localStorage and started deleting the legacy copy — a deploy token is the same class of secret, so it gets the same one-session lifetime. It is not a cookie: the edge strips `Origin` before proxying, so there is no CSRF backstop, and a cookie would ride along on any cross-site request. `assertOk` maps the edge's `{ code: 'unauthorized' }` discriminator to a distinct `BackendError.code`, dispatching on the body code rather than the message text as the `origin_not_allowed` precedent does, so the UI can prompt for a token instead of showing a raw 4xx. An absent token sends no header at all rather than an empty one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(web): run the heartbeat on streamSSE instead of EventSource `EventSource` has no API for custom headers, so the heartbeat was the one `/api/*` caller that could not send `Authorization: Bearer <token>`. On a gated deploy it would 401 forever no matter what the rest of the UI does, and the alternatives — exempting /api/heartbeat from the edge gate, or putting the token in the query string where Render logs it — both weaken the boundary #8 just established. `streamSSE` is already a fetch-based SSE reader, so the heartbeat now runs on it with `maxRetries: Infinity` and `capDelayMs: 15_000`. That reproduces what `EventSource` gave for free: indefinite reconnect with capped backoff, one `onReconnecting` notification per outage (deduped on `onOpen`), and teardown via the returned cleanup. `retryOnHttpError` is on for this stream, which is better than the old behavior: a 401 is transient here, so the heartbeat recovers by itself once a token is entered rather than needing a reload. The tests move off the `EventSource` mock to a fake fetch with a controllable ReadableStream, keeping every previous behavioral assertion and adding the header and 401-recovery cases. The backoff-cap test now pins the delay exactly — it fails if the cap is removed, where the old advance-16s-per-attempt version would still have passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(web): let the operator enter the deploy access token Completes the browser half of the edge token gate. A gated deploy answers 401 to every /api/* call, so the probe now distinguishes "no server there" from "server there, needs a token" (`probeBackendStatus`), and `useBackend` surfaces that as `isUnauthorized`. On a 401 the onboarding screen shows `AccessTokenPrompt` instead of the "run gitnexus serve" guide — the server is already running, so that guide is wrong advice. The copy names where the value lives: the gitnexus-web service's GITNEXUS_SERVE_AUTH_TOKEN environment variable in the Render dashboard. Saving it restarts the existing poll chain, which reconnects on its next tick, and the heartbeat recovers on its own now that it retries past a 401. Settings gains the same field for changing the token later. It renders unconditionally rather than inside the existing "Local Server" block: that block is gated on `backendUrl`/`onBackendUrlChange` props, and App.tsx never passes them, so a field placed there would never appear. Reviving that block is a separate question and out of scope here. `type="password"` with an explicit reveal toggle, never logged, never in a URL. `formatBackendError` maps the new `unauthorized` code so a 401 from any other call site reads as actionable guidance rather than a raw 4xx, in both locales. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(web): extract SecretInput for the masked token field The token field appeared twice, once in AccessTokenPrompt and once in SettingsPanel, as ~25 lines of identical input + reveal-toggle markup with the same Tailwind class string. That is the third copy of a shape the repo already has in settings/ProviderConfigCard's apiKey field. SecretInput owns the reveal state, so both call sites drop a useState and SettingsPanel drops its reset in the isOpen effect: the panel returns null when closed, which unmounts the field, so it reopens masked on its own. Also moves AUTH_TOKEN_STORAGE_KEY below the DEFAULT_*_URL group in ui-constants, where its 14-line comment no longer splits related defaults. No behavior change. 409 tests pass, tsc clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(web): make the probe status and the token save explicit Two review findings from the token work, both about representable states. `probeBackendStatus` returned `{ok, unauthorized}`, which admits `{ok: true, unauthorized: true}` — a backend that is simultaneously authorized and gated. It now returns a `BackendProbeStatus` of 'ok' | 'unauthorized' | 'unreachable', so the impossible pair cannot be built. `probeBackend` keeps its boolean signature, so `useAppState` is untouched. The settings panel persisted the token on every keystroke while every other field in it waits for Save. That stored half-typed tokens, sent them on the next probe, and gave no "Saved" confirmation for the one field that most needs it. The token now persists in `handleSave` with the rest, pinned by two tests: typing alone does not persist, and emptying the field then saving clears the stored token. 411 tests pass, tsc clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(web): drop the probeBackend wrapper and trim duplicated rationale `probeBackend` existed only as `probeBackendStatus() === 'ok'` for a single caller. `useAppState.isDatabaseReady` now does that comparison itself, so the module exports one probe instead of two. The probe tests move into backend-client-auth.test.ts, where the existing breaker reset, backend URL, and `jsonOk` helper already do their setup — they had been sitting in the component test file with a duplicate `beforeEach`. They also belong next to the client they exercise. Trims the access-token comments: AUTH_TOKEN_STORAGE_KEY holds the sessionStorage-and-not-a-cookie rationale, and backend-client was restating parts of it three times. 411 tests pass, tsc clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| e2e | ||
| src | ||
| test | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vercel.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||