GitNexus/gitnexus-web
Shifra Williams 72014cc7db
feat(web): let the operator enter the deploy access token (#9)
* 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>
2026-08-05 13:57:23 -07:00
..
e2e fix(web): use repo path identity in switcher (#2420) 2026-07-11 11:43:47 +01:00
src feat(web): let the operator enter the deploy access token (#9) 2026-08-05 13:57:23 -07:00
test feat(web): let the operator enter the deploy access token (#9) 2026-08-05 13:57:23 -07:00
.gitignore gitnexus wal cleanup preventing reindexing issue fixed 2026-02-06 06:35:15 +05:30
index.html feat: configure prettier with pre-commit hook (#563) 2026-03-28 14:58:04 +00:00
package-lock.json chore(deps)(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#2823) 2026-08-04 09:32:20 +01:00
package.json chore(deps)(deps): bump i18next from 26.3.0 to 26.3.6 in /gitnexus-web (#2751) 2026-07-31 10:34:16 +01:00
playwright.config.ts feat: configure prettier with pre-commit hook (#563) 2026-03-28 14:58:04 +00:00
tsconfig.app.json feat: unify web and cli ingestion pipeline (#536) 2026-03-28 14:07:11 +00:00
tsconfig.json feat: configure prettier with pre-commit hook (#563) 2026-03-28 14:58:04 +00:00
tsconfig.node.json feat: merge gitnexus-mcp into gitnexus package - unified CLI+MCP 2026-02-04 01:12:41 +05:30
vercel.json ci(web): use npm ci for deterministic Vercel installs (#1764) 2026-05-22 05:08:42 +01:00
vite.config.ts feat: add docker support (#848) 2026-04-18 08:39:18 +01:00
vitest.config.ts feat(web): Introduce Tree View and Circles View in Web Viewer (#1799) 2026-05-26 18:05:50 +01:00