Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationPackagesNative Zendesk sidebar

Native Zendesk sidebar

API2 natively serves GET and POST /zendesk-app/sidebar (ENG-3225), the page the Zendesk marketplace app loads in the ticket sidebar. The page frames Beam and bridges Zendesk App Framework (ZAF) ticket context, clipboard, and Jira internal-note messages to it over postMessage. Rails ZendeskAppController#sidebar remains deployed only as the retained fallback.

Contract

The HTML is the Rails template rulebase-api/app/views/zendesk_app/sidebar.html.erb with @beam_base_url substituted from RULEBASE_BEAM_BASE_URL; a test asserts byte-for-byte parity. Responses are text/html; charset=utf-8, carry no X-Frame-Options (Rails deletes it so Zendesk can frame the page), and carry no Content-Security-Policy, matching Rails (no CSP initializer is configured). The only header deviation is the omitted Rack ETag.

In production every request must carry a token parameter (POST body wins over query string, as Rails params does). Missing tokens return 400 Missing JWT token. Verification mirrors Rails exactly:

  1. Decode the token without verification only to read iss.
  2. Accept only <subdomain>.zendesk.com where the subdomain is a valid lowercase DNS label. Anything else is a generic 401.
  3. Resolve the kept (discarded_at IS NULL) Zendesk organization_data_sources row whose user_data->>'subdomain' equals the issuer subdomain, lowest id first. Unknown tenants are the same generic 401.
  4. Load that tenant’s own credentials (API2 active Zendesk connection, else the Rails-encrypted data.token) and fetch https://<subdomain>.zendesk.com/api/v2/apps/<ZENDESK_MARKETPLACE_APP_ID>/public_key.pem with a 10 s timeout, no redirect following, and a 16 KiB body cap. Provider failures return 502. Keys are never cached, so nothing can leak across tenants or outlive a rotation.
  5. Verify the token with RS256 only (jose rejects none/HS256 confusion) and standard time claims. Failures are the same generic 401.

The tenant lookup is a single unlocked SELECT; no transaction, row lock, or advisory lock is open while the Zendesk HTTP request runs.

Outside production (NODE_ENV !== "production") the page is served without verification, exactly as Rails.env.production? gated it.

Telemetry

Every request emits one native_zendesk_sidebar log with method, status, outcome, durationMs, and (once a tenant is resolved) subdomain, railsOrganizationId, and railsOrganizationDataSourceId. Outcomes:

OutcomeStatusMeaning
served200Verified token, page served
served_unverified200Non-production only
missing_token400Rails Missing JWT token parity
malformed_token, missing_issuer, untrusted_issuer401Rejected before any database read
tenant_not_found401No kept Zendesk data source for the subdomain
invalid_signature, claims_rejected401RS256 verification failed
credentials_missing, credentials_error500Tenant exists but has no usable token
provider_*, invalid_public_key502Zendesk public-key fetch failed; providerStatus carries the upstream status
paused_unavailable, misconfigured_unavailable503Paused, or RULEBASE_BEAM_BASE_URL / ZENDESK_MARKETPLACE_APP_ID missing in production (the former *_fallback outcomes that forwarded to Rails were removed in ENG-3495)
flipper_error—Flipper lookup failed; request continued natively

Rollback

pause_api2_zendesk_sidebar is a regional Flipper kill switch (safe state off, created 2026-09-17, remove by 2026-12-16). When enabled, requests return 503 (paused_unavailable); the route no longer forwards to Rails, whose production services are held at zero replicas. A missing RULEBASE_BEAM_BASE_URL or ZENDESK_MARKETPLACE_APP_ID in production returns 503 with misconfigured_unavailable. A code rollback is a revert of the ENG-3495 PR and would only restore the attempt to forward.

Edge frameability incident (2026-09-20, ENG-3290)

At 01:36–01:40Z Zendesk reported the sidebar no longer loading; the agent console showed api.rulebase.co refused to frame because of X-Frame-Options: sameorigin. Route, auth and Beam configuration were healthy: unauthenticated GET returned 400, a garbage token 401, and valid POSTs logged outcome=served status=200 for rho7005 and d3v-rulebase. A direct origin request carried no X-Frame-Options, while the same request through https://api.rulebase.co did. Root cause: the public API2 CloudFront distribution E1T87GN0P4WMPJ (aliases api.rulebase.co, api2.rulebase.co) has no path behaviors and its default response-headers policy is the AWS managed Managed-SecurityHeadersPolicy (67f7725c-6f97-4210-82d7-5512b31e9d03), which adds FrameOptions SAMEORIGIN to every response, including the one this route deliberately omits it from. The earlier hypothesis that Infisical supplied RULEBASE_BEAM_BASE_URL=http://localhost:5173 was wrong: prod and production-eu both hold https://beam.rulebase.co (the localhost value came from the CLI default dev environment), and eu.beam.rulebase.co does not resolve. Do not pin it.

Fix, in two layers:

  1. Edge (primary): a dedicated /zendesk-app/sidebar* ordered cache behavior that copies the default behavior’s origin, cache policy, origin-request policy and compression, allows all methods (POST required), and attaches a custom response-headers policy equal to the managed one minus FrameOptions. The default behavior is unchanged, so frame protection is not removed globally. Flightcontrol cannot express this, so it is an operator-run bounded change kept in infrastructure/cloudfront/zendesk-sidebar/ (apply.sh idempotent re-apply, check.sh HTTP drift check, config-check.sh AWS-side drift check). Landed 2026-09-20T01:50Z by explicit human authorization via the AWS CLI (not automation): custom policy Rulebase-ZendeskSidebar-Embeddable (76343034-8912-4a3f-bc02-588f973429b5, managed headers minus FrameOptions) attached to the new behavior on US E1T87GN0P4WMPJ and EU EX09ZU4R31WRP; both Deployed. Post-fix probes: US/EU query-string GET → 400 with no X-Frame-Options and all other security headers; /health still SAMEORIGIN in both regions; Infisical unchanged. check.sh run from the repo against both public hosts passes. Customer-path acceptance: after the CloudFront deployment Chidi reloaded the Zendesk sidebar in a real browser and confirmed it works. Drift guard: the scheduled zendesk-sidebar-edge-check GitHub workflow runs check.sh every 6 hours and on dispatch; a failure means the behavior was reconciled away and apply.sh must be re-run (human-gated).
  2. Origin (defence in depth): the served page now sends Content-Security-Policy: frame-ancestors https://*.zendesk.com. Browsers ignore X-Frame-Options when frame-ancestors is present, so the sidebar stays frameable by Zendesk (and only Zendesk) even if the edge exception is lost. Other response headers are unchanged.

Remaining verification (bounded, read-only): native_zendesk_sidebar shows outcome=served status=200 with no misconfigured_fallback; no legacy_rails_gateway event for /zendesk-app/sidebar; and infrastructure/cloudfront/zendesk-sidebar/check.sh passes (no X-Frame-Options on GET/POST sidebar responses including the 200 HTML when ZENDESK_SIDEBAR_TOKEN is supplied, frame-ancestors present, security headers intact, /health still SAMEORIGIN). Rollback: remove the ordered behavior and the custom policy; the CSP header is a code revert.

Production rollout plan

  1. Set RULEBASE_BEAM_BASE_URL on the API2 service in each region to the Rails beam_base_url for that region (US https://beam.rulebase.co). This is a config change and needs its own approval; until it lands, production requests emit misconfigured_fallback and Rails keeps serving.
  2. Deploy with pause_api2_zendesk_sidebar off in both regions.
  3. Open a Zendesk ticket in a customer account per region and confirm the sidebar renders Beam and ticket context loads. curl -s -o /dev/null -w '%{http_code}' https://<api2>/zendesk-app/sidebar with no token should return 400; -X POST -d token=garbage should return 401.
  4. Reconcile against the Rails baseline (US rolling 24 h ending 2026-09-17T20:10Z: 677 200 and 2 400; EU: 2 400, 0 200). served per region should track the Rails 200 count, missing_token the 400 count, and tenant_not_found/invalid_signature should stay near zero. Any provider_*, credentials_*, or misconfigured_* outcome above a handful per day is a regression to investigate before the window counts.
  5. Confirm legacy_rails_gateway events for /zendesk-app/sidebar drop to zero in both regions across a complete rolling 24-hour and seven-day window.
  6. Roll back in an affected region by enabling pause_api2_zendesk_sidebar; a code rollback reverts the PR.

Local checks

cd rulebase-web/rulebase-api2 pnpm vitest run src/routes/zendesk-app-sidebar src/lib/zendesk-sidebar src/app.route-ownership.test.ts pnpm typecheck
Last updated on