Skip to main content
Glama
scan_wordlist.ts4.67 kB
/* Scan endpoints using a wordlist with OPTIONS (safe). Env: API_BASE, AUTH_*, WORDLIST (path, optional), OUTPUT (path, optional), PROBE_DELAY_MS (default 800) */ import { loadLocalEnv } from './_load_env.js'; loadLocalEnv(); import fs from 'node:fs'; import path from 'node:path'; const BASE = process.env['API_BASE'] ?? ''; const AUTH_MODE = (process.env['AUTH_MODE'] ?? 'none').toLowerCase(); const AUTH_TOKEN = process.env['AUTH_TOKEN'] ?? ''; const AUTH_HEADER = process.env['AUTH_HEADER'] ?? 'Authorization'; const AUTH_QUERY_KEY = process.env['AUTH_QUERY_KEY'] ?? 'api_key'; const WORDLIST = process.env['WORDLIST'] ?? ''; const OUTPUT = process.env['OUTPUT'] ?? ''; const BASE_DELAY = Number(process.env['PROBE_DELAY_MS'] ?? '800'); if (!BASE) { console.error('Missing API_BASE'); process.exit(1); } function sleep(ms: number){ return new Promise(r=>setTimeout(r,ms)); } function parseRetryAfter(v: string | null): number | undefined { if (!v) return; const s = Number(v); if (Number.isFinite(s)) return s*1000; const ts = Date.parse(v); if (Number.isFinite(ts)) { const d=ts-Date.now(); return d>0?d:undefined; } } function joinUrl(base: string, path0: string){ const b = base.endsWith('/')? base.slice(0,-1): base; const p = path0.startsWith('/')? path0: `/${path0}`; return `${b}${p}`; } function applyAuth(u: string, headers: Record<string,string>): string { switch (AUTH_MODE) { case 'bearer': headers['Authorization'] = `Bearer ${AUTH_TOKEN}`; break; case 'header': headers[AUTH_HEADER] = AUTH_TOKEN; break; case 'basic': headers['Authorization'] = `Basic ${AUTH_TOKEN}`; break; case 'query': { try { const x = new URL(u); x.searchParams.set(AUTH_QUERY_KEY, AUTH_TOKEN); return x.toString(); } catch { return u + (u.includes('?')?'&':'?') + `${AUTH_QUERY_KEY}=${encodeURIComponent(AUTH_TOKEN)}`; } } default: break; } return u; } function loadWords(): string[] { const defaults = ` status health docs openapi users user accounts devices policies apps orders products search auth login logout metrics info v1 v2 `; // small, non-aggressive default if (!WORDLIST) return defaults.split(/\r?\n/).map(s=>s.trim()).filter(Boolean); const p = path.resolve(process.cwd(), WORDLIST); if (!fs.existsSync(p)) return []; const text = fs.readFileSync(p, 'utf8'); return text.split(/\r?\n/).map(s=>s.trim()).filter(Boolean); } async function probeOptions(p: string): Promise<{ status: number; allow?: string | null; retryAfter?: number }>{ const headers: Record<string,string> = { 'Content-Type': 'application/json' }; const u0 = joinUrl(BASE, p); const u = applyAuth(u0, headers); const res = await fetch(u, { method: 'OPTIONS', headers } as any); return { status: res.status, allow: res.headers.get('allow'), retryAfter: parseRetryAfter(res.headers.get('retry-after')) }; } async function main(){ const words = loadWords(); const results: any[] = []; let nextReady = Date.now(); let delay = BASE_DELAY; const MAX_DELAY = 20000; const MIN_DELAY = Math.max(300, BASE_DELAY); for (const w of words){ const now = Date.now(); if (now<nextReady) await sleep(nextReady-now); const path1 = `/${w}`; const r = await probeOptions(path1).catch(()=>({ status: -1 } as any)); if (r.status === 429) { if (typeof r.retryAfter === 'number') delay = Math.min(MAX_DELAY, r.retryAfter); else delay = Math.min(MAX_DELAY, Math.floor(delay*1.8)); nextReady = Date.now()+delay; } else { delay = Math.max(MIN_DELAY, Math.floor(delay*0.9)); nextReady = Date.now()+delay; } results.push({ path: path1, options: r.status, allow: r.allow }); // also try with trailing slash if (Date.now()<nextReady) await sleep(nextReady-Date.now()); const path2 = `/${w}/`; const r2 = await probeOptions(path2).catch(()=>({ status: -1 } as any)); if (r2.status === 429) { if (typeof r2.retryAfter === 'number') delay = Math.min(MAX_DELAY, r2.retryAfter); else delay = Math.min(MAX_DELAY, Math.floor(delay*1.8)); nextReady = Date.now()+delay; } else { delay = Math.max(MIN_DELAY, Math.floor(delay*0.9)); nextReady = Date.now()+delay; } results.push({ path: path2, options: r2.status, allow: r2.allow }); } const report = { base: BASE, count: results.length, results, timestamp: new Date().toISOString() }; if (OUTPUT){ const p = path.resolve(process.cwd(), OUTPUT); // @ts-ignore fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(report, null, 2)); console.log(p); } else { console.log(JSON.stringify(report, null, 2)); } } main().catch((e)=>{ console.error(e); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ghively/API2MCP-creator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server