Skip to content

feat(sidecar): Gmail Pub/Sub push transport layered over the poll path #589

Description

@schmug

⚠️ Blocked on PR #587 — everything this issue touches lands in that PR and is not on main yet.
Automated agents/routines: do not implement this issue while PR #587 is open. Before starting any work, run gh pr view 587 -R schmug/PhishSOC --json state -q .state and proceed only if it prints MERGED; otherwise stop silently (no comment, no branch). Remove the blocked/impl-blocked labels only after #587 merges.

Task

Add Gmail push notifications as a lower-latency transport for the Workspace sidecar: users.watch() + a Pub/Sub push subscription pointing at a new authenticated Worker route, layered ON TOP of the existing poll/backfill path (which stays as the gap-filler and becomes the fallback). Verdict latency drops from ≤1 min to seconds for tenants that opt in.

Motivation

As an operator with latency-sensitive mail flows, I want near-real-time verdicts; the issue #31 design named watch+Pub/Sub as the eventual transport and PR #587 deliberately shipped poll-first.

Pointers

  • workers/providers/workspace.tspollWorkspaceMailbox is the reuse seam: a push notification should just trigger the same poll step for that mailbox (the historyId cursor already dedupes/backfills)
  • workers/providers/gmail-client.ts — add watch()/stop() wrappers
  • workers/app.ts scheduled() — watch renewal job (7-day max expiry) joins the hourly branch
  • workers/routes/sidecar.ts — pattern for mounting; the push endpoint must validate Google's OIDC token on the Pub/Sub push (aud + issuer + email claim), NOT trust the payload
  • workers/durableObject/sidecar-state.tswatch_expiration column (new migration)
  • docs/sidecar-credentials.md — Pub/Sub topic + gmail-api-push@system.gserviceaccount.com publish-grant runbook

Constraints

  • Push is opt-in per mailbox (sidecar.transport: "poll" | "push", default poll); poll cadence relaxes but never disappears for push mailboxes (gap insurance)
  • The push endpoint is unauthenticated-by-CF-Access by necessity — it must verify the Pub/Sub OIDC JWT with a vetted library/crypto.subtle, never hand-rolled parsing, and must not process message content from the notification body (fetch via API using the cursor, as today)
  • Renewal failure surfaces in sidecar_health within one cycle

Acceptance criteria

  • Push-enabled mailbox: inbound mail scored within ~30s in a live test
  • Forged/unsigned push POSTs are rejected (401/403) with a test
  • watch renewal cron keeps subscriptions alive; expiry without renewal degrades to poll, not silence
  • Poll-only mailboxes are completely unaffected

Out of scope

M365/Graph subscriptions (file under the M365 provider follow-up); onboarding wizard changes beyond documenting the GCP setup.

Cross-references

#31 (parent), PR #587 (poll-first transport decision)

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedBlocked by a dependency or unresolved problemenhancementNew feature or requestimpl-blocked

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions