UNPKG

@s4e/jsentinel

Version:

Script integrity and clipboard tamper detection for browsers.

5 lines (4 loc) 5.37 kB
var x=new TextEncoder;async function S(t){let n=await crypto.subtle.digest("SHA-256",x.encode(t));return"sha256-"+btoa(String.fromCharCode(...new Uint8Array(n)))}function l(t){window.dispatchEvent(new CustomEvent("jsentinel",{detail:t,bubbles:!0,composed:!0}))}function b(t,n){let a=e=>{let d=e;d.detail?.type===t&&n(d.detail.data)};return window.addEventListener("jsentinel",a),a}function M(t){let n=a=>t(a.detail);return window.addEventListener("jsentinel",n),n}function E(t){window.removeEventListener("jsentinel",t)}function C(t){return t.trim().replace(/\r\n/g,` `).replace(/[ \t]+/g," ").replace(/[ \t]*\n[ \t]*/g,` `)}function J(t,n){return!!((t.type||"").toLowerCase()==="application/ld+json"||/__NEXT_DATA__|self\.__next_|window\.__NUXT__/.test(n)||/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/.test(n))}function L(t){let n=new Map,a=new Set,e=new Set,d=[],r=new Map;for(let i of t){if("inline"in i){i.type==="inline-stable"&&i.hash&&a.add(i.hash);continue}if(i.type==="same-origin"&&i.hash)try{let o=new URL(i.src,window.location.href);n.set(o.pathname,i.hash),n.set(decodeURIComponent(o.pathname),i.hash),n.set(i.src,i.hash);let u=(o.pathname.split("/").pop()||"").toLowerCase();u&&r.set(u,i.hash)}catch{n.set(i.src,i.hash);let o=(i.src.split("/").pop()||"").toLowerCase();o&&r.set(o,i.hash)}else i.type==="versioned-cdn"?e.add(i.src):i.type==="dynamic-loader"&&d.push({host:i.host,path:i.path,query:i.query})}return{sameOriginHashes:n,inlineHashes:a,versionedPins:e,dynamicPins:d,filenameIndex:r}}function v(t,n){t&&window.dispatchEvent(new CustomEvent("jsentinel",{detail:{type:"script.checked",data:n},bubbles:!0,composed:!0}))}function T(t,n){try{let a=new URL(t,window.location.href);return n.some(e=>!(e.host&&a.host!==e.host||e.path&&a.pathname!==e.path||e.query&&a.search.replace(/^\?/,"")!==(e.query||"")))}catch{return!1}}async function k(t,n,a){try{if(!t.src){let s=t.textContent||"",c=C(s),p=await S(c),y=n.inlineHashes.has(p);if(v(a,{key:"inline",src:null,inline:!0,expected:y?p:void 0,got:p,ok:y}),y)return l({type:"script.ok",data:{key:"inline",src:null,inline:!0,expected:p,got:p,ok:!0,severity:"info"}}),!0;let g=J(t,s);return l({type:"script.mismatch",data:{key:"inline",src:null,inline:!0,expected:void 0,got:p,ok:!1,severity:g?"warn":"error",reason:g?"volatile-inline":"manifest-missing-key"}}),!1}let e=t.src,d=!1,r="";try{let s=new URL(e,window.location.href);d=s.origin===window.location.origin,r=s.pathname}catch{}if(d){let c=n.sameOriginHashes.get(r)||n.sameOriginHashes.get(decodeURIComponent(r))||n.sameOriginHashes.get(e);if(!c){let h=(r.split("/").pop()||"").toLowerCase();if(h){let w=n.filenameIndex.get(h);w&&(c=w)}}let p="",y=!1;try{let h=await(await fetch(e,{cache:"no-store"})).text();p=await S(h)}catch{y=!0}if(y)return l({type:"script.mismatch",data:{key:r||e,src:e,inline:!1,expected:c,got:"",ok:!1,severity:"error",reason:"fetch-error"}}),!1;let g=c===p;return v(a,{key:r||e,src:e,inline:!1,expected:c,got:p,ok:g}),g?(l({type:"script.ok",data:{key:r||e,src:e,inline:!1,expected:c,got:p,ok:!0,severity:"info"}}),!0):(l({type:"script.mismatch",data:{key:r||e,src:e,inline:!1,expected:c,got:p,ok:!1,severity:"error",reason:c?"hash-different":"manifest-missing-key"}}),!1)}if(!/^https:\/\//i.test(e))return l({type:"script.mismatch",data:{key:e,src:e,inline:!1,expected:void 0,got:"",ok:!1,severity:"warn",reason:"insecure-http"}}),!1;let i="",o=!1;try{let s=await(await fetch(e,{cache:"no-store"})).text();i=await S(s),o=!0}catch{}let u=n.versionedPins.has(e),m=T(e,n.dynamicPins);return!u&&!m?(l({type:"script.mismatch",data:{key:e,src:e,inline:!1,expected:void 0,got:o?i:"",ok:!1,severity:"warn",reason:"url-pin-mismatch"}}),!1):(v(a,{key:e,src:e,inline:!1,expected:void 0,got:o?i:"",ok:!0}),l({type:"script.ok",data:{key:e,src:e,inline:!1,expected:void 0,got:o?i:"",ok:!0,severity:"info"}}),!0)}catch(e){return l({type:"error",data:{stage:"check",message:String(e?.message||e)}}),!1}}async function D(t={}){let n=t.manifestPath??"/manifest.json",a=t.watchClipboard!==!1,e=!!t.verboseChecks;try{let r=await(await fetch(n,{cache:"no-store"})).json(),i=Array.isArray(r?.scripts)?r.scripts:[],o=L(i),u=Array.from(document.scripts),m=await Promise.all(u.map(f=>k(f,o,e)));if(l({type:"scan.complete",data:{total:m.length,ok:m.filter(Boolean).length,mismatched:m.filter(f=>!f).length}}),new MutationObserver(f=>f.flatMap(s=>Array.from(s.addedNodes)).filter(s=>s.nodeName==="SCRIPT").forEach(s=>{k(s,o,e)})).observe(document.documentElement,{childList:!0,subtree:!0}),a){let f="";document.addEventListener("selectionchange",()=>{f=window.getSelection()?.toString()||f}),document.addEventListener("copy",()=>{let s=f;setTimeout(async()=>{try{let c=await navigator.clipboard.readText();s&&c&&c!==s&&l({type:"clipboard.tamper",data:{expected:s,got:c}})}catch{l({type:"clipboard.permission",data:{}})}},100)})}l({type:"ready",data:{}})}catch(d){l({type:"error",data:{stage:"init",message:String(d?.message||d)}})}}function O(){return M(t=>{let n=new Date().toISOString();t.type==="script.ok"?console.info(`[JSentinel ${n}] OK`,t.data.key):t.type==="script.mismatch"?console.warn(`[JSentinel ${n}] MISMATCH`,t.data):t.type==="scan.complete"&&console.log(`[JSentinel ${n}] Scan`,t.data)})}export{O as attachConsoleLogger,D as initJSentinel,E as offJSentinel,b as onJSentinel,M as onJSentinelAny}; //# sourceMappingURL=index.js.map