📄 SKILL.md

← Vault

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

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).