#!/usr/bin/env node /** * Move2Link resolver * * Mode token: * node move2link.js --token "eyJ..." * node move2link.js --token-file token.txt * * Mode auto (Fgsi WAF Session): * node move2link.js --url "https://move2link.co/4b08f3c" * */ import { readFileSync } from 'node:fs'; import { stdin as input } from 'node:process'; const API_BASE = 'https://api.move2link.com/api/v1'; const JWT_RE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/; const FGSI_KEY = 'AMBIL-KEY-DI-https://fgsi.dpdns.org'; const FGSI_WAF_URL = 'https://fgsi.dpdns.org/api/tools/cfclearance/waf-session'; function parseArgs() { const args = process.argv.slice(2); const get = (flag) => { const i = args.indexOf(flag); return i !== -1 && args[i + 1] ? args[i + 1] : null; }; return { url: get('--url') || args.find((a) => a.startsWith('http')) || null, token: get('--token'), cookie: get('--cookie'), tokenFile: get('--token-file'), stdin: args.includes('--stdin'), }; } function asJwt(raw) { if (!raw) return null; let t = String(raw).trim(); t = t.replace(/^SESSION=/i, ''); t = t.replace(/^["']|["']$/g, ''); return JWT_RE.test(t) ? t : null; } function extractTokenFromCookie(cookie) { if (!cookie) return null; const m = String(cookie).match(/(?:^|;\s*)SESSION=([^;]+)/i); if (m) return asJwt(decodeURIComponent(m[1].trim())); return asJwt(cookie); } function jwtProgress(token) { try { const part = token.split('.')[1]; if (!part) return null; const pad = '='.repeat((4 - (part.length % 4)) % 4); const b64 = part.replace(/-/g, '+').replace(/_/g, '/') + pad; const json = Buffer.from(b64, 'base64').toString('utf8'); const p = JSON.parse(json); const step = Number(p.step); const maxStep = Number(p.max_step); if (!Number.isFinite(step) || !Number.isFinite(maxStep) || maxStep < 1) return null; return { step, maxStep }; } catch { return null; } } async function apiCall(path, method, token) { const endpoint = API_BASE + path; const res = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', Origin: 'https://move2link.co', Referer: 'https://move2link.co/', }, body: JSON.stringify({ token, csrf_token: 'x', imps: [] }), }); const text = await res.text(); let parsed = {}; try { parsed = JSON.parse(text); } catch {} if (!res.ok) { throw new Error(path + ' HTTP ' + res.status + ': ' + text.slice(0, 400)); } return parsed.data ?? parsed ?? {}; } async function unlock(token) { let cur = token; const steps = []; for (;;) { const progress = jwtProgress(cur); if (!progress) throw new Error('JWT invalid / tidak ada step & max_step'); steps.push({ step: progress.step, maxStep: progress.maxStep }); if (progress.step >= progress.maxStep - 1) break; const data = await apiCall('/views/track', 'PUT', cur); const next = (data.token || '').trim(); if (!next || !JWT_RE.test(next)) { throw new Error('Track gagal. Response: ' + JSON.stringify(data).slice(0, 300)); } cur = next; } const data = await apiCall('/views/finalize', 'POST', cur); const finalUrl = String(data.redirect_url || data.url || '').trim(); if (!/^https?:\/\//i.test(finalUrl)) { throw new Error('Finalize gagal. Response: ' + JSON.stringify(data).slice(0, 400)); } return { finalUrl, steps }; } /** Auto: Fgsi WAF Session → ambil SESSION JWT → unlock */ async function resolveFromUrl(url) { const res = await fetch(`${FGSI_WAF_URL}?apikey=${FGSI_KEY}&url=${encodeURIComponent(url)}`); const json = await res.json(); if (!json.status || !json.data?.cookies) { throw new Error('Fgsi WAF Session gagal: ' + JSON.stringify(json).slice(0, 300)); } const sessionCookie = json.data.cookies.find((c) => c.name === 'SESSION'); if (!sessionCookie) { throw new Error('SESSION cookie tidak ada di response Fgsi'); } const token = asJwt(sessionCookie.value); if (!token) throw new Error('SESSION value bukan JWT valid'); return token; } async function readStdin() { const chunks = []; for await (const chunk of input) chunks.push(chunk); return Buffer.concat(chunks).toString('utf8').trim(); } async function resolveToken(opts) { if (opts.token) { const t = asJwt(opts.token); if (t) return { token: t, source: 'token' }; } if (opts.cookie) { const t = extractTokenFromCookie(opts.cookie); if (t) return { token: t, source: 'cookie' }; } if (opts.tokenFile) { const raw = readFileSync(opts.tokenFile, 'utf8'); const t = asJwt(raw) || extractTokenFromCookie(raw); if (t) return { token: t, source: 'token-file' }; } if (opts.stdin) { const raw = await readStdin(); const t = asJwt(raw) || extractTokenFromCookie(raw); if (t) return { token: t, source: 'stdin' }; } if (opts.url) { const t = await resolveFromUrl(opts.url); return { token: t, source: 'fgsi+url' }; } return null; } async function main() { const opts = parseArgs(); const t0 = Date.now(); const out = { ok: false, finalUrl: null, durationMs: 0, durationSec: 0, steps: [], source: null, input: opts.url || null, timestamp: new Date().toISOString(), }; try { const resolved = await resolveToken(opts); if (!resolved) { throw new Error( 'Butuh --token / --token-file / --cookie / --stdin / --url. ' + 'Untuk auto full: node move2link.js --url https://move2link.co/xxxx' ); } out.source = resolved.source; const { finalUrl, steps } = await unlock(resolved.token); out.ok = true; out.finalUrl = finalUrl; out.steps = steps; } catch (err) { out.error = err.message || String(err); } const ms = Date.now() - t0; out.durationMs = ms; out.durationSec = Math.round((ms / 1000) * 100) / 100; if (!out.input) delete out.input; if (!out.error) delete out.error; if (!out.ok) { delete out.finalUrl; delete out.steps; } console.log(JSON.stringify(out, null, 2)); process.exit(out.ok ? 0 : 1); } main();