/* =========================================================================== downloads.mjs - bilingual browser, PWA and verified desktop download choices The game origin owns the install prompt. Never install the marketing origin or expose desktop links until an actual release manifest supplies checksums. =========================================================================== */ const GAME_URL = "https://play.varkand.com/play"; const COPY = { en: { title: "Download Varkand", browser: "Play in browser", install: "Install as app", intro: "Play instantly in your browser, or keep Varkand as an app.", pwa: "Open the install page and choose Install Varkand. On iPhone or iPad, open it in Safari, tap Share, then Add to Home Screen. On Mac Safari, choose File, then Add to Dock.", cache: "Downloaded game files are kept locally. Updates fetch changed files. Clearing app data or device storage can require another download.", pending: "Desktop downloads are being prepared. You can play in your browser now.", warning: "These initial desktop builds are unsigned. Windows or macOS may show an operating system warning. Download only from varkand.com.", windows: "Windows 64-bit (.exe)", macArm: "macOS Apple silicon (.dmg)", macIntel: "macOS Intel (.dmg)", macUniversal: "macOS Apple silicon and Intel (.dmg)", checksum: "SHA-256 checksum", bytes: "bytes", ready: "Desktop downloads", requirements: "A WebGPU-capable device is required. The Mac app requires macOS 26 or later." }, ar: { title: "تحميل Varkand", browser: "العب في المتصفح", install: "تثبيت كتطبيق", intro: "العب مباشرة في المتصفح أو احتفظ بـ Varkand كتطبيق.", pwa: "افتح صفحة التثبيت واختر تثبيت Varkand. على iPhone أو iPad، افتحها في Safari واضغط مشاركة ثم إضافة إلى الشاشة الرئيسية. على Mac Safari، اختر ملف ثم إضافة إلى Dock.", cache: "تُحفظ ملفات اللعبة التي تم تنزيلها محليًا. تُنزّل التحديثات الملفات المتغيرة. قد يؤدي مسح بيانات التطبيق أو مساحة الجهاز إلى الحاجة لتنزيلها مجددًا.", pending: "يجري إعداد نسخ سطح المكتب. يمكنك اللعب في المتصفح الآن.", warning: "نسخ سطح المكتب الأولى غير موقعة. قد يعرض Windows أو macOS تحذيرًا من نظام التشغيل. حمّل من varkand.com فقط.", windows: "Windows ‏64 بت (.exe)", macArm: "macOS بمعالج Apple silicon ‏(.dmg)", macIntel: "macOS بمعالج Intel ‏(.dmg)", macUniversal: "macOS بمعالجات Apple silicon وIntel ‏(.dmg)", checksum: "بصمة SHA-256", bytes: "بايت", ready: "تنزيلات سطح المكتب", requirements: "يلزم جهاز يدعم WebGPU. يتطلب تطبيق Mac نظام macOS 26 أو أحدث." } }; const PLATFORMS = { "windows-x86_64": [ "windows", ".exe" ], "darwin-aarch64": [ "macArm", ".dmg" ], "darwin-x86_64": [ "macIntel", ".dmg" ], "darwin-universal": [ "macUniversal", ".dmg" ] }; /* ================ downloadRelease Only same-site, versioned artifact paths and complete hashes become links. ================ */ export function downloadRelease( value ) { if ( value?.schema !== 1 || typeof value.version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test( value.version ) || !Array.isArray( value.assets ) ) { throw Error( "Invalid desktop release" ); } const seen = new Set(); const seenUrls = new Set(); return value.assets.map( asset => { const spec = typeof asset.platform === "string" && Object.hasOwn( PLATFORMS, asset.platform ) ? PLATFORMS[asset.platform] : undefined; if ( !spec || seen.has( asset.platform ) || seenUrls.has( asset.url ) || typeof asset.sha256 !== "string" || asset.sha256.length !== 64 || !/^[a-f0-9]{64}$/.test( asset.sha256 ) || !Number.isSafeInteger( asset.bytes ) || asset.bytes <= 0 || !/^\/download\/[a-zA-Z0-9._/-]+$/.test( asset.url ) || asset.url.split( "/" ).some( segment => segment === "." || segment === ".." ) || asset.url.includes( "//" ) || !asset.url.startsWith( "/download/" + value.version + "/" ) || !asset.url.endsWith( spec[1] ) ) throw Error( "Invalid desktop artifact" ); seen.add( asset.platform ); seenUrls.add( asset.url ); return { ...asset, label: spec[0] }; } ); } /* ================ mountDownloads ================ */ export async function mountDownloads( root = document, fetchRelease = globalThis.fetch ) { const target = root.querySelector( "#varkand-downloads" ); if ( !target ) return; const mount = target; let assets = []; /* ================ render ================ */ function render() { const copy = COPY[root.documentElement.lang === "ar" ? "ar" : "en"]; mount.replaceChildren(); /* ================ append ================ */ function append( tag, text, parent = mount ) { const node = root.createElement( tag ); node.textContent = text; parent.append( node ); return node; } append( "h2", copy.title ).id = "download-heading"; append( "p", copy.intro ); for ( const [label, url] of [ [ copy.browser, GAME_URL ], [ copy.install, "https://play.varkand.com/varkand-install.html" ] ] ) { const link = append( "a", label ); link.href = url; link.className = "download-choice"; } append( "p", copy.pwa ); append( "p", copy.cache ); append( "p", copy.requirements ).className = "hint"; append( "h3", copy.ready ); if ( !assets.length ) { append( "p", copy.pending ); for ( const label of [ copy.windows, copy.macUniversal ] ) { const button = append( "button", label ); button.type = "button"; button.disabled = true; button.className = "download-choice"; } } for ( const asset of assets ) { const card = append( "div", "" ); card.className = "download-artifact"; append( "a", copy[asset.label], card ).href = asset.url; const size = new Intl.NumberFormat( root.documentElement.lang === "ar" ? "ar" : "en" ).format( asset.bytes ); append( "p", size + " " + copy.bytes, card ); append( "p", copy.checksum, card ); const hash = append( "code", asset.sha256, card ); hash.dir = "ltr"; } append( "p", copy.warning ).className = "hint"; } render(); root.querySelector( "#language" )?.addEventListener( "change", () => queueMicrotask( render ) ); new MutationObserver( render ).observe( root.documentElement, { attributes: true, attributeFilter: [ "lang" ] } ); try { const response = await fetchRelease( "/download/releases.json", { cache: "no-cache", signal: AbortSignal.timeout( 10000 ) } ); if ( !response.ok ) return; assets = downloadRelease( await response.json() ); render(); } catch { // Play and app-install choices remain available before desktop publication. } } if ( typeof document !== "undefined" ) void mountDownloads();