Guia
Copie estas receitas para qualquer projeto cliente (Lovable, Vite, PWA) para consumir o proxy opaco sem vazar o host upstream, nomes de rotas ou credenciais.
Os tenants vivem na tabela proxy_tenants no banco de dados. Estrutura sugerida:
[
{
"id": "acme",
"upstream": "https://xxxx.supabase.co",
"allowedOrigins": ["https://app.acme.com"],
"rateLimitPerMin": 120,
"injectHeaders": {
"apikey": "sb_publishable_...",
"authorization": "Bearer sb_publishable_..."
},
"verbMap": {
"0": { "prefix": "/rest/v1" },
"1": { "prefix": "/auth/v1" },
"2": { "prefix": "/storage/v1" },
"3": { "prefix": "/functions/v1" }
}
}
]Nunca chame /admin/token do navegador: a chave de admin vazaria. Emita no seu servidor, envie o token opaco de curta duração para o navegador.
// Server-side
const res = await fetch("https://<this-proxy>/api/public/admin/token", {
method: "POST",
headers: {
"content-type": "application/json",
"x-admin-key": process.env.PROXY_ADMIN_KEY,
},
body: JSON.stringify({ tenantId: "acme", ttlSeconds: 300 }),
});
const { token } = await res.json();Monte o host do proxy em tempo de execução a partir de bytes com máscara XOR para que nenhuma string simples apareça no bundle.
// src/lib/x/o.ts
const _k = [0x5a, 0x37, 0xc1, 0x09, 0x6e, 0xa2] as const;
// bytes = xor(host, _k) — precompute per-project
const _b = [/* ... */] as const;
function _x() {
let s = "";
for (let i = 0; i < _b.length; i++)
s += String.fromCharCode(_b[i] ^ _k[i % _k.length]);
return s;
}
const proto = String.fromCharCode(104,116,116,112,115,58,47,47);
export const origin = () => proto + _x();
export const gateway = () => origin() + "/api/public/e";// src/lib/x/c.ts
import { gateway } from "./o";
const V = { rest: "0", auth: "1", storage: "2", fn: "3" } as const;
type Verb = keyof typeof V;
let _t: { v: string; exp: number } | null = null;
async function tok(): Promise<string> {
if (_t && _t.exp - 10_000 > Date.now()) return _t.v;
// proxy your own /api/mint endpoint — never call /admin/token from browser
const r = await fetch("/api/mint", { method: "POST" });
const d = await r.json();
_t = { v: d.token, exp: d.expiresAt };
return _t.v;
}
export async function fx(v: Verb, tail = "", init: RequestInit = {}) {
const h = new Headers(init.headers || {});
h.set("x-t", await tok());
if (!h.has("content-type")) h.set("content-type", "application/json");
return fetch(gateway() + "/" + V[v] + (tail ? "/" + tail : ""), {
...init, headers: h, credentials: "omit", cache: "no-store",
});
}// vite.config.ts (excerpt)
export default defineConfig({
build: {
sourcemap: false,
minify: "terser",
terserOptions: {
compress: { drop_console: true, drop_debugger: true, passes: 2 },
mangle: { toplevel: true, properties: { regex: /^_/ } },
format: { comments: false },
},
rollupOptions: {
output: {
entryFileNames: "assets/[hash].js",
chunkFileNames: "assets/[hash].js",
assetFileNames: "assets/[hash][extname]",
manualChunks(id) {
if (id.includes("src/lib/x")) return "x";
},
},
},
},
});Sirva /version.json com no-store; em caso de divergência, carregue /api/public/sw-kill.js deste proxy para limpar SW obsoletos + caches.
// on app boot
const local = "1.0.0";
fetch("/version.json", { cache: "no-store" })
.then((r) => r.json())
.then((d) => {
if (d.version !== local) {
const s = document.createElement("script");
s.src = "https://<this-proxy>/api/public/sw-kill.js";
document.head.appendChild(s);
}
});# .github/workflows/scan.yml (or run locally after build)
bun run build
rg -i "supabase\\.co" dist && exit 1 || true
rg -i "functions/v1" dist && exit 1 || true
rg -i "service_role|sk_live|sk_test" dist && exit 1 || true
find dist -name "*.map" | grep . && exit 1 || true