Translated ['src/generic-methodologies-and-resources/phishing-methodolog

This commit is contained in:
Translator 2025-04-27 16:32:40 +00:00
parent 9b386a9577
commit b4e8dac4e6
5 changed files with 237 additions and 76 deletions

View File

@ -31,6 +31,7 @@ additional-js = [
"theme/tabs.js",
"theme/ht_searcher.js",
"theme/sponsor.js",
"theme/ai.js"
]
no-section-label = true
preferred-dark-theme = "hacktricks-dark"

View File

@ -561,6 +561,7 @@
- [CSRF (Cross Site Request Forgery)](pentesting-web/csrf-cross-site-request-forgery.md)
- [Dangling Markup - HTML scriptless injection](pentesting-web/dangling-markup-html-scriptless-injection/README.md)
- [SS-Leaks](pentesting-web/dangling-markup-html-scriptless-injection/ss-leaks.md)
- [DApps - Decentralized Applications](pentesting-web/dapps-DecentralizedApplications.md)
- [Dependency Confusion](pentesting-web/dependency-confusion.md)
- [Deserialization](pentesting-web/deserialization/README.md)
- [NodeJS - \_\_proto\_\_ & prototype Pollution](pentesting-web/deserialization/nodejs-proto-prototype-pollution/README.md)
@ -625,6 +626,7 @@
- [Regular expression Denial of Service - ReDoS](pentesting-web/regular-expression-denial-of-service-redos.md)
- [Reset/Forgotten Password Bypass](pentesting-web/reset-password.md)
- [Reverse Tab Nabbing](pentesting-web/reverse-tab-nabbing.md)
- [RSQL Injection](pentesting-web/rsql-injection.md)
- [SAML Attacks](pentesting-web/saml-attacks/README.md)
- [SAML Basics](pentesting-web/saml-attacks/saml-basics.md)
- [Server Side Inclusion/Edge Side Inclusion Injection](pentesting-web/server-side-inclusion-edge-side-inclusion-injection.md)

View File

@ -24,15 +24,15 @@
- **Palavra-chave**: O nome do domínio **contém** uma **palavra-chave** importante do domínio original (por exemplo, zelster.com-management.com).
- **subdomínio hifenizado**: Mudar o **ponto por um hífen** de um subdomínio (por exemplo, www-zelster.com).
- **Novo TLD**: Mesmo domínio usando um **novo TLD** (por exemplo, zelster.org).
- **Homoglyph**: **Substitui** uma letra no nome do domínio por **letras que parecem semelhantes** (por exemplo, zelfser.com).
- **Transposição:** **Troca duas letras** dentro do nome do domínio (por exemplo, zelsetr.com).
- **Homoglyph**: Ele **substitui** uma letra no nome do domínio por **letras que parecem semelhantes** (por exemplo, zelfser.com).
- **Transposição:** Ele **troca duas letras** dentro do nome do domínio (por exemplo, zelsetr.com).
- **Singularização/Pluralização**: Adiciona ou remove “s” no final do nome do domínio (por exemplo, zeltsers.com).
- **Omissão**: **Remove uma** das letras do nome do domínio (por exemplo, zelser.com).
- **Repetição:** **Repete uma** das letras no nome do domínio (por exemplo, zeltsser.com).
- **Substituição**: Como homoglyph, mas menos furtivo. Substitui uma das letras no nome do domínio, talvez por uma letra próxima da letra original no teclado (por exemplo, zektser.com).
- **Subdominado**: Introduz um **ponto** dentro do nome do domínio (por exemplo, ze.lster.com).
- **Inserção**: **Insere uma letra** no nome do domínio (por exemplo, zerltser.com).
- **Ponto ausente**: Anexa o TLD ao nome do domínio. (por exemplo, zelstercom.com)
- **Omissão**: Ele **remove uma** das letras do nome do domínio (por exemplo, zelser.com).
- **Repetição:** Ele **repete uma** das letras no nome do domínio (por exemplo, zeltsser.com).
- **Substituição**: Como homoglyph, mas menos furtivo. Ele substitui uma das letras no nome do domínio, talvez por uma letra próxima da letra original no teclado (por exemplo, zektser.com).
- **Subdominado**: Introduzir um **ponto** dentro do nome do domínio (por exemplo, ze.lster.com).
- **Inserção**: Ele **insere uma letra** no nome do domínio (por exemplo, zerltser.com).
- **Ponto ausente**: Anexar o TLD ao nome do domínio. (por exemplo, zelstercom.com)
**Ferramentas Automáticas**
@ -47,7 +47,7 @@
### Bitflipping
Há uma **possibilidade de que alguns bits armazenados ou em comunicação possam ser automaticamente invertidos** devido a vários fatores, como flares solares, raios cósmicos ou erros de hardware.
Há uma **possibilidade de que um de alguns bits armazenados ou em comunicação possa ser automaticamente invertido** devido a vários fatores, como flares solares, raios cósmicos ou erros de hardware.
Quando esse conceito é **aplicado a solicitações DNS**, é possível que o **domínio recebido pelo servidor DNS** não seja o mesmo que o domínio inicialmente solicitado.
@ -107,7 +107,7 @@ mkdir /opt/gophish/ssl_keys
cp "/etc/letsencrypt/live/$DOMAIN/privkey.pem" /opt/gophish/ssl_keys/key.pem
cp "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" /opt/gophish/ssl_keys/key.crt
```
**Configuração de Mail**
**Configuração de e-mail**
Comece instalando: `apt-get install postfix`
@ -126,7 +126,7 @@ Finalmente, modifique os arquivos **`/etc/hostname`** e **`/etc/mailname`** para
Agora, crie um **registro DNS A** de `mail.<domain>` apontando para o **endereço IP** do VPS e um **registro DNS MX** apontando para `mail.<domain>`
Agora vamos testar o envio de um email:
Agora vamos testar o envio de um e-mail:
```bash
apt install mailutils
echo "This is the body of the email" | mail -s "This is the subject line" test@email.com
@ -223,7 +223,7 @@ service gophish stop
### Espere e seja legítimo
Quanto mais antigo for um domínio, menos provável é que ele seja identificado como spam. Portanto, você deve esperar o máximo de tempo possível (pelo menos 1 semana) antes da avaliação de phishing. Além disso, se você colocar uma página sobre um setor reputacional, a reputação obtida será melhor.
Quanto mais antigo for um domínio, menos provável é que ele seja identificado como spam. Portanto, você deve esperar o máximo de tempo possível (pelo menos 1 semana) antes da avaliação de phishing. Além disso, se você criar uma página sobre um setor reputacional, a reputação obtida será melhor.
Observe que, mesmo que você tenha que esperar uma semana, pode terminar de configurar tudo agora.
@ -243,7 +243,7 @@ Este é o conteúdo que deve ser definido dentro de um registro TXT no domínio:
```bash
v=spf1 mx a ip4:ip.ip.ip.ip ?all
```
### Domain-based Message Authentication, Reporting & Conformance (DMARC) Record
### Registro de Autenticação, Relatório e Conformidade de Mensagens Baseado em Domínio (DMARC)
Você deve **configurar um registro DMARC para o novo domínio**. Se você não sabe o que é um registro DMARC [**leia esta página**](../../network-services-pentesting/pentesting-smtp/index.html#dmarc).
@ -305,11 +305,11 @@ A página [www.mail-tester.com](https://www.mail-tester.com) pode indicar se seu
- Decida de qual conta você vai enviar os emails de phishing. Sugestões: _noreply, support, servicedesk, salesforce..._
- Você pode deixar em branco o nome de usuário e a senha, mas certifique-se de marcar a opção Ignorar Erros de Certificado
![](<../../images/image (253) (1) (2) (1) (1) (2) (2) (3) (3) (5) (3) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (10) (15) (2).png>)
![](<../../images/image (253) (1) (2) (1) (1) (2) (2) (3) (3) (5) (3) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (10) (15) (2).png>)
> [!NOTE]
> É recomendado usar a funcionalidade "**Enviar Email de Teste**" para testar se tudo está funcionando.\
> Eu recomendaria **enviar os emails de teste para endereços de 10min** a fim de evitar ser colocado na lista negra durante os testes.
> Eu recomendaria **enviar os emails de teste para endereços de 10min** para evitar ser colocado na lista negra durante os testes.
### Modelo de Email
@ -338,7 +338,7 @@ WRITE HERE SOME SIGNATURE OF SOMEONE FROM THE COMPANY
Note que **para aumentar a credibilidade do e-mail**, é recomendável usar alguma assinatura de um e-mail do cliente. Sugestões:
- Envie um e-mail para um **endereço inexistente** e verifique se a resposta tem alguma assinatura.
- Pesquise por **e-mails públicos** como info@ex.com ou press@ex.com ou public@ex.com e envie um e-mail para eles e aguarde a resposta.
- Procure por **e-mails públicos** como info@ex.com ou press@ex.com ou public@ex.com e envie um e-mail para eles e aguarde a resposta.
- Tente contatar **algum e-mail válido descoberto** e aguarde a resposta.
![](<../../images/image (80).png>)
@ -403,13 +403,13 @@ phishing-documents.md
### Via Proxy MitM
O ataque anterior é bastante inteligente, pois você está falsificando um site real e coletando as informações fornecidas pelo usuário. Infelizmente, se o usuário não inserir a senha correta ou se o aplicativo que você falsificou estiver configurado com 2FA, **essa informação não permitirá que você se passe pelo usuário enganado**.
O ataque anterior é bastante inteligente, pois você está falsificando um site real e coletando as informações fornecidas pelo usuário. Infelizmente, se o usuário não inseriu a senha correta ou se o aplicativo que você falsificou está configurado com 2FA, **essa informação não permitirá que você se passe pelo usuário enganado**.
É aqui que ferramentas como [**evilginx2**](https://github.com/kgretzky/evilginx2)**,** [**CredSniper**](https://github.com/ustayready/CredSniper) e [**muraena**](https://github.com/muraenateam/muraena) são úteis. Essa ferramenta permitirá que você gere um ataque do tipo MitM. Basicamente, os ataques funcionam da seguinte maneira:
É aqui que ferramentas como [**evilginx2**](https://github.com/kgretzky/evilginx2)**,** [**CredSniper**](https://github.com/ustayready/CredSniper) e [**muraena**](https://github.com/muraenateam/muraena) são úteis. Esta ferramenta permitirá que você gere um ataque do tipo MitM. Basicamente, os ataques funcionam da seguinte maneira:
1. Você **falsifica o formulário de login** da página da web real.
2. O usuário **envia** suas **credenciais** para sua página falsa e a ferramenta as envia para a página da web real, **verificando se as credenciais funcionam**.
3. Se a conta estiver configurada com **2FA**, a página MitM pedirá isso e, uma vez que o **usuário o introduza**, a ferramenta o enviará para a página da web real.
1. Você **falsifica o formulário de login** da página real.
2. O usuário **envia** suas **credenciais** para sua página falsa e a ferramenta as envia para a página real, **verificando se as credenciais funcionam**.
3. Se a conta estiver configurada com **2FA**, a página MitM pedirá isso e, uma vez que o **usuário o introduza**, a ferramenta o enviará para a página real.
4. Uma vez que o usuário esteja autenticado, você (como atacante) terá **capturado as credenciais, o 2FA, o cookie e qualquer informação** de cada interação enquanto a ferramenta está realizando um MitM.
### Via VNC
@ -422,7 +422,7 @@ Você pode fazer isso com [**EvilnVNC**](https://github.com/JoelGMSec/EvilnoVNC)
Obviamente, uma das melhores maneiras de saber se você foi descoberto é **pesquisar seu domínio em listas negras**. Se ele aparecer listado, de alguma forma seu domínio foi detectado como suspeito.\
Uma maneira fácil de verificar se seu domínio aparece em alguma lista negra é usar [https://malwareworld.com/](https://malwareworld.com)
No entanto, existem outras maneiras de saber se a vítima está **ativamente procurando por atividades de phishing suspeitas na web**, conforme explicado em:
No entanto, existem outras maneiras de saber se a vítima está **ativamente procurando por atividades de phishing suspeitas na rede**, conforme explicado em:
{{#ref}}
detecting-phising.md

141
theme/ai.js Normal file
View File

@ -0,0 +1,141 @@
/**
* HackTricks AI Chat Widget v1.14 animated typing indicator
* ------------------------------------------------------------------------
* Replaces the static placeholder with a threedot **bouncing** loader
* while waiting for the assistants response.
* ------------------------------------------------------------------------
*/
(function () {
const LOG = "[HackTricks-AI]";
/* ---------------- Usertunable constants ---------------- */
const MAX_CONTEXT = 3000; // highlightedtext char limit
const MAX_QUESTION = 500; // question char limit
const TOOLTIP_TEXT =
"💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
const BRAND_RED = "#b31328"; // HackTricks brand
/* ------------------------------ State ------------------------------ */
let threadId = null;
let isRunning = false;
const $ = (sel, ctx = document) => ctx.querySelector(sel);
if (document.getElementById("ht-ai-btn")) { console.warn(`${LOG} Widget already injected.`); return; }
(document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", init) : init());
/* ==================================================================== */
async function init() {
console.log(`${LOG} Injecting widget… v1.14`);
await ensureThreadId();
injectStyles();
const btn = createFloatingButton();
createTooltip(btn);
const panel = createSidebar();
const chatLog = $("#ht-ai-chat");
const sendBtn = $("#ht-ai-send");
const inputBox = $("#ht-ai-question");
const resetBtn = $("#ht-ai-reset");
const closeBtn = $("#ht-ai-close");
/* ------------------- Selection snapshot ------------------- */
let savedSelection = "";
btn.addEventListener("pointerdown", () => { savedSelection = window.getSelection().toString().trim(); });
/* ------------------- Helpers ------------------------------ */
function addMsg(text, cls) {
const b = document.createElement("div");
b.className = `ht-msg ${cls}`;
b.textContent = text;
chatLog.appendChild(b);
chatLog.scrollTop = chatLog.scrollHeight;
return b;
}
const LOADER_HTML = '<span class="ht-loading"><span></span><span></span><span></span></span>';
function setInputDisabled(d) { inputBox.disabled = d; sendBtn.disabled = d; }
function clearThreadCookie() { document.cookie = "threadId=; Path=/; Max-Age=0"; threadId = null; }
function resetConversation() { chatLog.innerHTML=""; clearThreadCookie(); panel.classList.remove("open"); }
/* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => {
if (!savedSelection) { alert("Please highlight some text first to then ask Hacktricks AI about it."); return; }
if (savedSelection.length > MAX_CONTEXT) { alert(`Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`); return; }
chatLog.innerHTML=""; addMsg(savedSelection, "ht-context"); panel.classList.add("open"); inputBox.focus();
});
closeBtn.addEventListener("click", resetConversation);
resetBtn.addEventListener("click", resetConversation);
/* --------------------------- Messaging --------------------------- */
async function sendMessage(question, context=null) {
if (!threadId) await ensureThreadId();
if (isRunning) { addMsg("Please wait until the current operation completes.", "ht-ai"); return; }
isRunning = true; setInputDisabled(true);
const loadingBubble = addMsg("", "ht-ai");
loadingBubble.innerHTML = LOADER_HTML;
const content = context ? `### Context:\n${context}\n\n### Question to answer:\n${question}` : question;
try {
const res = await fetch(`${API_BASE}/${threadId}/messages`, { method:"POST", credentials:"include", headers:{"Content-Type":"application/json"}, body:JSON.stringify({content}) });
if (!res.ok) {
let err=`Unknown error: ${res.status}`;
try { const e=await res.json(); if(e.error) err=`Error: ${e.error}`; else if(res.status===429) err="Rate limit exceeded. Please try again later."; } catch(_){}
loadingBubble.textContent = err; return; }
const data = await res.json();
loadingBubble.remove();
if (Array.isArray(data.response)) data.response.forEach(p=>{ addMsg( p.type==="text"&&p.text&&p.text.value ? p.text.value : JSON.stringify(p), "ht-ai"); });
else if (typeof data.response === "string") addMsg(data.response, "ht-ai");
else addMsg(JSON.stringify(data,null,2), "ht-ai");
} catch (e) { console.error("Error sending message:",e); loadingBubble.textContent="An unexpected error occurred."; }
finally { isRunning=false; setInputDisabled(false); chatLog.scrollTop=chatLog.scrollHeight; }
}
async function handleSend(){ const q=inputBox.value.trim(); if(!q)return; if(q.length>MAX_QUESTION){alert(`Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`); return;} inputBox.value=""; addMsg(q,"ht-user"); await sendMessage(q,savedSelection||null);}
sendBtn.addEventListener("click", handleSend);
inputBox.addEventListener("keydown", e=>{ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); handleSend(); } });
}
/* ==================================================================== */
async function ensureThreadId(){ const m=document.cookie.match(/threadId=([^;]+)/); if(m&&m[1]){threadId=m[1];return;} try{ const r=await fetch(API_BASE,{method:"POST",credentials:"include"}); const d=await r.json(); if(!r.ok||!d.threadId) throw new Error(`${r.status} ${r.statusText}`); threadId=d.threadId; document.cookie=`threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`; }catch(e){ console.error("Error creating threadId:",e); alert("Failed to initialise the conversation. Please refresh and try again."); throw e; }}
/* ==================================================================== */
function injectStyles(){ const css=`
#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s}
#ht-ai-btn:hover{opacity:.85}
@media(max-width:768px){#ht-ai-btn{display:none}}
#ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000}
#ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)}
#ht-ai-panel{position:fixed;top:0;right:0;height:100%;width:350px;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif}
#ht-ai-panel.open{transform:translateX(0)}
@media(max-width:768px){#ht-ai-panel{display:none}}
#ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333}
#ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center}
#ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0}
#ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7}
#ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px}
.ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word}
.ht-user{align-self:flex-end;background:${BRAND_RED}}
.ht-ai{align-self:flex-start;background:#222}
.ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px}
#ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333}
#ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px}
#ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer}
#ht-ai-send:disabled{opacity:.5;cursor:not-allowed}
/* Loader animation */
.ht-loading{display:inline-flex;align-items:center;gap:4px}
.ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out}
.ht-loading span:nth-child(2){animation-delay:0.2s}
.ht-loading span:nth-child(3){animation-delay:0.4s}
@keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
::selection{background:#ffeb3b;color:#000}
::-moz-selection{background:#ffeb3b;color:#000}`;
const s=document.createElement("style"); s.id="ht-ai-style"; s.textContent=css; document.head.appendChild(s);}
function createFloatingButton(){ const d=document.createElement("div"); d.id="ht-ai-btn"; d.textContent="🤖"; document.body.appendChild(d); return d; }
function createTooltip(btn){ const t=document.createElement("div"); t.id="ht-ai-tooltip"; t.textContent=TOOLTIP_TEXT; document.body.appendChild(t); btn.addEventListener("mouseenter",()=>{const r=btn.getBoundingClientRect(); t.style.left=`${r.left+r.width/2}px`; t.style.top=`${r.top}px`; t.classList.add("show");}); btn.addEventListener("mouseleave",()=>t.classList.remove("show")); }
function createSidebar(){ const p=document.createElement("div"); p.id="ht-ai-panel"; p.innerHTML=`<div id="ht-ai-header"><strong>HackTricksAI Chat</strong><div class="ht-actions"><button id="ht-ai-reset" title="Reset">↺</button><span id="ht-ai-close" title="Close">✖</span></div></div><div id="ht-ai-chat"></div><div id="ht-ai-input"><textarea id="ht-ai-question" placeholder="Type your question…"></textarea><button id="ht-ai-send">Send</button></div>`; document.body.appendChild(p); return p; }
})();

View File

@ -1,3 +1,26 @@
/*
Polyfill so requestIdleCallback works everywhere (IE 11/Safari)
*/
if (typeof window.requestIdleCallback !== "function") {
window.requestIdleCallback = function (cb) {
const start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
window.cancelIdleCallback = window.clearTimeout;
}
/*
search.js
*/
"use strict";
window.search = window.search || {};
(function search(search) {
@ -471,64 +494,58 @@ window.search = window.search || {};
showResults(true);
}
(async function loadSearchIndex(lang = window.lang || 'en') {
/* ───────── paths ───────── */
const branch = lang === 'en' ? 'master' : lang;
const baseRemote = `https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/${branch}`;
const remoteJson = `${baseRemote}/searchindex.json`;
const remoteJs = `${baseRemote}/searchindex.js`;
const localJson = './searchindex.json';
const localJs = './searchindex.js';
const TIMEOUT_MS = 5_000;
/* ───────── helpers ───────── */
const fetchWithTimeout = (url, opt = {}) =>
Promise.race([
fetch(url, opt),
new Promise((_, r) => setTimeout(() => r(new Error('timeout')), TIMEOUT_MS))
]);
const loadScript = src =>
new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
(async function loadSearchIndex(lang = window.lang || "en") {
const branch = lang === "en" ? "master" : lang;
const rawUrl =
`https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/${branch}/searchindex.js`;
const localJs = "/searchindex.js";
const TIMEOUT_MS = 10_000;
const injectScript = (src) =>
new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.onload = () => resolve(src);
s.onerror = (e) => reject(e);
document.head.appendChild(s);
});
/* ───────── 1. remote JSON ───────── */
});
try {
const r = await fetchWithTimeout(remoteJson);
if (!r.ok) throw new Error(r.status);
return init(await r.json());
} catch (e) {
console.warn('Remote JSON failed →', e);
/* 1 — download raw JS from GitHub */
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
const res = await fetch(rawUrl, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
/* 2 — wrap in a Blob so the browser sees application/javascript */
const code = await res.text();
const blobUrl = URL.createObjectURL(
new Blob([code], { type: "application/javascript" })
);
/* 3 — execute it */
await injectScript(blobUrl);
/* PATCH
heavy parsing now deferred to idle time
*/
requestIdleCallback(() => init(window.search));
return; // ✔ UI remains responsive
} catch (eRemote) {
console.warn("Remote JS failed →", eRemote);
}
/* ───────── 2. remote JS ───────── */
/* ───────── fallback: local copy ───────── */
try {
await loadScript(remoteJs);
return init(window.search);
} catch (e) {
console.warn('Remote JS failed →', e);
}
/* ───────── 3. local JSON ───────── */
try {
const r = await fetch(localJson);
if (!r.ok) throw new Error(r.status);
return init(await r.json());
} catch (e) {
console.warn('Local JSON failed →', e);
}
/* ───────── 4. local JS ───────── */
try {
await loadScript(localJs);
return init(window.search);
} catch (e) {
console.error('Local JS failed →', e);
await injectScript(localJs);
/* ───────────── PATCH ───────────── */
requestIdleCallback(() => init(window.search));
return;
} catch (eLocal) {
console.error("Local JS failed →", eLocal);
}
})();