Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb — "Google OAuth connect and disconnect" (Feature 16, Google Calendar Sync, task 1 of 5).
Done-condition
GET /api/v1/integrations/google/connect redirects to Google's consent screen with scopes calendar.events and calendar.calendarlist.readonly, access_type=offline, prompt=consent and a state bound to the session; GET /api/v1/integrations/google/callback validates state, exchanges the code (client id from MEALQUEST_GOOGLE_CLIENT_ID, secret from MEALQUEST_GOOGLE_CLIENT_SECRET_FILE, redirect under MEALQUEST_PUBLIC_URL), stores the refresh token with secretbox and sets connected_at; DELETE /api/v1/integrations/google revokes the token at Google and clears it. Tests against an httptest fake Google token/revoke endpoint cover connect, bad state, missing refresh token (error asking to reconnect) and disconnect; no response ever contains the token.
Scope
Connect and disconnect only. The remaining four tasks (calendar list and selection, push to Calendar, token-failure handling, settings screen) are untouched, and the shopping-list trigger belongs to Feature 17 and was deliberately not stubbed.
Points for review
Configuration is required at boot (MEALQUEST_GOOGLE_CLIENT_ID, MEALQUEST_GOOGLE_CLIENT_SECRET_FILE), matching the OIDC variables and the fail-fast house rule rather than adding a second "Calendar not configured" path. Consequence, stated by the developer: production cannot start on a release containing this change until the owner creates the Google OAuth web client and sets the secret. The runbook section "Google Calendar OAuth client", deploy/compose.yaml's secret mount and deploy/.env.example were updated in the same commit. Please judge whether boot-required is the right call and whether that deployment coupling is acceptable and adequately documented.
The endpoints reuse Feature 03's session-bound state patterns rather than a second mechanism; the callback is reachable in a browser redirect context and the auth requirement chosen for each of the three endpoints is stated in the developer's NOTES.
internal/auth/cookie.go, internal/auth/auth.go and the NewRouter signature were touched; all NewRouter call sites in tests were updated.
migrations/queries/settings.sql was extended and sqlc regenerated (checked in, no drift); the existing integrations_google table was used, so no new migration.
go test -race -count=1 ./... — all packages ok (server 102.5s)
CGO_ENABLED=0 go build ./... — ok
make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc — no drift
cd web && npm run typecheck && npm test && npm run build — PASS, 45 files / 764 tests (no web change)
The developer also reports mutation checks (removing the session-binding, refresh-token or revoke-status check each fails its own test; bypassing the exchange-error redaction fails the leak checks), and that the real binary exits 1 naming both variables when they are missing, and refuses MEALQUEST_GOOGLE_CLIENT_SECRET set inline.
## Task
Maestro task `1e9d7850-3a09-4572-b8a1-cb18cd4774cb` — "Google OAuth connect and disconnect" (Feature 16, Google Calendar Sync, task 1 of 5).
## Done-condition
`GET /api/v1/integrations/google/connect` redirects to Google's consent screen with scopes `calendar.events` and `calendar.calendarlist.readonly`, `access_type=offline`, `prompt=consent` and a state bound to the session; `GET /api/v1/integrations/google/callback` validates state, exchanges the code (client id from `MEALQUEST_GOOGLE_CLIENT_ID`, secret from `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE`, redirect under `MEALQUEST_PUBLIC_URL`), stores the refresh token with secretbox and sets `connected_at`; `DELETE /api/v1/integrations/google` revokes the token at Google and clears it. Tests against an httptest fake Google token/revoke endpoint cover connect, bad state, missing refresh token (error asking to reconnect) and disconnect; no response ever contains the token.
## Scope
Connect and disconnect only. The remaining four tasks (calendar list and selection, push to Calendar, token-failure handling, settings screen) are untouched, and the shopping-list trigger belongs to Feature 17 and was deliberately not stubbed.
## Points for review
- **Configuration is required at boot** (`MEALQUEST_GOOGLE_CLIENT_ID`, `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE`), matching the OIDC variables and the fail-fast house rule rather than adding a second "Calendar not configured" path. Consequence, stated by the developer: **production cannot start on a release containing this change until the owner creates the Google OAuth web client and sets the secret.** The runbook section "Google Calendar OAuth client", `deploy/compose.yaml`'s secret mount and `deploy/.env.example` were updated in the same commit. Please judge whether boot-required is the right call and whether that deployment coupling is acceptable and adequately documented.
- The endpoints reuse Feature 03's session-bound `state` patterns rather than a second mechanism; the callback is reachable in a browser redirect context and the auth requirement chosen for each of the three endpoints is stated in the developer's NOTES.
- `internal/auth/cookie.go`, `internal/auth/auth.go` and the `NewRouter` signature were touched; all `NewRouter` call sites in tests were updated.
- `migrations/queries/settings.sql` was extended and sqlc regenerated (checked in, no drift); the existing `integrations_google` table was used, so no new migration.
## Evidence (orchestrator-run gate at 59d0f57a47955601d2c21ae4504568650e8d352d)
- `gofmt -l .` — empty; `go vet ./...` — clean
- `go test -race -count=1 ./...` — all packages ok (server 102.5s)
- `CGO_ENABLED=0 go build ./...` — ok
- `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift
- `cd web && npm run typecheck && npm test && npm run build` — PASS, 45 files / 764 tests (no web change)
The developer also reports mutation checks (removing the session-binding, refresh-token or revoke-status check each fails its own test; bypassing the exchange-error redaction fails the leak checks), and that the real binary exits 1 naming both variables when they are missing, and refuses `MEALQUEST_GOOGLE_CLIENT_SECRET` set inline.
Implementation commit: `59d0f57a47955601d2c21ae4504568650e8d352d`
GET /api/v1/integrations/google/connect sends the browser to Google's
consent screen (calendar.events and calendar.calendarlist.readonly,
access_type=offline, prompt=consent, PKCE S256) with a state carried in a
signed __Host-mealquest_google flow cookie bound to the session.
GET /api/v1/integrations/google/callback checks the state and session,
exchanges the code, refuses a response without a refresh token
(reconnect_required) or without both scopes, seals the refresh token with
secretbox into integrations_google and sets connected_at.
DELETE /api/v1/integrations/google revokes the token at Google and clears
it; a revoke failure keeps the connection so it can be retried.
- internal/gcal: the connect service; Google endpoints are overridable and
internal/gcal/gcaltest fakes the token and revoke endpoints.
- internal/auth: the login cookie becomes the exported FlowCookie both
flows use (same wire format); CheckRedirect and ExchangeError are
exported for reuse.
- internal/config: MEALQUEST_GOOGLE_CLIENT_ID and
MEALQUEST_GOOGLE_CLIENT_SECRET_FILE, required at boot.
- DeleteIntegrationsGoogle query (sqlc regenerated); no migration, the
integrations_google table already exists.
- deploy: compose mounts google_client_secret; .env.example and RUNBOOK
document the Google OAuth client, its redirect URI and rotation.
Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A disconnect read the stored refresh token, revoked it at Google, then
deleted the row only if it still held that token. Google revokes the whole
grant, so a reconnect whose token was issued before that revoke was kept
as connected with a dead token. Connect's exchange-and-store and
disconnect's read-revoke-delete now hold one mutex across the Google
request and the database write, and the delete is unconditional (the
conditional delete and ErrChanged are gone: the lock is the one guard).
The connect cookie was single-use only in that the browser was asked to
drop it; a saved copy verified again until it expired. The service now
keeps each started connect's state in memory until the cookie expires and
the callback consumes it, so a replayed cookie is refused as bad state.
The fake Google now issues a distinct refresh token per exchange, revokes
the whole grant like Google, and can hold a token or revoke request so
tests can interleave a reconnect and a disconnect.
Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb (MQ125-01, MQ125-02).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task
Maestro task
1e9d7850-3a09-4572-b8a1-cb18cd4774cb— "Google OAuth connect and disconnect" (Feature 16, Google Calendar Sync, task 1 of 5).Done-condition
GET /api/v1/integrations/google/connectredirects to Google's consent screen with scopescalendar.eventsandcalendar.calendarlist.readonly,access_type=offline,prompt=consentand a state bound to the session;GET /api/v1/integrations/google/callbackvalidates state, exchanges the code (client id fromMEALQUEST_GOOGLE_CLIENT_ID, secret fromMEALQUEST_GOOGLE_CLIENT_SECRET_FILE, redirect underMEALQUEST_PUBLIC_URL), stores the refresh token with secretbox and setsconnected_at;DELETE /api/v1/integrations/googlerevokes the token at Google and clears it. Tests against an httptest fake Google token/revoke endpoint cover connect, bad state, missing refresh token (error asking to reconnect) and disconnect; no response ever contains the token.Scope
Connect and disconnect only. The remaining four tasks (calendar list and selection, push to Calendar, token-failure handling, settings screen) are untouched, and the shopping-list trigger belongs to Feature 17 and was deliberately not stubbed.
Points for review
MEALQUEST_GOOGLE_CLIENT_ID,MEALQUEST_GOOGLE_CLIENT_SECRET_FILE), matching the OIDC variables and the fail-fast house rule rather than adding a second "Calendar not configured" path. Consequence, stated by the developer: production cannot start on a release containing this change until the owner creates the Google OAuth web client and sets the secret. The runbook section "Google Calendar OAuth client",deploy/compose.yaml's secret mount anddeploy/.env.examplewere updated in the same commit. Please judge whether boot-required is the right call and whether that deployment coupling is acceptable and adequately documented.statepatterns rather than a second mechanism; the callback is reachable in a browser redirect context and the auth requirement chosen for each of the three endpoints is stated in the developer's NOTES.internal/auth/cookie.go,internal/auth/auth.goand theNewRoutersignature were touched; allNewRoutercall sites in tests were updated.migrations/queries/settings.sqlwas extended and sqlc regenerated (checked in, no drift); the existingintegrations_googletable was used, so no new migration.Evidence (orchestrator-run gate at
59d0f57a47)gofmt -l .— empty;go vet ./...— cleango test -race -count=1 ./...— all packages ok (server 102.5s)CGO_ENABLED=0 go build ./...— okmake sqlc-check SQLC=$(go env GOPATH)/bin/sqlc— no driftcd web && npm run typecheck && npm test && npm run build— PASS, 45 files / 764 tests (no web change)The developer also reports mutation checks (removing the session-binding, refresh-token or revoke-status check each fails its own test; bypassing the exchange-error redaction fails the leak checks), and that the real binary exits 1 naming both variables when they are missing, and refuses
MEALQUEST_GOOGLE_CLIENT_SECRETset inline.Implementation commit:
59d0f57a47955601d2c21ae4504568650e8d352dIndependent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.