Skip to content

[DIAGNOSTIC] delegate fraud-stack sensors to unblock Dodo overlay 3DS (#4449)#4450

Merged
koala73 merged 2 commits into
mainfrom
fix/checkout-permissions-policy-overlay-4449
Jun 26, 2026
Merged

[DIAGNOSTIC] delegate fraud-stack sensors to unblock Dodo overlay 3DS (#4449)#4450
koala73 merged 2 commits into
mainfrom
fix/checkout-permissions-policy-overlay-4449

Conversation

@koala73

@koala73 koala73 commented Jun 26, 2026

Copy link
Copy Markdown
Owner

⚠️ DIAGNOSTIC / EXPERIMENT — verify on preview before merge

Tests whether the Dodo overlay checkout is salvageable from our side, before we commit to the bigger displayType:'redirect' change (#4449). Do not merge blindly — see the test plan.

Hypothesis

The overlay payment hangs at "Processing…" because its nested fraud/3DS stack — Hyperswitch → Airwallex → Sardine — can't read the device sensors our Permissions-Policy disables. Per the HAR (#4449), loader.min.js → initBiometricAndTracking is blocked on accelerometer / devicemotion / deviceorientation / bluetooth, and picture-in-picture is blocked in the checkout frame.

Change

Open the 5 fraud-stack sensor features (accelerometer, bluetooth, gyroscope, magnetometer, picture-in-picture) from () (blocked for the whole frame tree) to * on the dashboard Permissions-Policy only — embed/locked-down routes are untouched.

  • vercel.json + docker/nginx-security-headers.conf (kept in sync — deploy-config.test.mjs enforces parity).
  • deploy-config.test.mjs: pins the new delegation, drops the 4 sensors from the "disabled" set, retargets the picture-in-picture assertion. 81/81 green.

Why * and not a scoped origin list: the sensors are consumed by deeply nested grandchild frames (static.airwallex.com, *.sardine.ai) that a top-level origin allowlist can't reliably reach (Permissions-Policy delegates one frame level at a time, and subdomain wildcards aren't well-supported).

Honest caveat

This removes our top-level ceiling (level 1). It may still not be enough: the Dodo SDK's own iframe sets allow="payment keyboard-map *" (level 2, we don't control it) and Dodo's hosted page controls the Airwallex/Sardine delegation (level 3). The trailing * in the SDK's allow attribute is ambiguous — this experiment tells us whether level 1 was the (sole) blocker.

Test plan (decisive)

  1. Deploy this branch's Vercel preview.
  2. Do a real card purchase through the overlay on the preview.
  3. In parallel (free): open the hosted checkout link top-level — confirms whether the hosted page works at all.

Security note

accelerometer/gyroscope/magnetometer/bluetooth/picture-in-picture=* is a real loosening (any embedded iframe gains these). It is scoped to the dashboard route and is a diagnostic — if we keep it, it needs a security sign-off; if redirect mode wins, revert.

Part of #4449 / epic #4440.

…DS (#4449)

EXPERIMENT: the Dodo overlay-checkout payment hangs at "Processing…" because its
nested fraud/3DS stack (Hyperswitch → Airwallex → Sardine) can't read the device
sensors our Permissions-Policy disables — accelerometer/bluetooth/gyroscope/
magnetometer were `()` (= blocked for the entire frame tree) and picture-in-picture
wasn't granted to the checkout origins. Open them to `*` so the nested cross-origin
fraud frames (static.airwallex.com / *.sardine.ai) can run.

`*` rather than a scoped origin list because the sensors are needed by DEEPLY
NESTED grandchild frames that a top-level origin allowlist can't reliably reach.

- vercel.json + docker/nginx-security-headers.conf (kept in sync per the guardrail)
- deploy-config.test.mjs: pin the new delegation, drop the 4 sensors from the
  "disabled" set, retarget the picture-in-picture assertion

DIAGNOSTIC for #4449 — verify on a Vercel preview with a real card purchase before
merging. If the overlay still hangs, the bottleneck is below our control (the Dodo
SDK iframe `allow="payment keyboard-map *"` attribute, or processor-side) → adopt
redirect mode and REVERT these directives to `()`.

Refs #4449
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Jun 26, 2026 10:51am

Request Review

@koala73 koala73 added bug Something isn't working P0 Critical, blocks release or core functionality spike Time-boxed investigation, not an implementation task labels Jun 26, 2026
@koala73
koala73 marked this pull request as ready for review June 26, 2026 11:05
@koala73
koala73 merged commit c3b6e65 into main Jun 26, 2026
24 checks passed
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This diagnostic PR loosens the dashboard Permissions-Policy for five sensor/media features (accelerometer, bluetooth, gyroscope, magnetometer, picture-in-picture) from blocked (()) to unrestricted (*) to test whether the Dodo overlay 3DS fraud stack (Hyperswitch → Airwallex → Sardine) was hanging at "Processing…" due to blocked device sensors. Embed routes (/embed, /embed.html) are correctly isolated with a new nginx-embed-security-headers.conf file that keeps all five features blocked.

  • vercel.json and docker/nginx-security-headers.conf are updated in sync to open the five sensor directives to * on the dashboard catch-all route; the test suite enforces this parity.
  • New nginx-embed-security-headers.conf, Dockerfile COPY step, and matching location blocks in both nginx.conf and nginx.conf.template protect the embed entry point from inheriting the loosened dashboard policy.
  • tests/deploy-config.test.mjs drops the old disabled-set assertions for the four motion sensors, adds a dedicated fraud-stack delegation test, and adds an embed-route parity test.

Confidence Score: 3/5

Do not merge without confirming from the HAR whether the fraud library performs an actual Bluetooth requestDevice() call or only checks API presence — if the latter, bluetooth=* is broader than needed and should be removed before any permanent adoption.

The change deliberately opens five browser capabilities to * dashboard-wide. Four of them are well-motivated by the HAR evidence. The bluetooth=* entry is more ambiguous: unlike the passive motion sensors, the Web Bluetooth API allows embedded iframes to trigger a device-picker dialog on any dashboard page, and it is unclear whether the fraud library needs a real Bluetooth scan or only reads navigator.bluetooth !== undefined. The embed-route isolation work is clean and parity tests are thorough, but docker/nginx.conf inline embed headers are only prefix-matched, leaving a potential drift gap.

vercel.json and docker/nginx-security-headers.conf carry the sensor delegation that needs scrutiny before any non-diagnostic merge; tests/deploy-config.test.mjs has the embed parity gap worth tightening.

Security Review

  • bluetooth=* on the dashboard catch-all route (vercel.json, docker/nginx-security-headers.conf): the Web Bluetooth API allows initiating device scans and connections — this is qualitatively different from the passive motion sensors. Every frame already permitted by frame-src (YouTube, Cloudflare Turnstile, Stripe, Google Pay, Vercel Live, Dodo, etc.) now holds the policy-level grant to trigger the browser's Bluetooth device-picker on any dashboard page. If the fraud library only reads navigator.bluetooth !== undefined (capability detection) rather than calling requestDevice(), then bluetooth=* is broader than needed.
  • accelerometer=* / gyroscope=* / magnetometer=* dashboard-wide: these are passive reads with no user prompt; any iframe in frame-src silently gains access. The PR is explicitly a diagnostic and the author notes a security sign-off is required before a permanent merge.
  • Embed routes protected: /embed and /embed.html keep all five features at () in both the Vercel and Docker/nginx paths; parity is enforced by the updated test suite.

Important Files Changed

Filename Overview
vercel.json Loosens dashboard Permissions-Policy: accelerometer/bluetooth/gyroscope/magnetometer changed from () to *, picture-in-picture widened from a named-origin allowlist to *. The bluetooth=* change is qualitatively different from the motion sensors and warrants confirming whether the fraud library needs an actual Bluetooth scan or only capability presence.
docker/nginx-security-headers.conf Mirrors the vercel.json sensor policy loosening (accelerometer=*, bluetooth=*, gyroscope=*, magnetometer=*, picture-in-picture=*) for the Docker/nginx production path. Kept in sync with vercel.json; parity is enforced by the test.
docker/nginx-embed-security-headers.conf New file providing locked-down headers for the embed routes; all 5 newly loosened sensors remain blocked (=()) here. Content matches the vercel.json embed policy and the nginx.conf inline embed headers.
docker/nginx.conf Adds /embed and /embed.html location blocks with inline locked-down headers. Functionally correct, but the inline headers are not fully compared to nginx-embed-security-headers.conf in tests — only prefix-checked via regex.
docker/nginx.conf.template Adds embed location blocks that include embed_security_headers.conf and append Cache-Control. The structure is correct; add_header directives from the include and the block itself are both applied by nginx.
tests/deploy-config.test.mjs Drops the accelerometer/bluetooth/gyroscope/magnetometer disabled-set assertions; adds a fraud-stack delegation test pinning the new =* values; adds an embed-route parity test. The parity assertion covers nginx-embed-security-headers.conf vs vercel.json, but docker/nginx.conf's inline embed headers are only prefix-checked.
docker/Dockerfile Adds COPY docker/nginx-embed-security-headers.conf /etc/nginx/embed_security_headers.conf so the template-based production config can include it. Minimal and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B{Route}
    B -->|catch-all dashboard| C[nginx-security-headers.conf]
    B -->|"/embed or /embed.html"| D[nginx-embed-security-headers.conf]
    B -->|"/api/"| E[API proxy]
    C --> C1["accelerometer=* bluetooth=*\ngyroscope=* magnetometer=*\npicture-in-picture=*\nLOOSENED diagnostic"]
    C1 --> C2[All frame-src iframes gain sensor access]
    D --> D1["accelerometer=() bluetooth=()\ngyroscope=() magnetometer=()\npicture-in-picture=()\nLOCKED DOWN unchanged"]
    C2 --> F[Dodo overlay checkout iframe]
    F --> G[fraud stack initBiometricAndTracking]
    G --> H{Was level-1 block the only bottleneck?}
    H -->|Yes| I[Overlay completes - tighten origins]
    H -->|No| J[Adopt redirect mode - revert to empty]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Incoming Request] --> B{Route}
    B -->|catch-all dashboard| C[nginx-security-headers.conf]
    B -->|"/embed or /embed.html"| D[nginx-embed-security-headers.conf]
    B -->|"/api/"| E[API proxy]
    C --> C1["accelerometer=* bluetooth=*\ngyroscope=* magnetometer=*\npicture-in-picture=*\nLOOSENED diagnostic"]
    C1 --> C2[All frame-src iframes gain sensor access]
    D --> D1["accelerometer=() bluetooth=()\ngyroscope=() magnetometer=()\npicture-in-picture=()\nLOCKED DOWN unchanged"]
    C2 --> F[Dodo overlay checkout iframe]
    F --> G[fraud stack initBiometricAndTracking]
    G --> H{Was level-1 block the only bottleneck?}
    H -->|Yes| I[Overlay completes - tighten origins]
    H -->|No| J[Adopt redirect mode - revert to empty]
Loading

Reviews (1): Last reviewed commit: "fix(checkout): keep docker embed headers..." | Re-trigger Greptile

Comment thread vercel.json
{ "key": "Cross-Origin-Opener-Policy-Report-Only", "value": "same-origin; report-to=\"wm-coop-coep\"" },
{ "key": "Cross-Origin-Embedder-Policy-Report-Only", "value": "require-corp; report-to=\"wm-coop-coep\"" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(self), accelerometer=(), autoplay=(self \"https://www.youtube.com\" \"https://www.youtube-nocookie.com\"), bluetooth=(), display-capture=(), encrypted-media=(self \"https://www.youtube.com\" \"https://www.youtube-nocookie.com\"), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), midi=(), payment=(self \"https://checkout.dodopayments.com\" \"https://test.checkout.dodopayments.com\" \"https://pay.google.com\" \"https://hooks.stripe.com\" \"https://js.stripe.com\"), picture-in-picture=(self \"https://www.youtube.com\" \"https://www.youtube-nocookie.com\" \"https://challenges.cloudflare.com\"), unload=(*), screen-wake-lock=(), serial=(), usb=(), xr-spatial-tracking=(\"https://challenges.cloudflare.com\")" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(self), accelerometer=*, autoplay=(self \"https://www.youtube.com\" \"https://www.youtube-nocookie.com\"), bluetooth=*, display-capture=(), encrypted-media=(self \"https://www.youtube.com\" \"https://www.youtube-nocookie.com\"), gyroscope=*, hid=(), idle-detection=(), magnetometer=*, midi=(), payment=(self \"https://checkout.dodopayments.com\" \"https://test.checkout.dodopayments.com\" \"https://pay.google.com\" \"https://hooks.stripe.com\" \"https://js.stripe.com\"), picture-in-picture=*, unload=(*), screen-wake-lock=(), serial=(), usb=(), xr-spatial-tracking=(\"https://challenges.cloudflare.com\")" },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security bluetooth=* enables actual Bluetooth device connections, not just API presence

accelerometer, gyroscope, and magnetometer are passive read-only sensor APIs — once allowed by policy they're accessed silently with no user dialog. bluetooth=* is different: the Web Bluetooth API can enumerate paired devices and initiate connections, but the browser always requires an explicit user-facing device-picker prompt before any scan occurs.

The concern here is that every frame already in frame-src (YouTube, Cloudflare Turnstile, Dodo, Google Pay, Stripe, Vercel Live, etc.) now has the policy-level permission to trigger that Bluetooth picker on the dashboard. If the Sardine/Airwallex fraud library only calls navigator.bluetooth !== undefined to detect capability (not actually scan), then bluetooth=* is broader than necessary. It's worth confirming from the HAR whether the library performs an actual requestDevice() call or just checks API presence — if it's the latter, the sensor fix without bluetooth may already be sufficient to unblock it.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 963 to 994
assert.ok(!getCspDirectiveTokens(csp, 'script-src').includes("'unsafe-inline'"));
});
}

it('keeps Docker embed routes on the locked-down embed security headers', () => {
const nginxTemplate = readFileSync(resolve(__dirname, '../docker/nginx.conf.template'), 'utf-8');
assert.match(nginxTemplate, /location = \/embed \{[\s\S]*?include \/etc\/nginx\/embed_security_headers\.conf;/);
assert.match(nginxTemplate, /location = \/embed\.html \{[\s\S]*?include \/etc\/nginx\/embed_security_headers\.conf;/);
assert.match(frontendDockerfileSource, /COPY docker\/nginx-embed-security-headers\.conf \/etc\/nginx\/embed_security_headers\.conf/);
assert.match(dockerNginxSource, /location = \/embed \{[\s\S]*?add_header Permissions-Policy "camera=\(\), microphone=\(\), geolocation=\(\), accelerometer=\(\)/);
assert.match(dockerNginxSource, /location = \/embed\.html \{[\s\S]*?add_header Permissions-Policy "camera=\(\), microphone=\(\), geolocation=\(\), accelerometer=\(\)/);

const lockedPolicy = getHeaderValueForSource('/embed', 'Permissions-Policy');
const dockerLockedPolicy = getNginxHeaderValueFrom('docker/nginx-embed-security-headers.conf', 'Permissions-Policy');
assert.equal(dockerLockedPolicy, lockedPolicy, 'Docker embed Permissions-Policy must match Vercel embed policy');
for (const directive of [
'accelerometer=()',
'bluetooth=()',
'gyroscope=()',
'magnetometer=()',
'picture-in-picture=()',
'payment=()',
]) {
assert.ok(dockerLockedPolicy.includes(directive), `Docker embed policy must keep ${directive}`);
}

const dockerEmbedCsp = getNginxHeaderValueFrom('docker/nginx-embed-security-headers.conf', 'Content-Security-Policy');
assert.equal(dockerEmbedCsp, getHeaderValueForSource('/embed', 'Content-Security-Policy'));
});
});

// Per-route CSP override for the hosted brief magazine. The renderer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 docker/nginx.conf embed headers are only prefix-checked, not compared for full parity

The parity assertion assert.equal(dockerLockedPolicy, lockedPolicy) compares nginx-embed-security-headers.conf against the Vercel /embed policy — good. But docker/nginx.conf's inline embed Permissions-Policy is only spot-checked via a regex prefix match (camera=\(\), microphone=\(\), geolocation=\(\), accelerometer=\(\)). The same getNginxHeaderValueFrom + assert.equal pattern could be applied to docker/nginx.conf's embed location to catch any future drift between the static dev config and the template-based production path.

koala73 added a commit that referenced this pull request Jun 26, 2026
#4449) (#4453)

HAR-proven ineffective. With accelerometer=* / bluetooth=* / picture-in-picture=*
live on prod, the nested Dodo → Airwallex → Sardine frame STILL reported
"accelerometer is not allowed in this document" and the payment still hung at
"Processing…". The block is the Dodo SDK's own iframe `allow="payment keyboard-map *"`
attribute (level 2) plus Dodo's nested-frame delegation (level 3) — both outside our
control. The loosening was therefore a pure security regression with zero benefit.

Restores accelerometer/bluetooth/gyroscope/magnetometer to `()` and picture-in-picture
to the YouTube+Turnstile allowlist across vercel.json, docker/nginx-security-headers.conf,
and the deploy-config guardrail. Keeps the unrelated docker/nginx-embed work from #4450.

The Dodo overlay is not salvageable from our side; the fix is redirect mode (#4449).

Refs #4449 #4450
koala73 added a commit that referenced this pull request Jun 26, 2026
…y iframe (#4449) (#4454)

* fix(checkout): redirect to hosted Dodo checkout instead of the overlay iframe (#4449)

The overlay-checkout iframe cannot host Dodo's nested 3DS/fraud stack
(Hyperswitch → Airwallex → Sardine): the device sensors it fingerprints with are
blocked two frames deep by BOTH our Permissions-Policy AND the Dodo SDK's own
iframe `allow` attribute — HAR-confirmed, loosening our header to `*` changed
nothing (#4450). Card payments requiring 3DS hung at "Processing…" forever.

Switch both checkout surfaces to navigate the top window to Dodo's HOSTED checkout
(their documented primary flow): 3DS/fraud run unconstrained top-level, and #4447
returns the customer to /dashboard?wm_checkout=return to reconcile.

- src/services/checkout.ts (dashboard) + pro-test/src/services/checkout.ts (/pro):
  replace DodoPayments.Checkout.open(checkout_url) with a validated
  window.location.assign. New safeHostedCheckoutUrl() guards open-redirect
  (https + checkout.dodopayments.com origin only); a missing/untrusted URL falls
  through to the existing contract-violation handler.
- Overlay machinery (Initialize/onEvent/watchdog/openCheckout) left dormant
  pending removal — the overlay SDK is now never loaded on the dashboard.
- Test: checkout-overlay-lifecycle asserts startCheckout navigates (assignedUrls)
  and does NOT open the overlay (openedUrls empty). Red→green.

Refs #4449 #4450

* fix(checkout): address #4454 review — guard /pro return URL + test the redirect guard

#1 (P1) /pro pre-marked success: /pro sent Dodo `?wm_checkout=success` as the
return_url. With hosted redirect now the primary flow Dodo sends the buyer there for
EVERY outcome — a failed/cancelled/pending/no-ID return false-succeeded via the bare
success marker (checkout-return.ts:113). Switch /pro to the GUARDED `?wm_checkout=return`
contract (mirrors the dashboard / #4447): success reconciles only against authoritative
Dodo evidence (subscription_id/payment_id + success status); a non-success return can no
longer succeed.

#3 (P2) untested open-redirect guard: extract safeHostedCheckoutUrl + HOSTED_CHECKOUT_HOSTS
to src/services/hosted-checkout-url.ts (dependency-free, imported by checkout.ts) and add
tests/hosted-checkout-url.test.mts — http downgrade, third-party host, look-alike suffix
host, unlisted subdomain, javascript: URL, unparseable/empty strings, non-string inputs,
plus both valid hosts. 10 cases.

#4 (P2) stale comments: update the checkout.ts and pro-test file headers + the dormant
redirect_requested comment so they describe redirect as the active flow and the overlay
machinery as dormant.

#2 (P2 /pro test coverage): not addressed — pro-test has no test runner; adding one is a
separate infra task. The shared validator (#3) and the dashboard navigation test cover the
redirect logic, which /pro mirrors.

Rebuilt public/pro bundle. Refs #4449 #4454.

* fix(checkout): #4454 review round 2 — drop dead /pro overlay SDK load + Sentry parity + docs-stats

CI docs-stats: the new src/services/hosted-checkout-url.ts bumped serviceTopLevelEntries
187→188 — regenerate docs/generated/stats.json and update the AGENTS.md count.

Greptile (a) dead overlay SDK load on /pro: App.tsx called initOverlay() on mount, which
dynamically imported the heavy `dodopayments-checkout` SDK and registered a success banner
that can never fire under redirect mode (the buyer lands on the dashboard, not /pro). Remove
the call (+ now-unused imports). Symmetric with the dashboard, which simply stopped invoking
its overlay init. Bonus: initOverlay is now an unused export, so the bundler tree-shakes the
overlay SDK chunk out of the /pro bundle entirely (index.esm-*.js removed).

Greptile (b) Sentry parity: the /pro missing/untrusted checkout_url path was console.error-only;
add a Sentry.captureMessage so the server-contract violation is observable, matching the
dashboard's missing-checkout-url path.

Rebuilt public/pro. Refs #4449 #4454.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working P0 Critical, blocks release or core functionality spike Time-boxed investigation, not an implementation task

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant