import { connect } from "cloudflare:sockets"; let serviceName = ""; let APP_DOMAIN = ""; let prxIP = ""; let cachedPrxList = []; let memoryProxyCache = { data: [], lastUpdated: 0, ttl: 10 * 60 * 1000 }; const horse = "dHJvamFu"; const flash = "dm1lc3M="; const neko = "dmxlc3M="; const v2 = "djJyYXk="; const PORTS = [443, 80]; const PROTOCOLS = [atob(horse), atob(flash), atob(neko), "ss"]; const SUB_PAGE_URL = "https://foolvpn.web.id/nautica"; const DEFAULT_CONFIG = { DNS_TYPE: "DOH", DOH_URL: "https://cloudflare-dns.com/dns-query", DNS_SERVER_ADDRESS: "1.1.1.1", DNS_SERVER_PORT: "53", UDP_MODE: "RELAY", UDP_RELAY_HOST: "wsudprelay.up.railway.app", UDP_RELAY_PORT: "443", KV_PRX_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json", PRX_BANK_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt", REVERSE_PRX_TARGET: "", RAW_KV_JSON_DATA: "", RAW_KV_TXT_DATA: "", TCP_OPTIMIZE_MODE: "OFF", ZERO_COPY_MODE: "ON", EARLY_FLUSH_MODE: "ON", STREAM_BUFFER_MODE: "HIGH_THROUGHPUT", DNS_HISTORY: [ { type: "DOH", label: "Cloudflare DoH", dohUrl: "https://cloudflare-dns.com/dns-query", isDefault: true }, { type: "DOH", label: "Google DoH", dohUrl: "https://dns.google/dns-query", isDefault: true }, { type: "DOH", label: "NextDNS DoH", dohUrl: "https://dns.nextdns.io/dns-query", isDefault: true }, { type: "UDP", label: "Google DNS (8.8.8.8:53)", addr: "8.8.8.8", port: "53", isDefault: true }, { type: "UDP", label: "Cloudflare DNS (1.1.1.1:53)", addr: "1.1.1.1", port: "53", isDefault: true } ], RELAY_HISTORY: [ { label: "Railway WS Relay (wsudprelay.up.railway.app:443)", host: "wsudprelay.up.railway.app", port: "443", isDefault: true } ], PRX_BANK_HISTORY: [ { label: "Default Proxy Bank TXT", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt", isDefault: true } ], KV_PRX_HISTORY: [ { label: "Default Proxy JSON URL", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json", isDefault: true } ], CUSTOM_PRX_LIST: [] }; const CONVERTER_URL = "https://api.foolvpn.web.id/convert"; const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; const CORS_HEADER_OPTIONS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; const SALT_A1 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5X0xlbmd0aA=="); const SALT_A2 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2VfTGVuZ3Ro"); const SALT_A3 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5"); const SALT_A4 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2U="); const SALT_B1 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY="); const SALT_B2 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY="); const SALT_B3 = atob("QUVBRCBSZXNwIEhlYWRlciBLZXk="); const SALT_B4 = atob("QUVBRCBSZXNwIEhlYWRlciBJVg=="); const RELAY_MAGIC = new TextEncoder().encode("VLRLY004"); const RELAY_MODE_PACKET_UDP = 0x03; const ATYP_IPV4 = 0x01; const ATYP_DOMAIN = 0x02; const ATYP_IPV6 = 0x03; function concatBytes(...parts) { let len = 0; for (const p of parts) if (p) len += p.byteLength; const out = new Uint8Array(len); let off = 0; for (const p of parts) { if (p && p.byteLength) { out.set(new Uint8Array(p), off); off += p.byteLength; } } return out; } function parseRelayEndpoint(bytes, offset = 0) { const b = new Uint8Array(bytes); if (b.byteLength < offset + 3) return null; const port = (b[offset] << 8) | b[offset + 1]; const atyp = b[offset + 2]; let p = offset + 3; if (atyp === ATYP_IPV4) { if (b.byteLength < p + 4) return null; return { address: Array.from(b.subarray(p, p + 4)).join("."), port, addressType: atyp, next: p + 4 }; } if (atyp === ATYP_DOMAIN) { if (b.byteLength < p + 1) return null; const len = b[p++]; if (b.byteLength < p + len) return null; return { address: new TextDecoder("utf-8").decode(b.subarray(p, p + len)), port, addressType: atyp, next: p + len }; } if (atyp === ATYP_IPV6) { if (b.byteLength < p + 16) return null; const parts = []; for (let i = 0; i < 16; i += 2) parts.push(((b[p + i] << 8) | b[p + i + 1]).toString(16)); return { address: parts.join(":"), port, addressType: atyp, next: p + 16 }; } return null; } function encodeRelayEndpoint(address, port) { const p = Number(port); const head = new Uint8Array([(p >>> 8) & 0xff, p & 0xff]); if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(String(address))) { const ipBytes = new Uint8Array(String(address).split(".").map(Number)); const out = new Uint8Array(head.length + 1 + ipBytes.length); out.set(head, 0); out[head.length] = ATYP_IPV4; out.set(ipBytes, head.length + 1); return out; } if (String(address).includes(":")) { const parts = String(address).split(":"); const out = new Uint8Array(head.length + 1 + 16); out.set(head, 0); out[head.length] = ATYP_IPV6; for (let i = 0; i < 8; i++) { const val = parseInt(parts[i] || "0", 16); out[head.length + 1 + (i * 2)] = (val >>> 8) & 0xff; out[head.length + 1 + (i * 2) + 1] = val & 0xff; } return out; } const d = new TextEncoder().encode(String(address)); const out = new Uint8Array(head.length + 2 + d.length); out.set(head, 0); out[head.length] = ATYP_DOMAIN; out[head.length + 1] = d.length; out.set(d, head.length + 2); return out; } function parseSocksAddress(bytes, cursor = 0) { const b = new Uint8Array(bytes); if (b.byteLength < cursor + 1) return null; const atyp = b[cursor++]; let address = ""; if (atyp === 0x01) { if (b.byteLength < cursor + 6) return null; address = Array.from(b.subarray(cursor, cursor + 4)).join("."); cursor += 4; } else if (atyp === 0x03) { if (b.byteLength < cursor + 1) return null; const len = b[cursor++]; if (!len || b.byteLength < cursor + len + 2) return null; address = new TextDecoder("utf-8").decode(b.subarray(cursor, cursor + len)); cursor += len; } else if (atyp === 0x04) { if (b.byteLength < cursor + 18) return null; const parts = []; for (let i = 0; i < 16; i += 2) parts.push(((b[cursor + i] << 8) | b[cursor + i + 1]).toString(16)); address = parts.join(":"); cursor += 16; } else return null; const port = (b[cursor] << 8) | b[cursor + 1]; cursor += 2; return { address, port, addressType: atyp, next: cursor }; } function encodeSocksAddress(endpoint) { const address = endpoint.address; const port = Number(endpoint.port); let addr; if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(String(address))) { addr = concatBytes(new Uint8Array([0x01]), new Uint8Array(String(address).split(".").map(Number))); } else if (String(address).includes(":")) { const parts = String(address).split(":"); const b = new Uint8Array(16); for (let i = 0; i < 8; i++) { const val = parseInt(parts[i] || "0", 16); b[i * 2] = (val >>> 8) & 0xff; b[i * 2 + 1] = val & 0xff; } addr = concatBytes(new Uint8Array([0x04]), b); } else { const d = new TextEncoder().encode(String(address)); addr = concatBytes(new Uint8Array([0x03, d.byteLength]), d); } return concatBytes(addr, new Uint8Array([(port >>> 8) & 0xff, port & 0xff])); } function encodeTrojanUdpPacket(endpoint, payload) { const p = new Uint8Array(payload); return concatBytes( encodeSocksAddress(endpoint), new Uint8Array([(p.byteLength >>> 8) & 0xff, p.byteLength & 0xff, 0x0d, 0x0a]), p ); } function parseProxy(proxyStr) { if (!proxyStr) return null; const atSplit = proxyStr.split("@"); const hostPort = atSplit[0]; const auth = atSplit[1] || null; let [host, port] = hostPort.split(":"); port = parseInt(port, 10) || 443; let user = null; let pass = null; if (auth && auth.includes(":")) { const authParts = auth.split(":"); user = authParts[0]; pass = authParts.slice(1).join(":"); } return { host, port, user, pass }; } async function getConfig(env) { let kvConfig = {}; if (env && env.CONFIG_KV) { try { const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" }); if (stored) kvConfig = stored; } catch (e) {} } return { DNS_TYPE: kvConfig.DNS_TYPE || (env && env.DNS_TYPE) || DEFAULT_CONFIG.DNS_TYPE, DOH_URL: kvConfig.DOH_URL || (env && env.DOH_URL) || DEFAULT_CONFIG.DOH_URL, DNS_SERVER_ADDRESS: kvConfig.DNS_SERVER_ADDRESS || (env && env.DNS_SERVER_ADDRESS) || DEFAULT_CONFIG.DNS_SERVER_ADDRESS, DNS_SERVER_PORT: parseInt(kvConfig.DNS_SERVER_PORT || (env && env.DNS_SERVER_PORT) || DEFAULT_CONFIG.DNS_SERVER_PORT), UDP_MODE: kvConfig.UDP_MODE || (env && env.UDP_MODE) || DEFAULT_CONFIG.UDP_MODE, UDP_RELAY_HOST: kvConfig.UDP_RELAY_HOST || (env && env.UDP_RELAY_HOST) || DEFAULT_CONFIG.UDP_RELAY_HOST, UDP_RELAY_PORT: parseInt(kvConfig.UDP_RELAY_PORT || (env && env.UDP_RELAY_PORT) || DEFAULT_CONFIG.UDP_RELAY_PORT), KV_PRX_URL: kvConfig.KV_PRX_URL || (env && env.KV_PRX_URL) || DEFAULT_CONFIG.KV_PRX_URL, PRX_BANK_URL: kvConfig.PRX_BANK_URL || (env && env.PRX_BANK_URL) || DEFAULT_CONFIG.PRX_BANK_URL, REVERSE_PRX_TARGET: kvConfig.REVERSE_PRX_TARGET || DEFAULT_CONFIG.REVERSE_PRX_TARGET, RAW_KV_JSON_DATA: kvConfig.RAW_KV_JSON_DATA || DEFAULT_CONFIG.RAW_KV_JSON_DATA, RAW_KV_TXT_DATA: kvConfig.RAW_KV_TXT_DATA || DEFAULT_CONFIG.RAW_KV_TXT_DATA, TCP_OPTIMIZE_MODE: kvConfig.TCP_OPTIMIZE_MODE !== undefined ? kvConfig.TCP_OPTIMIZE_MODE : DEFAULT_CONFIG.TCP_OPTIMIZE_MODE, ZERO_COPY_MODE: kvConfig.ZERO_COPY_MODE !== undefined ? kvConfig.ZERO_COPY_MODE : DEFAULT_CONFIG.ZERO_COPY_MODE, EARLY_FLUSH_MODE: kvConfig.EARLY_FLUSH_MODE !== undefined ? kvConfig.EARLY_FLUSH_MODE : DEFAULT_CONFIG.EARLY_FLUSH_MODE, STREAM_BUFFER_MODE: kvConfig.STREAM_BUFFER_MODE !== undefined ? kvConfig.STREAM_BUFFER_MODE : DEFAULT_CONFIG.STREAM_BUFFER_MODE, DNS_HISTORY: kvConfig.DNS_HISTORY || DEFAULT_CONFIG.DNS_HISTORY, RELAY_HISTORY: kvConfig.RELAY_HISTORY || DEFAULT_CONFIG.RELAY_HISTORY, PRX_BANK_HISTORY: kvConfig.PRX_BANK_HISTORY || DEFAULT_CONFIG.PRX_BANK_HISTORY, KV_PRX_HISTORY: kvConfig.KV_PRX_HISTORY || DEFAULT_CONFIG.KV_PRX_HISTORY, CUSTOM_PRX_LIST: kvConfig.CUSTOM_PRX_LIST || DEFAULT_CONFIG.CUSTOM_PRX_LIST }; } async function getKVPrxList(config) { if (config.RAW_KV_JSON_DATA && config.RAW_KV_JSON_DATA.trim() !== "") { try { return JSON.parse(config.RAW_KV_JSON_DATA); } catch (e) {} } if (!config.KV_PRX_URL) throw new Error("No URL Provided!"); const kvPrx = await fetch(config.KV_PRX_URL); return kvPrx.status == 200 ? await kvPrx.json() : {}; } async function getPrxList(config) { let kvParsedList = []; let urlParsedList = []; if (config.RAW_KV_TXT_DATA && config.RAW_KV_TXT_DATA.trim() !== "") { const lines = config.RAW_KV_TXT_DATA.split("\n").filter(Boolean); kvParsedList = lines.map((entry) => { const [prxIP, prxPort, country, org] = entry.split(","); return { prxIP: prxIP ? prxIP.trim() : "Unknown", prxPort: prxPort ? prxPort.trim() : "Unknown", country: country ? country.trim().toUpperCase() : "Unknown", org: org ? org.trim() : "Unknown Org", }; }).filter(Boolean); } if (config.PRX_BANK_URL) { try { const prxBank = await fetch(config.PRX_BANK_URL); if (prxBank.status == 200) { const text = (await prxBank.text()) || ""; const prxString = text.split("\n").filter(Boolean); urlParsedList = prxString.map((entry) => { const [prxIP, prxPort, country, org] = entry.split(","); return { prxIP: prxIP ? prxIP.trim() : "Unknown", prxPort: prxPort ? prxPort.trim() : "Unknown", country: country ? country.trim().toUpperCase() : "Unknown", org: org ? org.trim() : "Unknown Org", }; }).filter(Boolean); } } catch (e) {} } let customList = config.CUSTOM_PRX_LIST || []; const existingIdentifiers = new Set([ ...kvParsedList.map(p => `${p.prxIP}:${p.prxPort}`), ...customList.map(p => `${p.prxIP}:${p.prxPort}`) ]); const filteredUrlList = urlParsedList.filter(p => !existingIdentifiers.has(`${p.prxIP}:${p.prxPort}`)); cachedPrxList = [...customList, ...kvParsedList, ...filteredUrlList]; return cachedPrxList; } async function checkProxyInternal(host, port, timeoutMs = 800) { const startTime = Date.now(); try { const socket = connect({ hostname: host, port: parseInt(port) || 443 }); await Promise.race([ socket.opened, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)) ]); const latency = Date.now() - startTime; try { socket.close(); } catch (e) {} return { status: "active", latency }; } catch (err) { return { status: "dead", latency: 99999 }; } } async function getBestInMemoryProxy(config) { const now = Date.now(); if (memoryProxyCache.data.length > 0 && (now - memoryProxyCache.lastUpdated < memoryProxyCache.ttl)) { const randomCached = memoryProxyCache.data[Math.floor(Math.random() * memoryProxyCache.data.length)]; return `${randomCached.prxIP}:${randomCached.prxPort}`; } const list = await getPrxList(config); if (list.length === 0) return "172.232.249.224:2053"; const prioritized = list.filter(p => p.country === "SG" || p.country === "ID"); const targetPool = prioritized.length > 0 ? prioritized : list; const samples = targetPool.sort(() => 0.5 - Math.random()).slice(0, 6); const testResults = await Promise.all( samples.map(async (p) => { const res = await checkProxyInternal(p.prxIP, parseInt(p.prxPort) || 443, 600); return { ...p, latency: res.latency }; }) ); testResults.sort((a, b) => a.latency - b.latency); const healthyOnly = testResults.filter(p => p.latency < 400); if (healthyOnly.length > 0) { memoryProxyCache.data = healthyOnly; memoryProxyCache.lastUpdated = now; return `${healthyOnly[0].prxIP}:${healthyOnly[0].prxPort}`; } const activeOnly = testResults.filter(p => p.latency < 99999); if (activeOnly.length > 0) { memoryProxyCache.data = activeOnly; memoryProxyCache.lastUpdated = now; return `${activeOnly[0].prxIP}:${activeOnly[0].prxPort}`; } return `${samples[0].prxIP}:${samples[0].prxPort}`; } async function getBestSgProxyInternal(config) { try { const kvPrx = await getKVPrxList(config); if (kvPrx && kvPrx["SG"] && Array.isArray(kvPrx["SG"]) && kvPrx["SG"].length > 0) { const sgPool = kvPrx["SG"]; return sgPool[Math.floor(Math.random() * sgPool.length)]; } } catch (e) {} const list = await getPrxList(config); const sgList = list.filter(p => p.country && p.country.toUpperCase() === "SG"); const targetList = sgList.length > 0 ? sgList : list; if (targetList.length === 0) return "172.232.249.224:2053"; return `${targetList[0].prxIP}:${targetList[0].prxPort}`; } async function reverseWeb(request, target, targetPath) { if (!target || target.trim() === "") target = "example.com"; const targetUrl = new URL(request.url); const targetChunk = target.split(":"); targetUrl.hostname = targetChunk[0]; targetUrl.port = targetChunk[1]?.toString() || "443"; targetUrl.pathname = targetPath || targetUrl.pathname; const modifiedHeaders = new Headers(request.headers); modifiedHeaders.set("Host", targetUrl.hostname); modifiedHeaders.set("X-Forwarded-Host", request.headers.get("Host")); const modifiedRequest = new Request(targetUrl, { method: request.method, headers: modifiedHeaders, body: request.body, redirect: "follow" }); const response = await fetch(modifiedRequest); const newResponse = new Response(response.body, response); for (const [key, value] of Object.entries(CORS_HEADER_OPTIONS)) newResponse.headers.set(key, value); newResponse.headers.set("X-Proxied-By", "Cloudflare Worker"); return newResponse; } export default { async fetch(request, env, ctx) { try { const url = new URL(request.url); APP_DOMAIN = url.hostname; serviceName = APP_DOMAIN.split(".")[0]; const currentConfig = await getConfig(env); if (url.pathname === "/api/get-proxy-list") { const list = await getPrxList(currentConfig); return new Response(JSON.stringify(list), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json", "Cache-Control": "no-store, no-cache, must-revalidate" } }); } if (url.pathname.startsWith("/api/check")) { const target = url.searchParams.get("ip"); if (!target || !target.includes(":")) { return new Response(JSON.stringify({ status: "error", message: "Gunakan parameter ?ip=IP:PORT" }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } }); } const hostPort = target.split("@")[0]; const [ip, port] = hostPort.split(":"); const res = await checkProxyInternal(ip, parseInt(port) || 443, 2500); return new Response(JSON.stringify({ status: res.status, ip, port, latency: res.status === "active" ? `${res.latency}ms` : "timeout" }), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json", "Cache-Control": "no-store, no-cache, must-revalidate" } }); } // FULL HTML UI NAUTICA (DENGAN DYNAMIC CURRENT BROWSER URL) const htmlContent = ` Nautica Worker Dashboard

⚡ DDFATHUVLES ULTRA SPEED

Smart Routing (/MULTI) & Browser-Host Sync Active

▌ BUG SNI (NORMAL / STANDAR)

▌ BUG SNI (REVERSE)

▌ BUG CDN (PROXY PORT)

`; if (url.pathname === "/ui" || url.pathname === "/ui/" || url.pathname === "/") { return new Response(htmlContent, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate", ...CORS_HEADER_OPTIONS }, }); } if (url.pathname === "/api/save-config" && request.method === "POST") { if (!env || !env.CONFIG_KV) { return new Response( JSON.stringify({ success: false, message: "Binding CONFIG_KV belum diset di Cloudflare Worker!" }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } const body = await request.json(); let oldConfig = {}; try { const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" }); if (stored) oldConfig = stored; } catch (e) {} const mergedConfig = { ...DEFAULT_CONFIG, ...oldConfig, ...body }; await env.CONFIG_KV.put("APP_CONFIG", JSON.stringify(mergedConfig)); return new Response( JSON.stringify({ success: true, message: "Pengaturan berhasil disimpan ke KV." }), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } const upgradeHeader = request.headers.get("Upgrade"); if (upgradeHeader === "websocket") { const rawPath = decodeURIComponent(url.pathname); const cleanPath = rawPath.replace(/^\//, ""); if (cleanPath.toUpperCase() === "MULTI") return await websocketHandler(request, currentConfig, "MULTI", ""); if (cleanPath.toUpperCase() === "DIRECT") return await websocketHandler(request, currentConfig, "DIRECT", ""); if (cleanPath.toUpperCase() === "SEMUA") { const picked = await getBestInMemoryProxy(currentConfig); return await websocketHandler(request, currentConfig, "PROXY", picked); } const proxyAuthMatch = cleanPath.match(/^([a-zA-Z0-9.-]+:\d+(@[a-zA-Z0-9._~%!$&'()*+,;=-]+:[a-zA-Z0-9._~%!$&'()*+,;=-]+)?)$/); if (proxyAuthMatch) return await websocketHandler(request, currentConfig, "PROXY", proxyAuthMatch[1]); const prxMatch = rawPath.match(/^\/(.+[:=-]\d+(@.+:.+)?)$/); if (prxMatch) return await websocketHandler(request, currentConfig, "PROXY", prxMatch[1]); if (/[a-z]/.test(cleanPath)) { const picked = await getBestSgProxyInternal(currentConfig); return await websocketHandler(request, currentConfig, "PROXY", picked); } } if (url.pathname.startsWith("/sub")) return Response.redirect(SUB_PAGE_URL + `?host=${APP_DOMAIN}`, 301); return await reverseWeb(request, currentConfig.REVERSE_PRX_TARGET); } catch (err) { return new Response(`Error: ${err.toString()}`, { status: 500, headers: CORS_HEADER_OPTIONS }); } } }; async function websocketHandler(request, config, routingMode = "PROXY", assignedProxy = "") { const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); let addressLog = ""; let portLog = ""; const log = (info, event) => console.log(`[${addressLog}:${portLog}] ${info}`, event || ""); const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); let remoteSocketWrapper = { value: null }; let udpRelaySession = null; let isDNS = false; readableWebSocketStream.pipeTo( new WritableStream({ async write(chunk, controller) { if (isDNS && config.DNS_TYPE === "DOH") { return handleDoHOutbound(config.DOH_URL, chunk, webSocket, null, log); } if (remoteSocketWrapper.value) { const writer = remoteSocketWrapper.value.writable.getWriter(); await writer.write(chunk); writer.releaseLock(); return; } if (udpRelaySession) { await udpRelaySession.write(chunk); return; } const protocol = await protocolSniffer(chunk); let protocolHeader; if (protocol === atob(horse)) protocolHeader = readHorseHeader(chunk); else if (protocol === atob(flash)) protocolHeader = await readStreamHeader(chunk); else if (protocol === atob(neko)) protocolHeader = readNekoHeader(chunk); else if (protocol === "ss") protocolHeader = readSsHeader(chunk); else throw new Error("Unknown Protocol!"); addressLog = protocolHeader.addressRemote; portLog = `${protocolHeader.portRemote} -> ${protocolHeader.isUDP ? "UDP" : "TCP"}`; if (protocolHeader.hasError) throw new Error(protocolHeader.message); let responseHeader = protocolHeader.version; if (protocol === atob(flash) && protocolHeader.needsResponse) { responseHeader = await generateStreamResponseHeader( protocolHeader.responseOptions, protocolHeader.encKey, protocolHeader.encIv ); } if (protocolHeader.isUDP) { if (protocolHeader.portRemote === 53 && config.DNS_TYPE === "DOH") { isDNS = true; return handleDoHOutbound(config.DOH_URL, chunk, webSocket, responseHeader, log); } udpRelaySession = new ExactPacketUdpRelay( config, protocol, protocolHeader.addressRemote, protocolHeader.portRemote, webSocket, responseHeader ); await udpRelaySession.init(protocolHeader.rawClientData); return; } handleTCPOutBound( remoteSocketWrapper, protocolHeader.addressRemote, protocolHeader.portRemote, protocolHeader.rawClientData, webSocket, responseHeader, log, config, routingMode, assignedProxy ); }, close() { if (udpRelaySession) udpRelaySession.close(); log(`readableWebSocketStream is closed`); }, abort(reason) { if (udpRelaySession) udpRelaySession.close(); log(`readableWebSocketStream is aborted`, reason); } }) ).catch(err => log("Stream pipe error", err)); return new Response(null, { status: 101, webSocket: client }); } // 1:1 PACKET-UDP ENGINE class ExactPacketUdpRelay { constructor(config, protocol, defaultAddr, defaultPort, clientWs, responseHeader) { this.config = config; this.protocol = protocol; this.defaultAddr = defaultAddr; this.defaultPort = defaultPort; this.clientWs = clientWs; this.responseHeader = responseHeader; this.relayWs = null; this.isReady = false; this.queue = []; this.closed = false; this.inBuffer = new Uint8Array(0); this.outBuffer = new Uint8Array(0); } async init(initialData) { try { const relayHost = (this.config.UDP_RELAY_HOST || "wsudprelay.up.railway.app") .replace(/^https?:\/\//i, "") .replace(/^wss?:\/\//i, "") .replace(/\/+$/, ""); const wsUrl = `https://${relayHost}/`; const resp = await fetch(wsUrl, { headers: { Upgrade: "websocket" } }); const rWs = resp.webSocket; if (!rWs) throw new Error("Failed to connect to Railway relay"); rWs.accept(); this.relayWs = rWs; rWs.send(concatBytes(RELAY_MAGIC, new Uint8Array([RELAY_MODE_PACKET_UDP]))); let gotAck = false; rWs.addEventListener("message", async (event) => { const chunk = new Uint8Array(event.data); if (!gotAck) { if (chunk.byteLength > 0 && chunk[0] === 0) { gotAck = true; this.isReady = true; this.flush(); if (chunk.byteLength > 1) { this.outBuffer = concatBytes(this.outBuffer, chunk.subarray(1)); } } else { this.close(); return; } } else { this.outBuffer = concatBytes(this.outBuffer, chunk); } while (this.outBuffer.byteLength > 0) { const ep = parseRelayEndpoint(this.outBuffer, 0); if (!ep || this.outBuffer.byteLength < ep.next + 2) break; const payloadLen = (this.outBuffer[ep.next] << 8) | this.outBuffer[ep.next + 1]; const totalEnd = ep.next + 2 + payloadLen; if (this.outBuffer.byteLength < totalEnd) break; const payload = this.outBuffer.slice(ep.next + 2, totalEnd); this.outBuffer = this.outBuffer.slice(totalEnd); this.emitToClient(ep, payload); } }); rWs.addEventListener("close", () => this.close()); rWs.addEventListener("error", () => this.close()); if (initialData && initialData.byteLength > 0) { await this.write(initialData); } } catch (e) { this.close(); } } async write(chunk) { if (this.closed) return; this.inBuffer = concatBytes(this.inBuffer, chunk); while (this.inBuffer.byteLength > 0) { let targetAddr = this.defaultAddr; let targetPort = this.defaultPort; let payload = null; let consumed = 0; if (this.protocol === atob(horse)) { const ep = parseSocksAddress(this.inBuffer, 0); if (!ep || this.inBuffer.byteLength < ep.next + 4) break; const len = (this.inBuffer[ep.next] << 8) | this.inBuffer[ep.next + 1]; const end = ep.next + 4 + len; if (this.inBuffer.byteLength < end) break; targetAddr = ep.address; targetPort = ep.port; payload = this.inBuffer.slice(ep.next + 4, end); consumed = end; } else { if (this.inBuffer.byteLength < 2) break; const len = (this.inBuffer[0] << 8) | this.inBuffer[1]; if (this.inBuffer.byteLength < 2 + len) break; payload = this.inBuffer.slice(2, 2 + len); consumed = 2 + len; } this.inBuffer = this.inBuffer.slice(consumed); const epBytes = encodeRelayEndpoint(targetAddr, targetPort); const lenBytes = new Uint8Array([(payload.byteLength >>> 8) & 0xff, payload.byteLength & 0xff]); const packet = concatBytes(epBytes, lenBytes, payload); if (this.isReady && this.relayWs && this.relayWs.readyState === WS_READY_STATE_OPEN) { this.relayWs.send(packet); } else { this.queue.push(packet); } } } emitToClient(ep, payload) { if (this.clientWs.readyState !== WS_READY_STATE_OPEN) return; let packetForClient; if (this.protocol === atob(horse)) { packetForClient = encodeTrojanUdpPacket(ep, payload); } else { const lenBytes = new Uint8Array([(payload.byteLength >>> 8) & 0xff, payload.byteLength & 0xff]); packetForClient = concatBytes(lenBytes, payload); } if (this.responseHeader) { this.clientWs.send(concatBytes(this.responseHeader, packetForClient).buffer); this.responseHeader = null; } else { this.clientWs.send(packetForClient.buffer || packetForClient); } } flush() { while (this.queue.length > 0) { const p = this.queue.shift(); if (this.relayWs && this.relayWs.readyState === WS_READY_STATE_OPEN) { this.relayWs.send(p); } } } close() { if (this.closed) return; this.closed = true; try { this.relayWs?.close(); } catch (e) {} try { safeCloseWebSocket(this.clientWs); } catch (e) {} } } async function handleTCPOutBound( remoteSocket, addressRemote, portRemote, rawClientData, webSocket, responseHeader, log, config, routingMode, assignedProxy ) { async function connectAndWrite(address, port, timeoutMs = 450) { const tcpSocket = connect({ hostname: address, port: parseInt(port, 10) }); remoteSocket.value = tcpSocket; if (timeoutMs > 0) { await Promise.race([ tcpSocket.opened, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)) ]); } if (rawClientData && rawClientData.byteLength > 0) { const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); writer.releaseLock(); } return tcpSocket; } async function connectViaHttpProxy(parsedPrx, targetHost, targetPort) { const tcpSocket = connect({ hostname: parsedPrx.host, port: parsedPrx.port }); remoteSocket.value = tcpSocket; const writer = tcpSocket.writable.getWriter(); let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`; if (parsedPrx.user && parsedPrx.pass) { connectReq += `Proxy-Authorization: Basic ${btoa(`${parsedPrx.user}:${parsedPrx.pass}`)}\r\n`; } connectReq += `User-Agent: Cloudflare-Worker\r\nProxy-Connection: Keep-Alive\r\n\r\n`; await writer.write(new TextEncoder().encode(connectReq)); const reader = tcpSocket.readable.getReader(); const { value: resChunk } = await reader.read(); reader.releaseLock(); if (!new TextDecoder().decode(resChunk || new Uint8Array(0)).includes(" 200 ")) { throw new Error("Proxy tunnel failed"); } if (rawClientData && rawClientData.byteLength > 0) await writer.write(rawClientData); writer.releaseLock(); return tcpSocket; } async function retryProxyFallback() { try { const backupPrx = assignedProxy || (await getBestSgProxyInternal(config)); const parsed = parseProxy(backupPrx); if (!parsed) return safeCloseWebSocket(webSocket); let tcpSocket = (parsed.user && parsed.pass) ? await connectViaHttpProxy(parsed, addressRemote, portRemote) : await connectAndWrite(parsed.host, parsed.port, 0); tcpSocket.closed.catch(() => {}).finally(() => safeCloseWebSocket(webSocket)); remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log); } catch (e) { safeCloseWebSocket(webSocket); } } if (routingMode === "PROXY" && assignedProxy) { const parsedPrx = parseProxy(assignedProxy); if (parsedPrx && parsedPrx.user && parsedPrx.pass) { try { const tcpSocket = await connectViaHttpProxy(parsedPrx, addressRemote, portRemote); remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log); return; } catch (e) { safeCloseWebSocket(webSocket); return; } } } try { const timeoutLimit = (routingMode === "MULTI") ? 450 : 0; const tcpSocket = await connectAndWrite(addressRemote, portRemote, timeoutLimit); remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, (routingMode === "MULTI" || routingMode === "PROXY") ? retryProxyFallback : null, log); } catch (err) { if (routingMode === "MULTI" || routingMode === "PROXY") await retryProxyFallback(); else safeCloseWebSocket(webSocket); } } async function remoteSocketToWSZeroCopy(remoteSocket, webSocket, responseHeader, retry, log) { let header = responseHeader; let hasIncomingData = false; await remoteSocket.readable.pipeTo( new WritableStream({ write(chunk) { hasIncomingData = true; if (webSocket.readyState !== WS_READY_STATE_OPEN) return; if (header && header.byteLength > 0) { const combined = new Uint8Array(header.byteLength + chunk.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(chunk), header.byteLength); webSocket.send(combined.buffer); header = null; } else { webSocket.send(chunk); } }, close() {}, abort() {} }) ).catch(() => safeCloseWebSocket(webSocket)); if (!hasIncomingData && retry) retry(); } async function handleDoHOutbound(dohUrl, dataChunk, webSocket, responseHeader, log) { try { const dohResponse = await fetch(dohUrl, { method: "POST", headers: { "Content-Type": "application/dns-message", "Accept": "application/dns-message" }, body: dataChunk, }); if (dohResponse.ok && webSocket.readyState === WS_READY_STATE_OPEN) { const dnsResponseBuffer = await dohResponse.arrayBuffer(); if (responseHeader) webSocket.send(await new Blob([responseHeader, dnsResponseBuffer]).arrayBuffer()); else webSocket.send(dnsResponseBuffer); } } catch (e) {} } function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; return new ReadableStream({ start(controller) { webSocketServer.addEventListener("message", (event) => { if (readableStreamCancel) return; controller.enqueue(event.data); }); webSocketServer.addEventListener("close", () => { safeCloseWebSocket(webSocketServer); if (readableStreamCancel) return; controller.close(); }); webSocketServer.addEventListener("error", (err) => controller.error(err)); const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); if (error) controller.error(error); else if (earlyData) controller.enqueue(earlyData); }, pull(controller) {}, cancel(reason) { if (readableStreamCancel) return; readableStreamCancel = true; safeCloseWebSocket(webSocketServer); }, }); } function safeCloseWebSocket(socket) { try { if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) socket.close(); } catch (error) {} } async function protocolSniffer(buffer) { if (buffer.byteLength >= 62) { const horseDelimiter = new Uint8Array(buffer.slice(56, 60)); if (horseDelimiter[0] === 0x0d && horseDelimiter[1] === 0x0a) { if (horseDelimiter[2] === 0x01 || horseDelimiter[2] === 0x03 || horseDelimiter[2] === 0x7f) { if (horseDelimiter[3] === 0x01 || horseDelimiter[3] === 0x03 || horseDelimiter[3] === 0x04) return atob(horse); } } } if (buffer.byteLength >= 18) { const version = new Uint8Array(buffer.slice(0, 1))[0]; if (version === 0) { const protocolUuid = new Uint8Array(buffer.slice(1, 17)); if (arrayBufferToHex(protocolUuid).match(/^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i)) return atob(neko); } } if (buffer.byteLength >= 42) { const firstByte = new Uint8Array(buffer.slice(0, 1))[0]; if (firstByte === 0x01 || firstByte === 0x03 || firstByte === 0x04) return "ss"; return atob(flash); } return "ss"; } async function generateStreamResponseHeader(responseOptions, encKey, encIv) { try { const key = (await sha256(encKey)).slice(0, 16); const lengthKey = (await kdf(key, [SALT_B1])).slice(0, 16); const lengthIv = (await kdf(key, [SALT_B2])).slice(0, 12); const lengthData = new Uint8Array([0, 4]); const encryptedLength = await aesGcmEncrypt(lengthKey, lengthIv, lengthData, new Uint8Array(0)); const headerPayload = new Uint8Array([responseOptions[0], 0x00, 0x00, 0x00]); const payloadKey = (await kdf(key, [SALT_B3])).slice(0, 16); const payloadIv = (await kdf(key, [SALT_B4])).slice(0, 12); const encryptedPayload = await aesGcmEncrypt(payloadKey, payloadIv, headerPayload, new Uint8Array(0)); const response = new Uint8Array(encryptedLength.length + encryptedPayload.length); response.set(encryptedLength, 0); response.set(encryptedPayload, encryptedLength.length); return response; } catch (e) { return new Uint8Array(0); } } async function readStreamHeader(buffer) { try { const uuidString = "00000000-0000-0000-0000-000000000000"; const uuidBytes = new Uint8Array(uuidString.replace(/-/g, "").match(/.{1,2}/g).map(byte => parseInt(byte, 16))); const authKey = await md5(uuidBytes, new TextEncoder().encode(atob("YzQ4NjE5ZmUtOGYwMi00OWUwLWI5ZTktZWRmNzYzZTE3ZTIx"))); const authId = new Uint8Array(buffer.slice(0, 16)); const encryptedLength = new Uint8Array(buffer.slice(16, 34)); const nonce = new Uint8Array(buffer.slice(34, 42)); const lengthKey = (await kdf(authKey, [SALT_A1, authId, nonce])).slice(0, 16); const lengthIv = (await kdf(authKey, [SALT_A2, authId, nonce])).slice(0, 12); const lengthBytes = await aesGcmDecrypt(lengthKey, lengthIv, encryptedLength, authId); const headerLength = (lengthBytes[0] << 8) | lengthBytes[1]; const encryptedHeader = new Uint8Array(buffer.slice(42, 42 + headerLength + 16)); const payloadKey = (await kdf(authKey, [SALT_A3, authId, nonce])).slice(0, 16); const payloadIv = (await kdf(authKey, [SALT_A4, authId, nonce])).slice(0, 12); const headerPayload = await aesGcmDecrypt(payloadKey, payloadIv, encryptedHeader, authId); const view = new DataView(headerPayload.buffer); let offset = 0; const version = view.getUint8(offset++); if (version !== 1) return { hasError: true, message: `Invalid protocol: ${version}` }; const encIv = new Uint8Array(headerPayload.slice(offset, offset + 16)); offset += 16; const encKey = new Uint8Array(headerPayload.slice(offset, offset + 16)); offset += 16; const options = new Uint8Array(headerPayload.slice(offset, offset + 4)); offset += 4; const cmd = view.getUint8(offset++); const isUDP = cmd !== 0x01; const portRemote = view.getUint16(offset, false); offset += 2; const addressType = view.getUint8(offset++); let addressRemote = ""; switch (addressType) { case 1: addressRemote = `${view.getUint8(offset)}.${view.getUint8(offset + 1)}.${view.getUint8(offset + 2)}.${view.getUint8(offset + 3)}`; offset += 4; break; case 2: case 3: const domainLength = view.getUint8(offset++); addressRemote = new TextDecoder().decode(headerPayload.slice(offset, offset + domainLength)); offset += domainLength; break; case 4: const ipv6Parts = []; for (let i = 0; i < 8; i++) ipv6Parts.push(view.getUint16(offset + i * 2, false).toString(16)); addressRemote = ipv6Parts.join(":"); offset += 16; break; default: return { hasError: true, message: `Invalid address type: ${addressType}` }; } const rawDataIndex = 42 + headerLength + 16; return { hasError: false, addressRemote, addressType, portRemote, rawDataIndex, rawClientData: buffer.slice(rawDataIndex), version: new Uint8Array([options[0], 0]), isUDP, needsResponse: true, responseOptions: options, encKey, encIv, }; } catch (e) { return { hasError: true, message: "Stream failed: " + e.message }; } } function readSsHeader(ssBuffer) { const view = new DataView(ssBuffer); const addressType = view.getUint8(0); let addressLength = 0; let addressValueIndex = 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 4: addressLength = 16; const dataView = new DataView(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `Invalid addressType: ${addressType}` }; } const portIndex = addressValueIndex + addressLength; const portRemote = new DataView(ssBuffer.slice(portIndex, portIndex + 2)).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: portIndex + 2, rawClientData: ssBuffer.slice(portIndex + 2), version: null, isUDP: portRemote == 53, }; } function readNekoHeader(buffer) { const version = new Uint8Array(buffer.slice(0, 1)); let isUDP = false; const optLength = new Uint8Array(buffer.slice(17, 18))[0]; const cmd = new Uint8Array(buffer.slice(18 + optLength, 18 + optLength + 1))[0]; if (cmd === 2) isUDP = true; else if (cmd !== 1) return { hasError: true, message: `cmd ${cmd} unsupported` }; const portIndex = 18 + optLength + 1; const portRemote = new DataView(buffer.slice(portIndex, portIndex + 2)).getUint16(0); let addressIndex = portIndex + 2; const addressType = new Uint8Array(buffer.slice(addressIndex, addressIndex + 1))[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 2: addressLength = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 3: addressLength = 16; const dataView = new DataView(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `invalid addressType ${addressType}` }; } return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, rawClientData: buffer.slice(addressValueIndex + addressLength), version: new Uint8Array([version[0], 0]), isUDP, }; } function readHorseHeader(buffer) { const dataBuffer = buffer.slice(58); if (dataBuffer.byteLength < 6) return { hasError: true, message: "invalid data" }; let isUDP = false; const view = new DataView(dataBuffer); const cmd = view.getUint8(0); if (cmd == 3) isUDP = true; else if (cmd != 1) throw new Error("Unsupported cmd"); let addressType = view.getUint8(1); let addressLength = 0; let addressValueIndex = 2; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(dataBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 4: addressLength = 16; const dataView = new DataView(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `invalid addressType ${addressType}` }; } const portIndex = addressValueIndex + addressLength; const portRemote = new DataView(dataBuffer.slice(portIndex, portIndex + 2)).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: portIndex + 4, rawClientData: dataBuffer.slice(portIndex + 4), version: null, isUDP, }; } function base64ToArrayBuffer(base64Str) { if (!base64Str) return { error: null }; try { base64Str = base64Str.replace(/-/g, "+").replace(/_/g, "/"); const decode = atob(base64Str); const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return { earlyData: arryBuffer.buffer, error: null }; } catch (error) { return { error }; } } function arrayBufferToHex(buffer) { return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join(""); } async function md5(...inputs) { const combined = new Uint8Array(inputs.reduce((acc, input) => acc + input.length, 0)); let offset = 0; for (const input of inputs) { combined.set(new Uint8Array(input), offset); offset += input.length; } const hashBuffer = await crypto.subtle.digest("MD5", combined); return new Uint8Array(hashBuffer); } async function sha256(input) { const hashBuffer = await crypto.subtle.digest("SHA-256", input); return new Uint8Array(hashBuffer); } async function kdf(key, path) { async function recursiveHash(keyBytes, innerHashFn) { return async (data) => { const ipad = new Uint8Array(64); const opad = new Uint8Array(64); ipad.set(keyBytes.slice(0, Math.min(64, keyBytes.length))); opad.set(keyBytes.slice(0, Math.min(64, keyBytes.length))); for (let i = 0; i < 64; i++) { ipad[i] ^= 0x36; opad[i] ^= 0x5c; } const innerData = new Uint8Array(ipad.length + data.length); innerData.set(ipad); innerData.set(data, ipad.length); const innerResult = await innerHashFn(innerData); const outerData = new Uint8Array(opad.length + innerResult.length); outerData.set(opad); outerData.set(innerResult, opad.length); return await innerHashFn(outerData); }; } const sha256Hash = async (data) => new Uint8Array(await crypto.subtle.digest("SHA-256", data)); let currentHashFn = await recursiveHash(new TextEncoder().encode("VMess AEAD KDF"), sha256Hash); for (const salt of path) { const saltBytes = typeof salt === "string" ? new TextEncoder().encode(salt) : new Uint8Array(salt); currentHashFn = await recursiveHash(saltBytes, currentHashFn); } return await currentHashFn(key); } async function aesGcmDecrypt(key, nonce, data, aad) { const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["decrypt"]); try { const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data); return new Uint8Array(decrypted); } catch (e) { throw new Error("AEAD decrypt error: " + e.message); } } async function aesGcmEncrypt(key, nonce, data, aad) { const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt"]); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data); return new Uint8Array(encrypted); }