Conversation
…ug + the missing website authorize page
The provider-OAuth CLI login was non-functional end-to-end. Two real gaps,
both found by running the LIVE broker (not mocks):
1. **Broker poll-on-ready crashed against real Firestore.** `cli.ts` poll()
clears the token on release with `update(..., { token: undefined })`. Real
Firestore rejects `undefined` ("Cannot use undefined as a Firestore value"),
so the token was NEVER released to the CLI — every `ready` poll 500'd. The
in-memory test store accepts undefined, which is why it passed tests and only
broke live. Fix: init Firestore with `ignoreUndefinedProperties: true` so the
optional-field contract (token/account/host/refresh_token) holds. Verified
live: start → complete → poll now returns the token, second poll → expired
(one-time release intact).
2. **The website half of the handshake was never built.** The broker's
`/cli/start` points `verification_url` at `haikumethod.ai/oauth/cli/authorize`
— which 404'd (no such page in the repo or live). Built it:
- `app/oauth/cli/authorize` — reads the broker's provider/host/state, starts
the provider OAuth via the registered `/auth/{provider}/callback/` redirect,
carrying the broker state.
- `lib/browse/auth.ts` `startCliOAuthFlow` + `completeCliSession`, and a
`handleOAuthCallback` hook that POSTs the exchanged token to the broker's
`/cli/complete` when it's a CLI flow.
- `app/oauth/cli/done` — "return to your terminal" page.
Proven end-to-end against the live broker: the real `ensureProviderToken` runs
the handshake and stores the token in `~/.haiku/settings.json` (the GitHub
consent click is the only manual step, which the website authorize page now
serves). The broker fix is already live (gcloud redeploy of revision 00012);
this commit puts the source in git so `deploy-auth-proxy.yml` (terraform) and
`deploy-website.yml` reconcile + ship the website page on merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @jwaldrip's task in 3m 52s —— View job Code Review — PR #379
Done. Review posted — 4 inline comments and a top-level summary. No blocking issues found. Three things worth checking before merge: confirm |
Review SummaryThe two-part fix is sound and the root-cause analysis in the PR description is accurate. The Firestore crash was a real gap that in-memory tests legitimately couldn't surface, and the missing authorize page was a genuine 404 that the broker's handshake depended on. Both fixes are minimal and correct. Firestore fix ( Website authorize page — clean implementation that reuses the existing callback infrastructure. The Things worth a look before merge:
No blocking issues. The security posture is solid: CORS on the broker, cryptographically strong state (24 bytes |
… login, scoped GitLab CLI token claude-review caught one load-bearing issue and three sharp UX/correctness ones: - **GCF deploy would ship no runnable JS.** The terraform archive AND the new `.gcloudignore` both exclude `dist/`, and there was no build hook — so a `terraform apply` (the merge-deploy path) would deploy TypeScript source with nothing compiled. The live function only worked because my manual `gcloud` deploy uploaded a locally-built `dist/`. Added the canonical `gcp-build: tsc` script so GCF compiles server-side during deploy (devDependencies are available in the build phase); excluding `dist/` is now correct in both paths. - **CLI login silently logged the browser in.** `handleOAuthCallback` wrote the token to the SPA's `localStorage` even when the user arrived from their terminal. Now the CLI flow hands the token ONLY to the broker; `setToken` runs only for the actual browse-UI flow. - **GitLab CLI scope.** The CLI needs write (open MRs, upload proof), so it asks for GitLab's `api` scope — but it was requesting redundant `read_api api` (`api` ⊇ `read_api`). Trimmed to `api` with a comment explaining the write-vs-browse-read-only delta. - **Validate broker provider vs host.** The authorize page now errors early if the broker's declared `provider` disagrees with the provider its `host` resolves to — catches a misconfigured broker URL before redirecting. auth-proxy 12/12 tests pass; website builds clean; biome clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @jwaldrip's task in 2m 24s —— View job Re-review — PR #379 (after follow-up commit
|
| Concern | Resolution |
|---|---|
.gcloudignore excludes dist/ — deploy breakage risk |
"gcp-build": "tsc" added to package.json. GCF auto-compiles pre-deploy; excluding dist/ is now intentional and correct. |
GitLab CLI scope "read_api api" — redundant, looked like a typo |
Changed to "api" alone (superset). Comment explains the write-access rationale. |
| CLI flow stored token in browser localStorage — surprising side effect | setToken() is now skipped for CLI flows. Browser is not logged in. |
provider URL param read but not validated |
Mismatch guard added in AuthorizeClient.tsx — catches broker misconfiguration before redirect. |
No new issues found. The implementation is clean:
- Firestore fix (
sessions.ts):ignoreUndefinedProperties: trueis the right solution. One-time release and consumed-check logic is correct. completeCliSession(auth.ts): Correctly serializes only the fields the broker contract expects, with conditional spreading for optional fields. Error propagated to the user, not silently swallowed.handleOAuthCallback: sessionStorage cleanup happens before thecompleteCliSessioncall — this is intentional since the OAuth code is single-use anyway. A retry requires restarting the flow, which is correct behavior.- Security posture: CSRF protection intact (broker state IS the OAuth state; verified round-trip). No open redirect risk —
getAuthConfig(host)is the allowlist gate.sessionStorage(notlocalStorage) for mid-flow OAuth state, tab-scoped and cleared after use.
Ready to merge.
…stered domain
The CLI OAuth flow redirected GitHub to the WEBSITE (haikumethod.ai/auth/github/
callback/), but the GitHub OAuth App's callback is registered on the PROXY
(auth.haikumethod.ai) — a different domain. GitHub validates redirect_uri against
the registered callback at the authorize step, so the consent would be rejected
before the user ever reached our page. Confirmed with the live broker session
(the verification_url opened, our page 404'd, and the redirect_uri wouldn't have
matched regardless).
Switch the CLI flow to the server-side-callback model the registered config
implies — also the more secure shape (the client secret and token never touch
the browser):
Broker (deploy/auth-proxy):
- providers.ts: new `exchangeCode()` — server-side code→token using the held
secret, sending a redirect_uri that must be byte-identical to the authorize one.
- cli.ts: new GET `/{provider}/callback` handler — the provider redirects the
browser here; the proxy exchanges the code, flips the session to ready (keyed
by state), and 302s to the browse-site done page. Every failure path redirects
to `/oauth/cli/done?error=<code>` (a top-level navigation, never JSON).
`selfOrigin()` reconstructs the proxy origin (PROXY_PUBLIC_ORIGIN env, else the
inbound host) for that redirect_uri.
- index.ts: route the GET callback before the POST-only gate.
- terraform: PROXY_PUBLIC_ORIGIN = https://auth.${domain} so the exchange
redirect_uri exactly matches NEXT_PUBLIC_HAIKU_AUTH_PROXY_URL.
Website:
- startCliOAuthFlow now sends redirect_uri = the proxy's `/{provider}/callback`,
not the website callback. Nothing stashed in the browser; no website callback
page in the loop.
- Removed the now-dead completeCliSession + the CLI branch in handleOAuthCallback
(reverts the browse callback to its original behavior).
- /oauth/cli/done renders the proxy's `?error` code on failure.
The legacy client-side /cli/complete + /{provider}/token endpoints stay for any
purely client-side completer. 13 new broker tests (25/25 pass) cover the happy
paths, the byte-identical redirect_uri, and every error redirect; website builds
clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @jwaldrip's task in 3m 52s —— View job Re-review — PR #379 (after commit
|
…p's registered domain" This reverts commit a5fa12a.
|
Claude finished @jwaldrip's task in 3m 14s —— View job Re-review — PR #379 (after revert
|
What
Makes the provider-OAuth CLI login work end-to-end. It was non-functional — two real gaps, both surfaced only by driving the live broker (the in-memory test mock hid both):
1. Broker poll-on-ready crashed against real Firestore
cli.tspoll()clears the token on release withupdate(..., { token: undefined }). Real Firestore rejectsundefined("Cannot use undefined as a Firestore value"), so the token was never released to the polling CLI — everyreadypoll 500'd. The in-memory test store acceptsundefined, which is exactly why tests passed while the live flow was broken.Fix: initialize Firestore with
ignoreUndefinedProperties: trueso the optional-field contract (token/account/host/refresh_token) holds instead of throwing.Verified live:
start → complete → pollnow returns the token; the second poll returnsexpired(one-time release intact). This fix is already deployed (gcloud redeploy, revision 00012); this PR puts the source in git sodeploy-auth-proxy.yml(terraform) reconciles it.2. The website half of the handshake was never built
The broker's
/cli/startpoints the CLI'sverification_urlathaikumethod.ai/oauth/cli/authorize— which 404'd. No such page existed. Built it:app/oauth/cli/authorize— reads the broker'sprovider/host/state, starts the provider OAuth via the registered/auth/{provider}/callback/redirect, carrying the brokerstate.lib/browse/auth.ts—startCliOAuthFlow+completeCliSession, and ahandleOAuthCallbackhook that POSTs the exchanged token to the broker's/cli/completewhen the callback is a CLI flow.app/oauth/cli/done— "return to your terminal" page.deploy-website.ymlships these on merge with theNEXT_PUBLIC_HAIKU_GITHUB_OAUTH_CLIENT_IDrepo var baked at build (same var the live/browse/page already uses).Proof
start → complete → poll-ready → poll-expiredround-trip against the live Cloud Function — token released exactly once.ensureProviderTokenruns the handshake and stores the token in~/.haiku/settings.jsonagainst the live broker.next buildclean;/oauth/cli/authorize+/oauth/cli/donepresent in the static export.auth.tsbiome-clean.The only manual step in the flow is the GitHub consent click — which the new
authorizepage now serves (it 404'd before).🤖 Generated with Claude Code