Skip to content

fix(integrations): no-store the callback hand-off and trace chat callbacks as server spans - #1673

Merged
Makisuo merged 6 commits into
mainfrom
fix/oauth-state-browser-binding
Oct 22, 2026
Merged

Makisuo merged 6 commits into
mainfrom
fix/oauth-state-browser-binding

Conversation

@Makisuo

@Makisuo Makisuo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #1675, which landed the initiator binding for OAuth, app-install and chat-identity callbacks. This branch first carried its own version of that fix; after #1675 merged it was reduced to the two pieces #1675 does not have.

  • Callback hand-off headers. The 302 from a provider or chat callback into the dashboard carries the provider's code in its fragment. It is now sent with cache-control: no-store and referrer-policy: no-referrer (forwardToDashboard), so no cache keeps that redirect and the landing request sends no Referer.
  • Chat callback spans. ApiObservabilityLive suppresses the auto server span for /oauth/chat/*/callback, so the handler span is the trace root. It is now server-kind with http.route, http.request.method and http.response.status_code, like IntegrationsCallbackRouter.oauthCallback.

Tests: integrations-callback.http.test.ts asserts both headers on a provider redirect and a chat redirect (6/6 pass). The api typecheck and lint are clean.

🤖 Generated with Claude Code

…rted them

Every connect, app-install and chat account-link callback is public, and it
used to redeem its state row and store the provider's grant under whatever org
and user the row named. The authorize URL is transferable, so a tenant admin
could send theirs to somebody else and receive that person's GitHub install,
Cloudflare/PlanetScale/Supabase/Hazel tokens or chat workspace, and a member
could attach somebody else's chat account to their own Maple user.

The public callbacks now complete nothing. They redirect to the dashboard with
the provider's query in the URL fragment, and the signed-in integrations page
finishes the flow through `oauthComplete` / `chatComplete`. Every service
redeems a state row only for the session's user and org (`OAuthStateInitiator`),
burning it on a mismatch, so a forwarded URL finished by another account fails
without storing anything. Random state, TTL, provider scoping, PKCE and
external-resource uniqueness are unchanged.
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 6/10 · needs attention
The initiator checks are sound and pinned per service, but the new completer that wires the session to them has no test.
2 warnings −2 · 1 note · tests partial −1 · risk high −1 · 2/2 new units observable

Public provider and chat callbacks now only 302 their query into the dashboard fragment; the signed-in page finishes the flow through new session-only endpoints whose services refuse a state row started by another org or user. The design holds up — the gap is test coverage on the new completer.

  • All seven provider callbacks return a 302 to /integrations with the provider query in the fragment
  • requireStateRow and the three complete* services take an OAuthStateInitiator and burn the row on mismatch
  • New session-only POST /internal/integrations/{oauth,chat}/complete performs the completion
  • The integrations page reads the fragment, clears it, and relays the popup result same-origin

Before merge

  • Manual · Run a popup round-trip on a preview:web preview against one real provider (Hazel or GitHub)
  • Manual · Deploy web and api together: an API-first window leaves popups on /integrations without completing

Findings

🟠 Warning · F1 · it.live.each spreads the row, so finisher is a character of the case label

tests · packages/backend/src/services/auth/oauth/connection-helpers.test.ts:449

it.live.each passes each case's elements as separate arguments (the convention the rest of the suite uses, e.g. apps/web/src/lib/agent-sessions/session-axis.test.ts:74), so the callback's first parameter is the label string and ([, finisher]) destructures that string — finisher becomes "n", not the { orgId, userId } object the rows name. requireStateRow then refuses because a string has no orgId, so both rows exercise the same path and the "same user in another org" case never tests the org comparison at all.

Take both parameters explicitly — `(_label, finisher) => …` — and type the rows so `finisher` is an `OAuthStateInitiator` rather than the loose union `each` infers.
🟠 Warning · F2 · Nothing exercises makeIntegrationCompleter

tests · apps/api/src/routes/internal/integrations-complete.ts:65

The new completer holds the logic this PR moves out of the deleted integrations-callback-page.test.ts — the failure-to-copy mapping, the RFC 9207 iss refusal, and the single-org PlanetScale finalize — and no test file references it. A typo in the iss comparison or a swapped provider branch would ship silently.

Add an endpoint-level test over `oauthComplete`/`chatComplete` with a session for another org and another user, plus one case per mapped failure reason and one for a mismatched `iss`.
🔵 Note · F3 · The issuer-mismatch refusal returns 200 on an Unset span with no log

observability · STAT-01 · apps/api/src/routes/internal/integrations-complete.ts:137

An RFC 9207 mix-up attempt (and the missing-code/state branches at 189–209) now ends a POST /internal/integrations/oauth/complete as HTTP 200 with status: "error" and nothing recorded, where the removed callback answered 400 and carried http.response.status_code; only the complete() failures reach Effect.logError at 224. A refused completion leaves no trace to investigate.

Log the refusal at warn with the provider and a fixed reason (never the query), and annotate the span with a `maple.integration.outcome` value before returning the error response.
🤖 Prompt to fix all 3 findings with an AI agent
Findings from an automated review of commit 402f8e542379f79af1aa7951050d9362f75f8189. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · tests · packages/backend/src/services/auth/oauth/connection-helpers.test.ts:449
`it.live.each` spreads the row, so `finisher` is a character of the case label
`it.live.each` passes each case's elements as separate arguments (the convention the rest of the suite uses, e.g. `apps/web/src/lib/agent-sessions/session-axis.test.ts:74`), so the callback's first parameter is the label string and `([, finisher])` destructures that string — `finisher` becomes `"n"`, not the `{ orgId, userId }` object the rows name. `requireStateRow` then refuses because a string has no `orgId`, so both rows exercise the same path and the "same user in another org" case never tests the org comparison at all.
Suggested fix: Take both parameters explicitly — `(_label, finisher) => …` — and type the rows so `finisher` is an `OAuthStateInitiator` rather than the loose union `each` infers.

---

F2 · Warning · tests · apps/api/src/routes/internal/integrations-complete.ts:65
Nothing exercises `makeIntegrationCompleter`
The new completer holds the logic this PR moves out of the deleted `integrations-callback-page.test.ts` — the failure-to-copy mapping, the RFC 9207 `iss` refusal, and the single-org PlanetScale finalize — and no test file references it. A typo in the `iss` comparison or a swapped provider branch would ship silently.
Suggested fix: Add an endpoint-level test over `oauthComplete`/`chatComplete` with a session for another org and another user, plus one case per mapped failure reason and one for a mismatched `iss`.

---

F3 · Note · observability · STAT-01 · apps/api/src/routes/internal/integrations-complete.ts:137
The issuer-mismatch refusal returns 200 on an Unset span with no log
An RFC 9207 mix-up attempt (and the missing-`code`/`state` branches at 189–209) now ends a `POST /internal/integrations/oauth/complete` as HTTP 200 with `status: "error"` and nothing recorded, where the removed callback answered 400 and carried `http.response.status_code`; only the `complete()` failures reach `Effect.logError` at 224. A refused completion leaves no trace to investigate.
Suggested fix: Log the refusal at warn with the provider and a fixed reason (never the query), and annotate the span with a `maple.integration.outcome` value before returning the error response.

Production impact

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/api/src/routes/chat-integration.http.ts 936 GET 795/day, 2.3% err
GET 141/day, 0.1% err
apps/api/src/routes/integrations-callback.http.ts 936 GET 795/day, 2.3% err
GET 141/day, 0.1% err
Telemetry this change adds and removes (5)
  • ➖ attribute http.response.status_code (~2.6M/day today) · apps/api/src/routes/integrations-callback.http.ts:78
  • ➕ span name IntegrationCompleter.oauth · apps/api/src/routes/internal/integrations-complete.ts:176
  • ➕ span name IntegrationCompleter.chat · apps/api/src/routes/internal/integrations-complete.ts:232
  • ➕ attribute maple.chat.callback · apps/api/src/routes/internal/integrations-complete.ts:238
  • ➕ attribute vcs.connect.reason · packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts:324

Removed here but still emitted elsewhere at this commit:

  • http.response.status_code · apps/api/src/routes/webhooks/clerk.http.ts:67
What was checked
  • The state row is consumed before the initiator check, so a forwarded link cannot be replayed (ChatWorkspaceService.ts:391, GithubConnectService.ts:298)
  • Every state row is written with the session's initiatedByUserId, so no flow creates a row its initiator cannot redeem
  • The popup's postMessage targets the app origin and isCallbackOrigin accepts it (subscription.ts:234)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
Public /api/integrations/*/callback and /oauth/chat/:connector[/identity]/callback (redirect only) HTTP entrypoint yes Server span with http.route and maple.integration.provider (apps/api/src/routes/integrations-callback.http.ts:67); chat callbacks annotate maple.chat.connector (chat-integration.http.ts:47)
POST /internal/integrations/oauth/complete and /internal/integrations/chat/complete HTTP entrypoint yes HttpApi group endpoint, span annotated with maple.integration.provider / maple.chat.connector (apps/api/src/routes/internal/integrations-complete.ts:181,238)

402f8e5 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one. Check ids refer to Maple's instrumentation audit.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 80e446b3-17a4-45a0-88a9-07116e3db2f1
📥 Commits

Reviewing files that changed from the base of the PR and between 0d7955b and 402f8e5.

📒 Files selected for processing: 34
  • .oxlintrc.json
  • apps/api/src/routes/chat-integration.http.ts
  • apps/api/src/routes/integrations-callback-page.test.ts
  • apps/api/src/routes/integrations-callback.http.test.ts
  • apps/api/src/routes/integrations-callback.http.ts
  • apps/api/src/routes/internal/integrations-complete.ts
  • apps/api/src/routes/internal/integrations.http.ts
  • apps/web/src/page/integrations/forwarded-callback.scene.test.ts
  • apps/web/src/page/integrations/forwarded-callback.ts
  • apps/web/src/page/integrations/message.ts
  • apps/web/src/page/integrations/update.ts
  • apps/web/src/route/search.ts
  • packages/backend/src/services/auth/CloudflareOAuthService.test.ts
  • packages/backend/src/services/auth/CloudflareOAuthService.ts
  • packages/backend/src/services/auth/HazelOAuthService.ts
  • packages/backend/src/services/auth/PlanetScaleOAuthService.test.ts
  • packages/backend/src/services/auth/PlanetScaleOAuthService.ts
  • packages/backend/src/services/auth/SupabaseOAuthService.test.ts
  • packages/backend/src/services/auth/SupabaseOAuthService.ts
  • packages/backend/src/services/auth/oauth/connection-helpers.test.ts
  • packages/backend/src/services/auth/oauth/connection-helpers.ts
  • packages/backend/src/services/auth/oauth/state-initiator.ts
  • packages/backend/src/services/integrations/ChatWorkspaceService.test.ts
  • packages/backend/src/services/integrations/ChatWorkspaceService.ts
  • packages/backend/src/services/integrations/PlanetScaleConnectionService.test.ts
  • packages/backend/src/services/integrations/PlanetScaleDiscoveryService.test.ts
  • packages/backend/src/services/integrations/PlanetScaleService.test.ts
  • packages/backend/src/services/integrations/SupabaseConnectionService.test.ts
  • packages/backend/src/services/integrations/SupabaseInsightsService.test.ts
  • packages/backend/src/services/integrations/SupabaseLogsService.test.ts
  • packages/backend/src/services/integrations/supabase/scrape-targets.test.ts
  • packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts
  • packages/backend/src/services/integrations/vcs/vendor/github/__tests__/GithubConnectService.test.ts
  • packages/domain/src/http/integrations.ts
💤 Files with no reviewable changes: 1
  • apps/api/src/routes/integrations-callback-page.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

📝 Walkthrough

OAuth and chat callbacks now forward to the dashboard, where signed-in users complete the flows through internal API endpoints. OAuth and chat services validate that the completing user and organization match the callback state initiator.

Changes: Integration callback completion

Layer / File(s) Summary
Initiator-bound state validation
packages/backend/src/services/auth/oauth/*
OAuth state redemption now checks both the initiating user and organization. A mismatch consumes the state and returns a validation error. Tests cover mismatches and state consumption.
Provider completion checks
packages/backend/src/services/auth/*OAuthService.ts, packages/backend/src/services/integrations/ChatWorkspaceService.ts, packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts, related service tests
OAuth and chat completion methods now require the initiator identity and pass it into state validation. Tests update completion calls and cover mismatched initiators.
Authenticated completion endpoints
packages/domain/src/http/integrations.ts, apps/api/src/routes/internal/integrations-complete.ts, apps/api/src/routes/internal/integrations.http.ts
Adds OAuth and chat completion schemas and internal endpoints. The route handlers pass the current tenant’s organization and user IDs to the completer. The completer validates callback inputs, dispatches provider-specific work, and returns callback results.
Provider callback forwarding
apps/api/src/routes/integrations-callback.http.ts, apps/api/src/routes/chat-integration.http.ts, apps/api/src/routes/integrations-callback.http.test.ts, .oxlintrc.json, apps/api/src/routes/integrations-callback-page.test.ts
OAuth and chat callback routes redirect to the dashboard with callback details and query data in the URL fragment. Redirect responses include no-store and no-referrer headers. Tests check the redirects; the provider-import exclusion changes to the new completion route, and callback-page tests are removed.
Dashboard callback processing
apps/web/src/page/integrations/forwarded-callback.ts, apps/web/src/page/integrations/message.ts, apps/web/src/page/integrations/update.ts, apps/web/src/route/search.ts, apps/web/src/page/integrations/forwarded-callback.scene.test.ts
The integrations page parses forwarded callbacks, sends completion requests, handles OAuth relays and chat outcomes, removes callback data, and navigates to the resulting integration URL. Tests cover callback selection and outcome handling.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Provider as OAuth or chat provider
  participant CallbackRoute as API callback route
  participant Dashboard as Dashboard integrations page
  participant IntegrationRoute as Internal integrations route
  participant Completer as Integration completer
  participant ProviderService as OAuth or chat service
  Provider->>CallbackRoute: Send callback query
  CallbackRoute->>Dashboard: Redirect with callback details and query fragment
  Dashboard->>IntegrationRoute: Submit completion payload
  IntegrationRoute->>Completer: Pass payload and signed-in tenant identity
  Completer->>ProviderService: Complete provider or chat connection
Loading
Merge Risk: ⚪ Minimal · up to 402f8

OAuth and chat installs now complete only as the signed-in user and organization that started them, which tightens the integration flow. No concrete merge-blocking risk remains. If the API deploys before the web app, users will need to retry the flow once.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title accurately describes the added no-store callback redirects and server-span tracing, both present in the changeset. It does not mention the primary change: completing OAuth and chat callbacks…
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 32 files. (1 skipped: 1 unsupported.)

Full details: Title check

Explanation

The title accurately describes the added no-store callback redirects and server-span tracing, both present in the changeset. It does not mention the primary change: completing OAuth and chat callbacks only for the initiating user and organization.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 inline notes from Maple's review. The score and summary are in the review comment above.

Comment thread packages/backend/src/services/auth/oauth/connection-helpers.test.ts Outdated
Comment thread apps/api/src/routes/internal/integrations-complete.ts Outdated
…ions

The completer that wires the dashboard session into each provider's completion
had no test: cover the initiator hand-off per provider, the failure-to-copy and
chat reason mappings, the pre-exchange refusals (issuer mismatch, missing
code/state, provider error, pending GitHub approval) and the PlanetScale
single-org finalize.

Refusals before any exchange now log a warning with a fixed reason and stamp
maple.integration.outcome, as do completions and failures, so a refused
completion leaves a trace. The initiator-binding test asserts its finisher
differs from the initiator in exactly one field.
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
The added completer test pins initiator pass-through and refusal copy; remaining risk is auth scoping, whose core comparison lives in already-reviewed code.
no findings · tests covered · risk high −1 · 1/1 new units observable

Adds a test suite for makeIntegrationCompleter and logs refused completions; the state rows in connection-helpers.test.ts are now typed as [label, initiator] tuples read as one argument. Nothing new to fix.

  • integrations-complete.test.ts asserts each provider's completion receives the session initiator
  • refuse logs every pre-exchange refusal and annotates maple.integration.outcome
  • connection-helpers.test.ts rows are typed [label, OAuthStateInitiator] and read as one argument

Before merge

  • Manual · Run one popup round-trip against a real provider on a preview before merging, since no browser end-to-end check was done

Fixed since the last review

  • ✅ F1 · it.live.each spreads the row, so finisher is a character of the case label
  • ✅ F2 · Nothing exercises makeIntegrationCompleter
  • ✅ F3 · The issuer-mismatch refusal returns 200 on an Unset span with no log

Production impact

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/api/src/routes/chat-integration.http.ts 937 GET 796/day, 2.3% err
GET 141/day, 0.1% err
apps/api/src/routes/integrations-callback.http.ts 937 GET 796/day, 2.3% err
GET 141/day, 0.1% err
Telemetry this change adds and removes (6)
  • ➖ attribute http.response.status_code (~2.6M/day today) · apps/api/src/routes/integrations-callback.http.ts:78
  • ➕ span name IntegrationCompleter.oauth · apps/api/src/routes/internal/integrations-complete.ts:164
  • ➕ attribute maple.integration.outcome · apps/api/src/routes/internal/integrations-complete.ts:177
  • ➕ span name IntegrationCompleter.chat · apps/api/src/routes/internal/integrations-complete.ts:241
  • ➕ attribute maple.chat.callback · apps/api/src/routes/internal/integrations-complete.ts:247
  • ➕ attribute vcs.connect.reason · packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts:324

Removed here but still emitted elsewhere at this commit:

  • http.response.status_code · apps/api/src/routes/webhooks/vcs.http.ts:70
What was checked
  • Pre-exchange refusals (provider error, missing code/state, GitHub setup_action, Supabase iss) call no service: assert.lengthOf(calls, 0) at integrations-complete.test.ts:236
  • IntegrationReturnPath drops the GitHub stub's absolute returnTo (integrations-complete.test.ts:127, assertion at :182)
  • it.effect.each rows are destructured as a single argument, pinned by the green typecheck rather than by inspection
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
IntegrationCompleter.oauth / .chat request handler yes Effect.fn spans in apps/api/src/routes/internal/integrations-complete.ts:164,241 with maple.integration.outcome and provider/connector attributes

0677be1 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

The callback spans are the only trace root for these routes; the forwarding
rewrite dropped http.response.status_code from them. Every branch is a 302.
@Makisuo Makisuo added the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Oct 22, 2026
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Moves OAuth completion behind a session check but is thoroughly tested; the one gap is the chat callbacks' missing Server span.
1 note · tests covered · risk high −1 · 1/2 new units observable −1

Public OAuth and chat callbacks now only 302 the provider's query into the dashboard fragment; the signed-in page finishes the flow through new session-only endpoints that redeem a state row only for its initiator. The two files changed since the last review just stamp the redirect status; safe to merge.

Before merge

  • Manual · Deploy web with the API: a web build older than this API never finishes a forwarded callback, so popups land on the integrations page and store nothing.

Findings

🔵 Note · F4 · Chat callbacks get no Server span, unlike the integration callbacks

observability · SPAN-01 · apps/api/src/routes/chat-integration.http.ts:40-42

ApiObservabilityLive suppresses the auto server span for /oauth/chat/*/callback (src/http/api-observability.ts:38), and this span is built without kind: "server" or http.route, so a chat install or account link is an API entry point with no server span: its latency never counts as server work and the 302 stamped below lands on an internal span. The sibling handler added in this PR sets both (integrations-callback.http.ts:67); pass the route path into callback and do the same.

Make `callback` take the route path and create the span as `Effect.fn(span, { kind: "server", attributes: { "http.route": path, "http.request.method": "GET" } })`, matching `IntegrationsCallbackRouter.oauthCallback`.
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit 7188dc23ead9286061a19fa96eab7dd444fa60d3. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F4 · Note · observability · SPAN-01 · apps/api/src/routes/chat-integration.http.ts:40-42
Chat callbacks get no Server span, unlike the integration callbacks
`ApiObservabilityLive` suppresses the auto server span for `/oauth/chat/*/callback` (`src/http/api-observability.ts:38`), and this span is built without `kind: "server"` or `http.route`, so a chat install or account link is an API entry point with no server span: its latency never counts as server work and the 302 stamped below lands on an internal span. The sibling handler added in this PR sets both (`integrations-callback.http.ts:67`); pass the route path into `callback` and do the same.
Suggested fix: Make `callback` take the route path and create the span as `Effect.fn(span, { kind: "server", attributes: { "http.route": path, "http.request.method": "GET" } })`, matching `IntegrationsCallbackRouter.oauthCallback`.

Production impact

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/api/src/routes/chat-integration.http.ts 937 GET 796/day, 2.3% err
GET 141/day, 0.1% err
apps/api/src/routes/integrations-callback.http.ts 937 GET 796/day, 2.3% err
GET 141/day, 0.1% err
Telemetry this change adds and removes (5)
  • ➕ span name IntegrationCompleter.oauth · apps/api/src/routes/internal/integrations-complete.ts:164
  • ➕ attribute maple.integration.outcome · apps/api/src/routes/internal/integrations-complete.ts:177
  • ➕ span name IntegrationCompleter.chat · apps/api/src/routes/internal/integrations-complete.ts:241
  • ➕ attribute maple.chat.callback · apps/api/src/routes/internal/integrations-complete.ts:247
  • ➕ attribute vcs.connect.reason · packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts:324
What was checked
  • Callback query goes into the fragment raw and the web reads window.location.hash (forwarded-callback.ts:37); integrations-callback.http.test.ts:45 pins the exact Location.
  • Suppression regex still matches both rewritten chat paths (api-observability.ts:18), so the code/state stays out of url.query.
  • Unknown chat connector forwards no query and keeps the chat/chat_reason codes the page already renders (http.test.ts:67).
Observability coverage: 1 of 2 changes observable
Change Kind Observable Evidence
Provider OAuth callbacks (hazel, github, cloudflare, planetscale, supabase) forwarding to the dashboard inbound HTTP yes Effect.fn("IntegrationsCallbackRouter.oauthCallback", { kind: "server", attributes: { http.route, http.request.method, maple.integration.provider } }) at integrations-callback.http.ts:67-73
Chat connector callbacks (/oauth/chat/:connector/callback and /identity/callback) inbound HTTP no Effect.fn(span) at chat-integration.http.ts:40 carries no kind/http.route while the auto server span is suppressed at api-observability.ts:38

7188dc2 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one. Check ids refer to Maple's instrumentation audit.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🍁 Maple PR preview

Note

Preview resources were removed when this pull request closed.

Final commit 37bcf04 · View workflow run

The auto server span is suppressed for these paths, so the handler span is
the trace root; make it server-kind with http.route like the provider callbacks.
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
The only new code is a redirect-only callback plus its Server span, pinned by integrations-callback.http.test.ts; the rest was reviewed earlier.
no findings · tests covered · risk high −1 · 2/2 new units observable

The chat callbacks now only 302 into the dashboard with the provider query in the fragment, and each carries its own Server span. Safe to merge; the OAuth flow change is the one thing to exercise end to end.

  • chat-integration.http.ts drops ChatWorkspaceService and forwards both chat callbacks via forwardToDashboard
  • callback wraps each chat route in a Server span named ChatOAuth.callback / ChatOAuth.identityCallback

Fixed since the last review

  • ✅ F4 · Chat callbacks get no Server span, unlike the integration callbacks

Production impact

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/api/src/routes/chat-integration.http.ts 896 GET 758/day, 1.9% err
GET 138/day, 0.0% err
apps/api/src/routes/integrations-callback.http.ts 896 GET 758/day, 1.9% err
GET 138/day, 0.0% err
Telemetry this change adds and removes (5)
  • ➕ span name IntegrationCompleter.oauth · apps/api/src/routes/internal/integrations-complete.ts:164
  • ➕ attribute maple.integration.outcome · apps/api/src/routes/internal/integrations-complete.ts:177
  • ➕ span name IntegrationCompleter.chat · apps/api/src/routes/internal/integrations-complete.ts:241
  • ➕ attribute maple.chat.callback · apps/api/src/routes/internal/integrations-complete.ts:247
  • ➕ attribute vcs.connect.reason · packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts:324
What was checked
  • ApiObservabilityLive still suppresses the auto span for /oauth/chat/<c>/callback and /identity/callback, so the new span is the trace root and no query string is recorded (api-observability.ts:1…
  • The unknown-connector branch keeps the old chat=error&chat_reason=unknown shape the web reads (search.ts:248)
  • Fragment kinds chat-install / chat-identity match forwardedCallbackOf (forwarded-callback.ts:24)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
GET /oauth/chat/:connector/callback entrypoint yes chat-integration.http.ts:42 Effect.fn kind server with http.route
GET /oauth/chat/:connector/identity/callback entrypoint yes chat-integration.http.ts:42 Effect.fn kind server with http.route

b534037 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

…er-binding

# Conflicts:
#	.oxlintrc.json
#	apps/api/src/routes/chat-integration.http.ts
#	apps/api/src/routes/integrations-callback.http.test.ts
#	apps/api/src/routes/integrations-callback.http.ts
#	apps/api/src/routes/internal/integrations.http.ts
#	apps/web/src/page/integrations/update.ts
#	packages/backend/src/services/auth/CloudflareOAuthService.test.ts
#	packages/backend/src/services/auth/CloudflareOAuthService.ts
#	packages/backend/src/services/auth/HazelOAuthService.ts
#	packages/backend/src/services/auth/PlanetScaleOAuthService.test.ts
#	packages/backend/src/services/auth/PlanetScaleOAuthService.ts
#	packages/backend/src/services/auth/SupabaseOAuthService.test.ts
#	packages/backend/src/services/auth/SupabaseOAuthService.ts
#	packages/backend/src/services/auth/oauth/connection-helpers.test.ts
#	packages/backend/src/services/auth/oauth/connection-helpers.ts
#	packages/backend/src/services/integrations/ChatWorkspaceService.test.ts
#	packages/backend/src/services/integrations/ChatWorkspaceService.ts
#	packages/backend/src/services/integrations/PlanetScaleConnectionService.test.ts
#	packages/backend/src/services/integrations/PlanetScaleDiscoveryService.test.ts
#	packages/backend/src/services/integrations/PlanetScaleService.test.ts
#	packages/backend/src/services/integrations/SupabaseConnectionService.test.ts
#	packages/backend/src/services/integrations/SupabaseInsightsService.test.ts
#	packages/backend/src/services/integrations/SupabaseLogsService.test.ts
#	packages/backend/src/services/integrations/supabase/scrape-targets.test.ts
#	packages/backend/src/services/integrations/vcs/vendor/github/GithubConnectService.ts
#	packages/backend/src/services/integrations/vcs/vendor/github/__tests__/GithubConnectService.test.ts
…callbacks as server spans

The redirect into the dashboard carries the provider's code in its fragment, so it
is sent with no-store and no-referrer. The chat callbacks' auto server span is
suppressed, so their handler span is now server-kind with http.route, like the
provider callbacks.
@Makisuo
Makisuo deployed to pr-preview October 22, 2026 23:02 — with GitHub Actions Active
@Makisuo Makisuo changed the title Finish OAuth and chat installs as the user who started them fix(integrations): no-store the callback hand-off and trace chat callbacks as server spans Oct 22, 2026
@maple-review-bot

maple-review-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
The no-store/no-referrer hand-off is pinned by both router tests; only the new server-span kind and status attribute have no assertion.
no findings · tests partial −1 · risk medium · 1/1 new units observable

Routes every callback redirect through a shared forwardToDashboard that forbids caching and referrers, and gives the chat callbacks a server-kind span carrying route and 302 status. The change is small and safe.

  • forwardToDashboard sets cache-control: no-store and referrer-policy: no-referrer on callback redirects
  • ChatCallbackRouter emits a server-kind span with http.route and http.response.status_code: 302

Production impact

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/api/src/routes/chat-integration.http.ts 895 GET 756/day, 1.9% err
GET 139/day, 0.0% err
What was checked
  • Suppression regex in api-observability.ts:18 matches both chat paths, so the new server span stays the trace root
  • Chat callback span status is 302 on every branch: toDashboard is the handler's only return (chat-integration.http.ts:49-64)
  • No import cycle: integrations-callback.http.ts does not import the chat router
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
chat connector callbacks (/oauth/chat/:connector/callback, .../identity/callback) inbound HTTP entrypoint yes ChatOAuth.callback / ChatOAuth.identityCallback server spans with http.route and status at apps/api/src/routes/chat-integration.http.ts:38-43

37bcf04 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo merged commit b196da1 into main Oct 22, 2026
27 checks passed
@Makisuo
Makisuo deleted the fix/oauth-state-browser-binding branch October 22, 2026 23:10
@Makisuo
Makisuo deployed to pr-preview October 22, 2026 23:10 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
pr-preview — 37bcf047 Deployed Oct 22, 2026 by Makisuo via deploy-pr-preview #6416
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant