/* Cache the single page-owned app response before it registers the worker. */ (function (root) { 'use strict'; const doc=root.document, manifest=root.Lazy.manifest, key=root.Lazy.url('/app.js'); let domReady=doc.readyState==='complete', appSawDom=false; doc.addEventListener('DOMContentLoaded',()=>{domReady=true;appSawDom=typeof root.boot==='function';},{once:true}); function execute(node) { doc.head.append(node); node.remove(); delete root.Lazy.appResponse; // app.js keeps its original DOMContentLoaded listener. Async response // consumption can finish after that event; boot exactly once in that case. if(domReady)root.boot(); } async function external(url) { await new Promise((resolve,reject)=>{ const node=doc.createElement('script');node.src=url; node.onload=()=>{if(domReady&&!appSawDom)root.boot();resolve();};node.onerror=()=>reject(Error('Unable to load player')); doc.head.append(node); }); } async function verified(response, expected) { const bytes=await response.clone().arrayBuffer(); const digest=new Uint8Array(await root.crypto.subtle.digest('SHA-256',bytes)); const hex=Array.from(digest,b=>b.toString(16).padStart(2,'0')).join(''); if(hex.slice(0,10)!==expected)throw Error('Player body hash mismatch'); return {bytes,digest}; } async function start() { if(!manifest.appCache || !root.crypto?.subtle || (!root.Lazy.captureApp&&!root.navigator?.serviceWorker?.controller))return external(key); const expected=manifest.files['/app.js'].h; let cache; try { cache=await root.caches?.open('ytplayer-assets'); } catch {} let response=await cache?.match(key); if(!root.Lazy.captureApp && response?.ok && response.headers.get('X-Asset-Hash')===expected)return external(key); if(!response || response.headers.get('X-Asset-Hash')!==expected) { try { response=await (root.Lazy.appResponse || root.fetch(key,{credentials:'same-origin'})); } catch {} } if(!response?.ok || response.headers.get('X-Asset-Hash')!==expected) { // Preserve staged N-1 boot after an individual cache eviction. Only the // controller can serve this old URL safely; never fetch stale URLs bare. const state=cache && await root.AssetSyncCore.state(cache); const previousKey=state && root.AssetSyncCore.fallback(manifest,state.previous,'/app.js'); const previous=previousKey && await cache.match(previousKey); const previousHash=state?.previous?.files['/app.js']?.h; const contractsMatch=state?.previous && Object.entries(manifest.groups).every(([name,group])=>state.previous.groups[name]?.contract===group.contract); if(contractsMatch && root.navigator?.serviceWorker?.controller && previous?.ok && previous.headers.get('X-Asset-Hash')===previousHash) { await verified(previous,previousHash); delete root.Lazy.appResponse; return external(previousKey); } throw Error('Player asset hash mismatch'); } const {bytes,digest}=await verified(response,expected); const permission="'sha256-"+root.btoa(String.fromCharCode(...digest))+"'"; if(!doc.querySelector('meta[http-equiv="Content-Security-Policy"]').content.includes(permission))throw Error('Player body hash mismatch'); // CacheStorage may share a full quota with saved music. Never remove data // to make room; boot from these verified bytes even if caching is refused. try { await cache?.put(key,response); } catch(error) { root.console.warn('[app-cache]',error.message); } if(!root.Lazy.captureApp)return external(key); const node=doc.createElement('script');node.textContent=new root.TextDecoder().decode(bytes); execute(node); } const ready=start(); root.AppBootstrap={ready}; ready.catch(error=>root.console.error('[app-cache]',error.message)); })(typeof window!=='undefined'?window:globalThis);