name: painel-corretor-tampermonkey-csp
description: Tampermonkey script on Painel do Corretor — CSP blocks GM_xmlhttpRequest but allows fetch(). Workaround using native fetch + AbortController.
Painel do Corretor - Tampermonkey Script CSP Issue
Context
Building a Tampermonkey userscript that runs on https://app2.paineldocorretor.com.br/crm/negocios to sync leads to Supabase.
The Problem
The Painel do Corretor app has a strict CSP (Content Security Policy) connect-src directive that does NOT include supabase.co or api.paineldocorretor.net domains.
CSP connect-src (confirmed 2026-05-19):
`
connect-src 'self' auth.paineldocorretor.com.br paineldocorretor.auth0.com api.paineldocorretor.net
https://sessions.bugsnag.com https://widget.freshworks.com notify.bugsnag.com js.hs-banner.com
api.hubapi.com .hubspot.com .doubleclick.net *.google.com api-js.mixpanel.com backend.getbeamer.com
wss://faye-us-east.stream-io-api.com/faye .stream-io-api.com .googleapis.com google.com
.zapier.com .refiner.io *.googleadservices.com https://trindade.freshdesk.com
https://zapier.com/api/v4/tracking/event/ https://viacep.com.br
`
Notice: NO supabase.co, NO dauftiqcvgaydddoxqhh.supabase.co
Critical Finding
- -
GM_xmlhttpRequest(Tampermonkey special API) → BLOCKED by CSP — requests hang forever with no error - - Native
fetch()→ ALLOWED by CSP — works normally - -
GM_xmlhttpRequestbypasses the browser's CSP enforcement but the CSP still applies because the Painel app sets a strong CSP header that the browser enforces even for Tampermonkey scripts - - Cloudflare on
chatwoot.rochasalesseguros.com.brblocks Tampermonkey direct-install URLs. Workaround: download script file, then import via Tampermonkey Dashboard → Utilities → Import from file. - - Auth0 tenant:
a1af7a2f-7ec6-40cd-b257-86c045875979 - - Auth URL:
https://auth.paineldocorretor.com.br/oauth/token - - GraphQL URL:
https://api.paineldocorretor.net/graphql - - Endpoint:
https://api.paineldocorretor.net/graphql - - Auth: Bearer token from Auth0, header
x-tenant: a1af7a2f-7ec6-40cd-b257-86c045875979 - - Query:
negociosElasticwithetapaIdfilter - -
crm_leads_duplicate— main leads table - -
chatwoot_painel_map— Kanban mapping
Solution
Use native fetch() with AbortController for timeout instead of GM_xmlhttpRequest:
`javascript
function fetchWithTimeout(url, opts) {
opts = opts || {};
var timeout = opts.timeout || 20000;
var controller = new AbortController();
var timer = setTimeout(function() { controller.abort(); }, timeout);
return fetch(url, Object.assign({ signal: controller.signal }, opts))
.finally(function() { clearTimeout(timer); });
}
`
Also Note
Verified Working Script
https://chatwoot.rochasalesseguros.com.br/crm-sync-v4.user.js (V4.1, uses fetch + AbortController)
Relevant Painel GraphQL
Supabase Tables
Validating Sync Accuracy
When the sync appears to work but numbers don't match, use this 3-way validation:
Step 1 — Painel API count (server-side curl):
`bash
TOKEN=$(curl -s -X POST "https://auth.paineldocorretor.com.br/oauth/token" \
-H "Content-Type: application/json" \
-d '{"grant_type":"http://auth0.com/oauth/grant-type/password-realm","realm":"Username-Password-Authentication","username":"contato@rochasalesseguros.com.br","password":"221109","client_id":"MtCJ0_REDACTED","audience":"https://paineldocorretor.com.br/api/"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
Sum all stages (empty conditions = all leads):
curl -s -X POST "https://api.paineldocorretor.net/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "x-tenant: a1af7a2f-7ec6-40cd-b257-86c045875979" \
-d '{"query":"{ negociosElastic(request: {busca: null, take: 1, skip: 0, conditions: []}) { quantidade } }"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['negociosElastic']['quantidade'])"
`
Step 2 — Supabase counts:
`python
Via Python urllib (bypasses browser CSP):
import urllib.request
ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
for table in ["crm_leads_duplicate", "chatwoot_painel_map"]:
url = f"https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/{table}?select=id&limit=1"
req = urllib.request.Request(url)
req.add_header("apikey", ANON)
req.add_header("Authorization", f"Bearer {ANON}")
req.add_header("Prefer", "count=exact")
res = urllib.request.urlopen(req, timeout=15)
print(table, res.info().get('Content-Range'))
`
Step 3 — Kanban cards via browser console:
`javascript
var iframe = document.querySelector('iframe');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
var cols = iframeDoc.querySelectorAll('div[style*="width: 280px"]');
var result = [];
cols.forEach(function(col) {
var headerSpans = col.querySelectorAll('span');
var name = '', count = '';
for (var i = 0; i < headerSpans.length; i++) {
var s = headerSpans[i];
if (s.style.fontWeight === '700' && s.style.color === 'rgb(107, 114, 128)') name = s.textContent.trim();
if (s.style.backgroundColor === 'rgba(107, 114, 128, 0.2)') count = s.textContent.trim();
}
var dropArea = col.querySelector('div[style*="rgb(15, 17, 23)"]');
var actualCards = dropArea ? dropArea.querySelectorAll('div[style*="rgb(31, 41, 55)"]').length : 0;
result.push({name, headerCount: count, actualCards});
});
console.log(JSON.stringify(result, null, 2));
`
Expected: Painel > Supabase ≈ Kanban (small differences normal due to real-time updates between syncs).