@hazem3500/palette 0.9.110 → 0.9.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/setup.js CHANGED
@@ -1 +1 @@
1
- import{a as n,d as o}from"./chunk-U55ASP2Y.js";import"./chunk-KLPQTFVH.js";import"./chunk-RBHY7UDS.js";import"./chunk-LMXFSXO6.js";import"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import"./chunk-2BS2OWWH.js";import"./chunk-J47R52WX.js";import"./chunk-VX6XCU4E.js";import"./chunk-4HBBEVFK.js";function r(r){return(t={})=>o(n({...r,...t}))}var t=r;export{t as defineKomo,r as definePalette};
1
+ import{a as n,d as o}from"./chunk-CF4GJN3B.js";import"./chunk-KLPQTFVH.js";import"./chunk-RBHY7UDS.js";import"./chunk-LMXFSXO6.js";import"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import"./chunk-2BS2OWWH.js";import"./chunk-J47R52WX.js";import"./chunk-VX6XCU4E.js";import"./chunk-4HBBEVFK.js";function r(r){return(t={})=>o(n({...r,...t}))}var t=r;export{t as defineKomo,r as definePalette};
@@ -1 +1 @@
1
- var e="data-palette-no-snapshot",t=`[${e}],[data-komo-no-snapshot]`,a=e=>e.matches(t),n="[data-branch-comments],[data-palette-move-clone],[data-palette-viewport],[data-palette-snapshot-sandbox]",r="[redacted]",o=new Set(["script","noscript","template","style","link","meta","base","title"]),i=/^(?:href|src|action|formaction|xlink:href|poster|data|background)$/i,l=e=>"textarea"===e.localName||"input"===e.localName&&!/^(?:button|submit|reset|image|checkbox|radio|range|color)$/i.test(e.getAttribute("type")??""),s=2e4,c=200,h=500,d=(e,t)=>e.length>t?`${e.slice(0,t-1)}…`:e,u=e=>(new TextEncoder).encode(e).length,m=e=>!!e.closest(`${t},${n}`);function f(e,t=1/0){for(let{name:a,value:n}of[...e.attributes])/^on/i.test(a)||"srcdoc"===a||/^data-(palette|komo)-/.test(a)&&!/-no-snapshot$/.test(a)||i.test(a)&&/^\s*(?:javascript|vbscript):/i.test(n)?e.removeAttribute(a):n.length>t&&e.setAttribute(a,d(n,t))}function p(e,t,i=s){let c=e.ownerDocument,h=i,m=e=>{if(3===e.nodeType){let t=d(e.nodeValue??"",500);return h-=t.length,c.createTextNode(t)}if(1!==e.nodeType)return null;let i=e;if(o.has(i.localName)||i.matches(n))return null;let s=i.cloneNode(!1);if(f(s,200),l(i)&&function(e,t){let a=e.value??e.getAttribute("value")??"";"textarea"===e.localName?t.textContent=a?r:"":(a||t.hasAttribute("value"))&&t.setAttribute("value",r)}(i,s),h-=s.outerHTML.length,a(i))return s;if(t?.(i))return l(i)||(s.textContent=r),s;if("textarea"===i.localName)return s;for(let e of i.childNodes){if(h<=0){s.append(c.createComment(" trimmed "));break}let t=m(e);t&&s.append(t)}return s},p=m(e);if(!p)return"";let g=p.outerHTML;if(u(g)<=i)return g;let w=c.createComment(" trimmed ");p.append(w);for(let e=u(p.outerHTML)-i;e>0&&p.firstChild!==w;e=u(p.outerHTML)-i)for(;e>0&&p.firstChild!==w;){let t=w.previousSibling;for(;t.lastChild;)t=t.lastChild;e-=1===t.nodeType?t.outerHTML.length:(t.nodeValue?.length??0)+7,t.parentNode.removeChild(t)}return g=p.outerHTML,u(g)>i?"":g}var g=class extends Error{constructor(e){super(e),this.reason=e}reason},w=25e4,y=e=>new Promise((t,a)=>{let n=new FileReader;n.onload=()=>t(n.result),n.onerror=()=>a(n.error),n.readAsDataURL(e)});async function b(e,t){if(e.startsWith("data:"))return e;let a=new URL(e,t.document.baseURI);try{let e=await fetch(a,{mode:"cors",signal:AbortSignal.timeout(5e3)});return e.ok?await y(await e.blob()):""}catch{if(a.origin!==t.location.origin)throw new g("cross-origin");return""}}function x(e,t,a){let n="";for(let r=0;r<e.length;r++){let o=e[r];if(o.startsWith("--"))continue;let i=e.getPropertyValue(o);(i!==t.get(o)||a&&i!==a.getPropertyValue(o))&&(n+=`${o}:${i};`)}return n}var v=/url\((['"]?)(.*?)\1\)/g;async function $(e,t,i){let s=e.ownerDocument,c=s.defaultView,h=e.getBoundingClientRect(),d=e.offsetWidth??h.width,u=Math.min(e.offsetHeight??h.height,2e3);if(!d||!u)throw new g("failed");let m=function(){let e=document.createElement("iframe");e.setAttribute("data-palette-snapshot-sandbox",""),e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.style.cssText="position:fixed;left:-10px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none",document.documentElement.append(e);let t=e.contentDocument;try{t.open(),t.write("<!doctype html><html><head></head><body></body></html>"),t.close()}catch{}let a=new Map;return{of(e){let n=`${e.namespaceURI} ${e.localName}`,r=a.get(n);if(!r){let o=t.createElementNS(e.namespaceURI,e.localName);(t.body??t.documentElement).append(o);let i=t.defaultView.getComputedStyle(o);r=new Map;for(let e=0;e<i.length;e++)r.set(i[e],i.getPropertyValue(i[e]));o.remove(),a.set(n,r)}return r},dispose:()=>e.remove()}}(),p=[],w=new Set,y=new Map,$=[],M="",C=0,T=e=>(e.catch(()=>{}),e),A=e=>{for(let[,,t]of e.matchAll(v))t&&!t.startsWith("data:")&&!y.has(t)&&y.set(t,T(b(t,c)));return e},E=(e,h)=>{if(3===e.nodeType)return s.createTextNode(e.nodeValue??"");if(1!==e.nodeType)return null;let y=e,v=y.localName;if(o.has(v)||y.matches(n))return null;if(++C>3e3)throw new g("too-large");let I,L=c.getComputedStyle(y);if("none"===L.display)return null;if("canvas"===v||"video"===v){I=s.createElement("img");let e=document.createElement("canvas");e.width=y.width||y.videoWidth||1,e.height=y.height||y.videoHeight||1;try{e.getContext("2d").drawImage(y,0,0,e.width,e.height),I.setAttribute("src",e.toDataURL("image/png"))}catch{throw new g("cross-origin")}}else if("iframe"===v||"embed"===v||"object"===v)I=s.createElement("div");else{I=y.cloneNode(!1),f(I);for(let e of["id","class","srcset","sizes","loading","style"])I.removeAttribute(e)}l(y)&&(I.removeAttribute("value"),I.hasAttribute("placeholder")&&t?.(y)&&I.setAttribute("placeholder",r)),"img"===v&&p.push(T(async function(e,t,a){let n=e.currentSrc||e.src;if(!n||n.startsWith("data:"))return n;await e.decode().catch(()=>{});let r=Math.min(e.naturalWidth,Math.ceil(e.offsetWidth*t)||e.naturalWidth),o=Math.round(r*e.naturalHeight/(e.naturalWidth||1));if(r&&o){let t=document.createElement("canvas");t.width=r,t.height=o;try{return t.getContext("2d").drawImage(e,0,0,r,o),t.toDataURL("image/png")}catch{}}return b(n,a)}(y,i,c).then(e=>I.setAttribute("src",e)))),w.add(L.fontFamily.split(",")[0].replace(/["']/g,"").trim().toLowerCase());let R=x(L,m.of(y),h);h||(R+=`${"inline"===L.display?"display:inline-block;":""}margin:0;position:relative;inset:auto;float:none;transform:none;translate:none;rotate:none;scale:none;flex:none;box-sizing:border-box;width:${d}px;height:${u}px;max-width:none;max-height:none;min-width:0;min-height:0;`);let P=a(y)&&!!h;P&&(R+="color:transparent;background:#e5e5e5;background-image:none;"),$.push([I,A(R)]);for(let e of["::before","::after"]){let t=c.getComputedStyle(y,e);P||/^(?:none|normal)$/.test(t.content)||(I.setAttribute("data-snapshot",String(C)),M+=`[data-snapshot="${C}"]${e}{${A(x(t,m.of(y),L))}content:${t.content};}`)}if(P)return I;if(t?.(y))return l(y)||(I.textContent=r),I;if("textarea"===v)return I;for(let e of y.childNodes){if("picture"===v&&"img"!==e.localName)continue;let t=E(e,L);t&&I.append(t)}return I};try{await(s.fonts?.ready);let t=E(e),a=new Map(await Promise.all([...y].map(async([e,t])=>[e,await t]))),n=e=>e.replace(v,(e,t,n)=>a.has(n)?`url("${a.get(n)}")`:e);for(let[e,t]of $)e.setAttribute("style",n(t));M=n(M),await Promise.all(p);let r=s.createElement("div");r.setAttribute("style",`width:${d+32}px;height:${u+32}px;padding:16px;box-sizing:border-box;overflow:hidden;background:${function(e,t){for(let a=e.parentElement;a;a=a.parentElement){let e=t.getComputedStyle(a).backgroundColor;if(e&&!/^(?:transparent|rgba\(.*,\s*0\))$/.test(e))return e}return"#fff"}(e,c)}`);let o=s.createElement("style");o.textContent=await async function(e,t,a){let n=[],r=2e6,o=async(e,i)=>{for(let l of e){if(l.cssRules&&await o(l.cssRules,i),!l.cssText.startsWith("@font-face"))continue;let e=l.style,s=e.getPropertyValue("font-family").replace(/["']/g,"").trim().toLowerCase(),c=e.getPropertyValue("src").match(/url\((['"]?)(.*?)\1\)/)?.[2];if(!t.has(s)||!c||r<=0)continue;let h=await b(new URL(c,i).href,a).catch(()=>"");h&&(r-=h.length,n.push(`@font-face{font-family:"${s}";src:url("${h}");${["font-weight","font-style","font-stretch","unicode-range"].map(t=>e.getPropertyValue(t)?`${t}:${e.getPropertyValue(t)};`:"").join("")}}`))}};for(let t of e.styleSheets){let a;try{a=t.cssRules}catch{continue}await o(a,t.href??e.baseURI)}return n.join("")}(s,w,c)+M,r.append(o,t);let i={width:d+32,height:u+32},l=`<svg xmlns="http://www.w3.org/2000/svg" width="${i.width}" height="${i.height}"><foreignObject x="0" y="0" width="100%" height="100%">${(new XMLSerializer).serializeToString(r)}</foreignObject></svg>`,h=new Image;return h.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(l)}`,await h.decode(),{image:h,...i}}finally{m.dispose()}}var M=(e,t)=>new Promise((a,n)=>{try{e.toBlob(a,"image/webp",t)}catch(e){n(e)}});async function C(e,t={}){let a=t.maxBytes??w,n=e.ownerDocument.defaultView??window,r=e.offsetWidth||e.getBoundingClientRect().width,o=Math.min(n.devicePixelRatio||1,2,1200/(r+32)),i=await $(e,t.redact,o),l=document.createElement("canvas"),s=.8;for(let e=0;e<12&&i.width*o>=48;e++){l.width=Math.max(1,Math.round(i.width*o)),l.height=Math.max(1,Math.round(i.height*o));let e,t=l.getContext("2d");if(!t)throw new g("unsupported");t.drawImage(i.image,0,0,l.width,l.height);try{e=await M(l,s)}catch{throw new g("cross-origin")}if(!e||"image/webp"!==e.type)throw new g("unsupported");if(e.size<=a)return y(e);s>.5?s=Math.round(100*(s-.15))/100:o*=Math.min(.85,Math.max(.3,.95*Math.sqrt(a/e.size)))}throw new g("too-large")}var T={"cross-origin":"the element shows images from another origin","too-large":"the element is too large to draw",unsupported:"this browser can't encode WebP",failed:"it couldn't be drawn"},A=e=>`\x3c!-- palette: no image snapshot, ${T[e]}. --\x3e`;async function E(e,t,a=s){if(!e?.isConnected||m(e))return null;let n={},r=()=>p(e,t.redact,a-120);if(t.html&&(n.html=r()),t.image){await new Promise(e=>"function"==typeof requestIdleCallback?requestIdleCallback(e,{timeout:500}):setTimeout(e));try{n.image=await C(e,t)}catch(e){n.note=e instanceof g?e.reason:"failed","cross-origin"===n.note&&(n.html??=r()),n.html&&(n.html=A(n.note)+n.html)}}return n.image||n.html?n:null}var I=e=>{let[t,a]=e.split(","),n=atob(a),r=new Uint8Array(n.length);for(let e=0;e<n.length;e++)r[e]=n.charCodeAt(e);return new Blob([r],{type:t.slice(5,t.indexOf(";"))})},L=new WeakMap;function R(e,t,a,n,r){let o=L.get(e);if(!o){let r={...a,maxBytes:Math.min(a.maxBytes??w,n.maxImageBytes||1/0)};o=a.image||a.html?E(t(e),r,Math.min(s,n.maxHtmlBytes||1/0)).catch(()=>null):Promise.resolve(null),L.set(e,o)}return r&&o.then(async e=>{for(let t of["image","html"])e?.[t]&&await r(t,"image"===t?I(e.image):new Blob([e.html],{type:"text/html;charset=utf-8"})).catch(e=>console.info(`Palette: the comment posted without its ${t} snapshot.`,e instanceof Error?e.message:e))}),o}export{c as ATTRIBUTE_LIMIT,s as HTML_LIMIT,w as IMAGE_BYTES,e as OPT_OUT,r as PLACEHOLDER,h as TEXT_LIMIT,E as captureSnapshot,A as noteComment,m as skipped,R as snapshotFor,p as snapshotHtml,C as snapshotImage};
1
+ var e="data-palette-no-snapshot",t=`[${e}],[data-komo-no-snapshot]`,a=e=>e.matches(t),n="[data-branch-comments],[data-palette-move-clone],[data-palette-viewport],[data-palette-snapshot-sandbox]",r="[redacted]",o=new Set(["script","noscript","template","style","link","meta","base","title"]),i=/^(?:href|src|action|formaction|xlink:href|poster|data|background)$/i,l=e=>"textarea"===e.localName||"input"===e.localName&&!/^(?:button|submit|reset|image|checkbox|radio|range|color)$/i.test(e.getAttribute("type")??""),s=2e4,c=200,h=500,d=(e,t)=>e.length>t?`${e.slice(0,t-1)}…`:e,u=e=>(new TextEncoder).encode(e).length,m=e=>!!e.closest(`${t},${n}`);function f(e,t=1/0){for(let{name:a,value:n}of[...e.attributes])/^on/i.test(a)||"srcdoc"===a||/^data-(palette|komo)-/.test(a)&&!/-no-snapshot$/.test(a)||i.test(a)&&/^\s*(?:javascript|vbscript):/i.test(n)?e.removeAttribute(a):n.length>t&&e.setAttribute(a,d(n,t))}function p(e,t,i=s){let c=e.ownerDocument,h=i,m=e=>{if(3===e.nodeType){let t=d(e.nodeValue??"",500);return h-=t.length,c.createTextNode(t)}if(1!==e.nodeType)return null;let i=e;if(o.has(i.localName)||i.matches(n))return null;let s=i.cloneNode(!1);if(f(s,200),l(i)&&function(e,t){let a=e.value??e.getAttribute("value")??"";"textarea"===e.localName?t.textContent=a?r:"":(a||t.hasAttribute("value"))&&t.setAttribute("value",r)}(i,s),h-=s.outerHTML.length,a(i))return s;if(t?.(i))return l(i)||(s.textContent=r),s;if("textarea"===i.localName)return s;for(let e of i.childNodes){if(h<=0){s.append(c.createComment(" trimmed "));break}let t=m(e);t&&s.append(t)}return s},p=m(e);if(!p)return"";let g=p.outerHTML;if(u(g)<=i)return g;let w=c.createComment(" trimmed ");p.append(w);for(let e=u(p.outerHTML)-i;e>0&&p.firstChild!==w;e=u(p.outerHTML)-i)for(;e>0&&p.firstChild!==w;){let t=w.previousSibling;for(;t.lastChild;)t=t.lastChild;e-=1===t.nodeType?t.outerHTML.length:(t.nodeValue?.length??0)+7,t.parentNode.removeChild(t)}return g=p.outerHTML,u(g)>i?"":g}var g=class extends Error{constructor(e){super(e),this.reason=e}reason},w=25e4,y=e=>new Promise((t,a)=>{let n=new FileReader;n.onload=()=>t(n.result),n.onerror=()=>a(n.error),n.readAsDataURL(e)});async function b(e,t){if(e.startsWith("data:"))return e;let a=new URL(e,t.document.baseURI);try{let e=await fetch(a,{mode:"cors",signal:AbortSignal.timeout(5e3)});return e.ok?await y(await e.blob()):""}catch{if(a.origin!==t.location.origin)throw new g("cross-origin");return""}}function x(e,t,a){let n="";for(let r=0;r<e.length;r++){let o=e[r];if(o.startsWith("--"))continue;let i=e.getPropertyValue(o);(i!==t.get(o)||a&&i!==a.getPropertyValue(o))&&(n+=`${o}:${i};`)}return n}var v=/url\((['"]?)(.*?)\1\)/g;async function $(e,t,i){let s=e.ownerDocument,c=s.defaultView,h=e.getBoundingClientRect(),d=e.offsetWidth??h.width,u=Math.min(e.offsetHeight??h.height,2e3);if(!d||!u)throw new g("failed");let m=function(){let e=document.createElement("iframe");e.setAttribute("data-palette-snapshot-sandbox",""),e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.style.cssText="position:fixed;left:-10px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none",document.documentElement.append(e);let t=e.contentDocument;try{t.open(),t.write("<!doctype html><html><head></head><body></body></html>"),t.close()}catch{}let a=new Map;return{of(e){let n=`${e.namespaceURI} ${e.localName}`,r=a.get(n);if(!r){let o=t.createElementNS(e.namespaceURI,e.localName);(t.body??t.documentElement).append(o);let i=t.defaultView.getComputedStyle(o);r=new Map;for(let e=0;e<i.length;e++)r.set(i[e],i.getPropertyValue(i[e]));o.remove(),a.set(n,r)}return r},dispose:()=>e.remove()}}(),p=[],w=new Set,y=new Map,$=[],M="",C=0,T=e=>(e.catch(()=>{}),e),A=e=>{for(let[,,t]of e.matchAll(v))t&&!t.startsWith("data:")&&!y.has(t)&&y.set(t,T(b(t,c)));return e},I=(e,h)=>{if(3===e.nodeType)return s.createTextNode(e.nodeValue??"");if(1!==e.nodeType)return null;let y=e,v=y.localName;if(o.has(v)||y.matches(n))return null;if(++C>3e3)throw new g("too-large");let E,L=c.getComputedStyle(y);if("none"===L.display)return null;if("canvas"===v||"video"===v){E=s.createElement("img");let e=document.createElement("canvas");e.width=y.width||y.videoWidth||1,e.height=y.height||y.videoHeight||1;try{e.getContext("2d").drawImage(y,0,0,e.width,e.height),E.setAttribute("src",e.toDataURL("image/png"))}catch{throw new g("cross-origin")}}else if("iframe"===v||"embed"===v||"object"===v)E=s.createElement("div");else{E=y.cloneNode(!1),f(E);for(let e of["id","class","srcset","sizes","loading","style"])E.removeAttribute(e)}l(y)&&(E.removeAttribute("value"),E.hasAttribute("placeholder")&&t?.(y)&&E.setAttribute("placeholder",r)),"img"===v&&p.push(T(async function(e,t,a){let n=e.currentSrc||e.src;if(!n||n.startsWith("data:"))return n;await e.decode().catch(()=>{});let r=Math.min(e.naturalWidth,Math.ceil(e.offsetWidth*t)||e.naturalWidth),o=Math.round(r*e.naturalHeight/(e.naturalWidth||1));if(r&&o){let t=document.createElement("canvas");t.width=r,t.height=o;try{return t.getContext("2d").drawImage(e,0,0,r,o),t.toDataURL("image/png")}catch{}}return b(n,a)}(y,i,c).then(e=>E.setAttribute("src",e)))),w.add(L.fontFamily.split(",")[0].replace(/["']/g,"").trim().toLowerCase());let R=x(L,m.of(y),h);h||(R+=`${"inline"===L.display?"display:inline-block;":""}margin:0;position:relative;inset:auto;float:none;transform:none;translate:none;rotate:none;scale:none;flex:none;box-sizing:border-box;width:${d}px;height:${u}px;max-width:none;max-height:none;min-width:0;min-height:0;`);let k=a(y)&&!!h;k&&(R+="color:transparent;background:#e5e5e5;background-image:none;"),$.push([E,A(R)]);for(let e of["::before","::after"]){let t=c.getComputedStyle(y,e);k||/^(?:none|normal)$/.test(t.content)||(E.setAttribute("data-snapshot",String(C)),M+=`[data-snapshot="${C}"]${e}{${A(x(t,m.of(y),L))}content:${t.content};}`)}if(k)return E;if(t?.(y))return l(y)||(E.textContent=r),E;if("textarea"===v)return E;for(let e of y.childNodes){if("picture"===v&&"img"!==e.localName)continue;let t=I(e,L);t&&E.append(t)}return E};try{await(s.fonts?.ready);let t=I(e),a=new Map(await Promise.all([...y].map(async([e,t])=>[e,await t]))),n=e=>e.replace(v,(e,t,n)=>a.has(n)?`url("${a.get(n)}")`:e);for(let[e,t]of $)e.setAttribute("style",n(t));M=n(M),await Promise.all(p);let r=s.createElement("div");r.setAttribute("style",`width:${d+32}px;height:${u+32}px;padding:16px;box-sizing:border-box;overflow:hidden;background:${function(e,t){for(let a=e.parentElement;a;a=a.parentElement){let e=t.getComputedStyle(a).backgroundColor;if(e&&!/^(?:transparent|rgba\(.*,\s*0\))$/.test(e))return e}return"#fff"}(e,c)}`);let o=s.createElement("style");o.textContent=await async function(e,t,a){let n=[],r=2e6,o=async(e,i)=>{for(let l of e){if(l.cssRules&&await o(l.cssRules,i),!l.cssText.startsWith("@font-face"))continue;let e=l.style,s=e.getPropertyValue("font-family").replace(/["']/g,"").trim().toLowerCase(),c=e.getPropertyValue("src").match(/url\((['"]?)(.*?)\1\)/)?.[2];if(!t.has(s)||!c||r<=0)continue;let h=await b(new URL(c,i).href,a).catch(()=>"");h&&(r-=h.length,n.push(`@font-face{font-family:"${s}";src:url("${h}");${["font-weight","font-style","font-stretch","unicode-range"].map(t=>e.getPropertyValue(t)?`${t}:${e.getPropertyValue(t)};`:"").join("")}}`))}};for(let t of e.styleSheets){let a;try{a=t.cssRules}catch{continue}await o(a,t.href??e.baseURI)}return n.join("")}(s,w,c)+M,r.append(o,t);let i={width:d+32,height:u+32},l=`<svg xmlns="http://www.w3.org/2000/svg" width="${i.width}" height="${i.height}"><foreignObject x="0" y="0" width="100%" height="100%">${(new XMLSerializer).serializeToString(r)}</foreignObject></svg>`,h=new Image;return h.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(l)}`,await h.decode(),{image:h,...i}}finally{m.dispose()}}var M=(e,t)=>new Promise((a,n)=>{try{e.toBlob(a,"image/webp",t)}catch(e){n(e)}});async function C(e,t={}){let a=t.maxBytes??w,n=e.ownerDocument.defaultView??window,r=e.offsetWidth||e.getBoundingClientRect().width,o=Math.min(n.devicePixelRatio||1,2,1200/(r+32)),i=await $(e,t.redact,o),l=document.createElement("canvas"),s=.8;for(let e=0;e<12&&i.width*o>=48;e++){l.width=Math.max(1,Math.round(i.width*o)),l.height=Math.max(1,Math.round(i.height*o));let e,t=l.getContext("2d");if(!t)throw new g("unsupported");t.drawImage(i.image,0,0,l.width,l.height);try{e=await M(l,s)}catch{throw new g("cross-origin")}if(!e||"image/webp"!==e.type)throw new g("unsupported");if(e.size<=a)return y(e);s>.5?s=Math.round(100*(s-.15))/100:o*=Math.min(.85,Math.max(.3,.95*Math.sqrt(a/e.size)))}throw new g("too-large")}var T={"cross-origin":"cross-origin images","too-large":"the element is too large to draw",unsupported:"this browser can't encode WebP",failed:"it couldn't be drawn"},A=e=>`\x3c!-- palette: no image snapshot, ${T[e]}. --\x3e`;async function I(e,t,a=s){if(!e?.isConnected||m(e))return null;let n={},r=()=>p(e,t.redact,a-120);if(t.html&&(n.html=r()),t.image){await new Promise(e=>"function"==typeof requestIdleCallback?requestIdleCallback(e,{timeout:500}):setTimeout(e));try{n.image=await C(e,t)}catch(e){n.note=e instanceof g?e.reason:"failed","cross-origin"===n.note&&(n.html??=r()),n.html&&(n.html=A(n.note)+n.html)}}return n}var E=e=>{let[t,a]=e.split(","),n=Uint8Array.from(atob(a),e=>e.charCodeAt(0));return new Blob([n],{type:t.slice(5,-7)})},L=new WeakMap;function R(e,t,a,n,r){let o=L.get(e);if(!o){let r={...a,maxBytes:Math.min(a.maxBytes??w,n.maxImageBytes||1/0)};o=a.image||a.html?I(t(e),r,Math.min(s,n.maxHtmlBytes||1/0)).catch(()=>({note:"failed"})):Promise.resolve(null),L.set(e,o)}return r&&o.then(async e=>{let t="failed"===e?.note;for(let a of["image","html"]){let n=e?.[a];n&&await r(a,"image"===a?E(n):new Blob([n],{type:"text/html;charset=utf-8"})).catch(()=>t=!0)}if(n.readiness&&(t||!e?.image&&!e?.html))return r("status",new Blob([`{"status":"${t?"failed":"skipped"}"}`],{type:"application/json"})).catch(()=>{})}),o}export{c as ATTRIBUTE_LIMIT,s as HTML_LIMIT,w as IMAGE_BYTES,e as OPT_OUT,r as PLACEHOLDER,h as TEXT_LIMIT,I as captureSnapshot,A as noteComment,m as skipped,R as snapshotFor,p as snapshotHtml,C as snapshotImage};
@@ -28,6 +28,7 @@ export declare const IMAGE_BYTES = 250000;
28
28
  export declare function snapshotImage(element: Element, options?: PaletteSnapshots): Promise<string>;
29
29
  /** The server's snapshot support, from `/config`. */
30
30
  export interface SnapshotLimits {
31
+ readiness?: boolean;
31
32
  /** Whether guest sessions may attach snapshots. */
32
33
  guests: boolean;
33
34
  maxImageBytes: number;
@@ -44,6 +45,6 @@ export declare function captureSnapshot(element: Element | null, options: Palett
44
45
  /**
45
46
  * The snapshot of a draft's target, captured once per anchor: the composer reads it
46
47
  * for a preview, and posting passes `post` to upload each kind as its raw file after
47
- * the thread is saved. Never rejects; a failed upload is logged and the comment stays posted.
48
+ * the thread is saved. Never rejects; failed uploads are reported when supported and the comment stays posted.
48
49
  */
49
- export declare function snapshotFor<Anchor extends object>(anchor: Anchor, resolve: (anchor: Anchor) => Element | null, options: PaletteSnapshots, limits: SnapshotLimits, post?: (kind: "image" | "html", body: Blob) => Promise<unknown>): Promise<CapturedSnapshot | null>;
50
+ export declare function snapshotFor<Anchor extends object>(anchor: Anchor, resolve: (anchor: Anchor) => Element | null, options: PaletteSnapshots, limits: SnapshotLimits, post?: (kind: "image" | "html" | "status", body: Blob) => Promise<unknown>): Promise<CapturedSnapshot | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazem3500/palette",
3
- "version": "0.9.110",
3
+ "version": "0.9.112",
4
4
  "description": "Point, area, and threaded comments for website branches.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/server/index.ts CHANGED
@@ -20,7 +20,8 @@ import { authOptions, type ProxyAuthProvider, type ProxyIdentity } from "./proxy
20
20
  import { paletteEnv } from "./public-env";
21
21
  import setupClient from "./setup-client.txt";
22
22
  import {
23
- dispatchWebhook,
23
+ enqueueWebhook,
24
+ drainWebhooks,
24
25
  type WebhookEvent,
25
26
  } from "./webhooks";
26
27
  import {
@@ -202,14 +203,16 @@ async function body(request: Request): Promise<Record<string, unknown>> {
202
203
  : path === "/me"
203
204
  ? ["name", "avatarUrl", "accentColor"]
204
205
  : path === "/threads"
205
- ? ["body", "page", "anchor", "tags", "scope"]
206
+ ? ["body", "page", "anchor", "tags", "scope", "snapshotIntent"]
206
207
  : path.endsWith("/comments")
207
208
  ? ["body"]
208
209
  : path.includes("/comments/")
209
210
  ? ["body"]
210
- : path.startsWith("/threads/")
211
- ? ["anchor", "resolved", "tag", "untag", "scope"]
212
- : [];
211
+ : path.endsWith("/snapshot")
212
+ ? ["status"]
213
+ : path.startsWith("/threads/")
214
+ ? ["anchor", "resolved", "tag", "untag", "scope"]
215
+ : [];
213
216
  check(
214
217
  Object.keys(result).every((key) => allowed.includes(key)),
215
218
  400,
@@ -286,12 +289,10 @@ async function authenticate(
286
289
  .get("Authorization")
287
290
  ?.match(/^Bearer (.{1,200})$/)?.[1];
288
291
  check(bearer, 401, "Enter your name to comment.");
289
- const { proxy, sessionMaxAge, allowGuests } = authOptions(env);
292
+ const { proxy, proxyAccessTokens, sessionMaxAge, allowGuests } = authOptions(env);
290
293
  if (isPersonalToken(bearer)) {
291
- // A token would outlive the proxy's own sign-in checks; agents exchange
292
- // their proxy identity for a session instead.
293
294
  check(
294
- !proxy,
295
+ !proxy || proxyAccessTokens,
295
296
  403,
296
297
  "Access tokens are off behind the sign-in proxy. Use /auth/proxy/session."
297
298
  );
@@ -1055,7 +1056,7 @@ async function route(
1055
1056
  }
1056
1057
  if (accountRoute) {
1057
1058
  check(
1058
- !auth.proxy,
1059
+ !auth.proxy || auth.proxyAccessTokens,
1059
1060
  403,
1060
1061
  "Access tokens are off behind the sign-in proxy. Use /auth/proxy/session."
1061
1062
  );
@@ -1072,6 +1073,7 @@ async function route(
1072
1073
  tokens: await listTokens(env, user.id),
1073
1074
  projects: await accountProjects(env, user.id),
1074
1075
  scopes: PAT_SCOPES,
1076
+ ...(auth.proxy ? { policy: { explicitProjects: true, maxLifetimeDays: 365 } } : {}),
1075
1077
  });
1076
1078
  if (url.pathname === "/account/tokens" && request.method === "POST") {
1077
1079
  await limit(env, `pat:create:${user.id}`, 10, 3600);
@@ -1147,11 +1149,10 @@ async function route(
1147
1149
  !(await privateAccess(env, project)),
1148
1150
  private: await privateAccess(env, project),
1149
1151
  guestResolve: config.allowGuestResolve !== false,
1150
- // Signed-in Google accounts can manage personal access tokens here,
1151
- // except behind a sign-in proxy.
1152
- tokens: !auth.proxy,
1152
+ tokens: !auth.proxy || auth.proxyAccessTokens,
1153
1153
  // Element snapshots are opt-in on the client; guests need project consent.
1154
1154
  snapshots: {
1155
+ readiness: true,
1155
1156
  guests: auth.allowGuests && config.allowGuestSnapshots === true,
1156
1157
  maxImageBytes: SNAPSHOT_LIMITS.image,
1157
1158
  maxHtmlBytes: SNAPSHOT_LIMITS.html,
@@ -1540,6 +1541,8 @@ async function route(
1540
1541
  }
1541
1542
  const id = crypto.randomUUID();
1542
1543
  const firstCommentId = crypto.randomUUID();
1544
+ check(data.snapshotIntent === undefined || typeof data.snapshotIntent === "boolean", 400, "Invalid snapshot intent.");
1545
+ const snapshotStatus = data.snapshotIntent === undefined ? null : data.snapshotIntent ? "pending" : "skipped";
1543
1546
  await env.DB.batch([
1544
1547
  // Page-only threads rely on the column default, as before the scope column existed.
1545
1548
  scope === "all"
@@ -1552,18 +1555,8 @@ async function route(
1552
1555
  env.DB.prepare(
1553
1556
  "INSERT INTO comments(id,thread_id,user_id,body,created_at) VALUES(?,?,?,?,?)"
1554
1557
  ).bind(firstCommentId, id, user.id, text, now),
1555
- ...retainReviewerComments(
1556
- env.DB,
1557
- project,
1558
- user.id,
1559
- config.retainedCommentsPerUser
1560
- ),
1561
- ]);
1562
- ctx.waitUntil(
1563
- dispatchWebhook(
1564
- env,
1565
- project,
1566
- webhookEvent(
1558
+ env.DB.prepare("UPDATE threads SET snapshot_status=? WHERE id=?").bind(snapshotStatus, id),
1559
+ enqueueWebhook(env.DB, project, { ...webhookEvent(
1567
1560
  "comment.created",
1568
1561
  project,
1569
1562
  repo,
@@ -1574,9 +1567,15 @@ async function route(
1574
1567
  user.name,
1575
1568
  text,
1576
1569
  origin || url.origin
1577
- )
1578
- ).catch(() => {})
1579
- );
1570
+ ), ...(snapshotStatus ? { snapshotStatus } : {}) }, firstCommentId),
1571
+ ...retainReviewerComments(
1572
+ env.DB,
1573
+ project,
1574
+ user.id,
1575
+ config.retainedCommentsPerUser
1576
+ ),
1577
+ ]);
1578
+ ctx.waitUntil(drainWebhooks(env));
1580
1579
  return json({ id, commentId: firstCommentId }, 201);
1581
1580
  }
1582
1581
  const snapshotRoute = url.pathname.match(
@@ -1585,12 +1584,18 @@ async function route(
1585
1584
  if (snapshotRoute) {
1586
1585
  const [, threadId, pathKind] = snapshotRoute;
1587
1586
  const thread = await env.DB.prepare(
1588
- "SELECT id FROM threads WHERE id=? AND project=? AND repo=? AND branch=?"
1587
+ "SELECT id,snapshot_status,created_at FROM threads WHERE id=? AND project=? AND repo=? AND branch=?"
1589
1588
  )
1590
1589
  .bind(threadId, project, repo, branch)
1591
- .first<{ id: string }>();
1590
+ .first<{ id: string; snapshot_status: string | null; created_at: number }>();
1592
1591
  check(thread, 404, "Thread not found on this branch.");
1593
1592
  if (request.method === "GET") {
1593
+ if (!pathKind) {
1594
+ const rows = await env.DB.prepare("SELECT kind FROM snapshots WHERE thread_id=?").bind(threadId).all<{kind:string}>();
1595
+ let status = rows.results.length ? "ready" : thread.snapshot_status ?? "unknown";
1596
+ if (status === "pending" && now - thread.created_at > 600000) status = "failed";
1597
+ return json({ status, kinds: rows.results.map(row => row.kind) });
1598
+ }
1594
1599
  const kind = snapshotKind(pathKind);
1595
1600
  const row = await env.DB.prepare(
1596
1601
  "SELECT mime,bytes FROM snapshots WHERE thread_id=? AND kind=?"
@@ -1638,13 +1643,20 @@ async function route(
1638
1643
  threadId,
1639
1644
  kind
1640
1645
  ),
1641
- env.DB.prepare("UPDATE threads SET updated_at=? WHERE id=?").bind(
1646
+ env.DB.prepare("UPDATE threads SET snapshot_status=CASE WHEN EXISTS (SELECT 1 FROM snapshots WHERE thread_id=?) THEN 'ready' ELSE 'skipped' END,updated_at=? WHERE id=?").bind(
1647
+ threadId,
1642
1648
  now,
1643
1649
  threadId
1644
1650
  ),
1645
1651
  ]);
1646
1652
  return json({ ok: true, deleted: deleted.meta.changes });
1647
1653
  }
1654
+ if (!pathKind && request.headers.get("Content-Type")?.startsWith("application/json")) {
1655
+ const data = await body(request);
1656
+ check(data.status === "skipped" || data.status === "failed", 400, "Report skipped or failed capture.");
1657
+ await env.DB.prepare("UPDATE threads SET snapshot_status=?,updated_at=? WHERE id=? AND snapshot_status='pending'").bind(data.status, now, threadId).run();
1658
+ return json({ ok: true });
1659
+ }
1648
1660
  await limit(env, `${project}:snapshot:${user.id}`, 60, 600);
1649
1661
  const snapshot = await readSnapshot(
1650
1662
  request,
@@ -1660,8 +1672,9 @@ async function route(
1660
1672
  snapshotCap(config.maxSnapshotBytes),
1661
1673
  true
1662
1674
  ),
1663
- env.DB.prepare("UPDATE threads SET updated_at=? WHERE id=?").bind(
1675
+ env.DB.prepare("UPDATE threads SET snapshot_status='ready',updated_at=? WHERE id=? AND EXISTS (SELECT 1 FROM snapshots WHERE thread_id=?)").bind(
1664
1676
  now,
1677
+ threadId,
1665
1678
  threadId
1666
1679
  ),
1667
1680
  ]);
@@ -1777,35 +1790,20 @@ async function route(
1777
1790
  );
1778
1791
  check(typeof data.resolved === "boolean", 400, "Invalid resolved state.");
1779
1792
  const becameResolved = !thread.resolved && data.resolved;
1780
- await env.DB.prepare(
1781
- "UPDATE threads SET resolved=?,resolved_by=?,updated_at=? WHERE id=?"
1782
- )
1783
- .bind(
1784
- Number(data.resolved),
1785
- data.resolved ? user.id : null,
1786
- now,
1787
- threadId
1788
- )
1789
- .run();
1790
- if (becameResolved)
1791
- ctx.waitUntil(
1792
- dispatchWebhook(
1793
- env,
1794
- project,
1795
- webhookEvent(
1796
- "comment.resolved",
1797
- project,
1798
- repo,
1799
- branch,
1800
- threadId,
1801
- thread.page,
1802
- thread.anchor,
1803
- user.name,
1804
- null,
1805
- origin || url.origin
1806
- )
1807
- ).catch(() => {})
1808
- );
1793
+ const firstComment = becameResolved
1794
+ ? await env.DB.prepare("SELECT id FROM comments WHERE thread_id=? ORDER BY created_at,id LIMIT 1")
1795
+ .bind(threadId).first<{ id: string }>()
1796
+ : null;
1797
+ await env.DB.batch([
1798
+ env.DB.prepare(
1799
+ "UPDATE threads SET resolved=?,resolved_by=?,updated_at=? WHERE id=?"
1800
+ ).bind(Number(data.resolved), data.resolved ? user.id : null, now, threadId),
1801
+ ...(firstComment ? [enqueueWebhook(env.DB, project, webhookEvent(
1802
+ "comment.resolved", project, repo, branch, threadId, thread.page,
1803
+ thread.anchor, user.name, null, origin || url.origin
1804
+ ), firstComment.id)] : []),
1805
+ ]);
1806
+ ctx.waitUntil(drainWebhooks(env));
1809
1807
  return json({ ok: true });
1810
1808
  }
1811
1809
  if (
@@ -1823,6 +1821,18 @@ async function route(
1823
1821
  now,
1824
1822
  threadId
1825
1823
  ),
1824
+ enqueueWebhook(env.DB, project, webhookEvent(
1825
+ "comment.replied",
1826
+ project,
1827
+ repo,
1828
+ branch,
1829
+ threadId,
1830
+ thread.page,
1831
+ thread.anchor,
1832
+ user.name,
1833
+ text,
1834
+ origin || url.origin
1835
+ ), replyId),
1826
1836
  ...retainReviewerComments(
1827
1837
  env.DB,
1828
1838
  project,
@@ -1835,24 +1845,7 @@ async function route(
1835
1845
  409,
1836
1846
  "This thread has reached 200 comments. Start another thread."
1837
1847
  );
1838
- ctx.waitUntil(
1839
- dispatchWebhook(
1840
- env,
1841
- project,
1842
- webhookEvent(
1843
- "comment.replied",
1844
- project,
1845
- repo,
1846
- branch,
1847
- threadId,
1848
- thread.page,
1849
- thread.anchor,
1850
- user.name,
1851
- text,
1852
- origin || url.origin
1853
- )
1854
- ).catch(() => {})
1855
- );
1848
+ ctx.waitUntil(drainWebhooks(env));
1856
1849
  return json({ ok: true, id: replyId }, 201);
1857
1850
  }
1858
1851
  check(commentId, 405, "Method not allowed.");
@@ -1957,6 +1950,7 @@ const settingsFor = new WeakMap<Env, Env>();
1957
1950
  export default {
1958
1951
  async scheduled(_event, env, ctx) {
1959
1952
  ctx.waitUntil(maintain(env.DB, true));
1953
+ ctx.waitUntil(drainWebhooks(paletteEnv(env)));
1960
1954
  },
1961
1955
  async fetch(request, settings, ctx) {
1962
1956
  // Deployments configured before the rename set KOMO_ names. One copy per
@@ -2047,3 +2041,5 @@ export default {
2047
2041
  return new Response(response.body, { status: response.status, headers });
2048
2042
  },
2049
2043
  } satisfies ExportedHandler<Env>;
2044
+
2045
+ export { drainWebhooks } from "./webhooks";
@@ -0,0 +1,14 @@
1
+ CREATE TABLE webhook_outbox (
2
+ id TEXT PRIMARY KEY,
3
+ project TEXT NOT NULL,
4
+ body TEXT NOT NULL,
5
+ url TEXT NOT NULL,
6
+ secret TEXT NOT NULL,
7
+ attempts INTEGER NOT NULL DEFAULT 0,
8
+ next_attempt BIGINT NOT NULL,
9
+ claim_until BIGINT NOT NULL DEFAULT 0,
10
+ claim_token TEXT NOT NULL DEFAULT '',
11
+ state TEXT NOT NULL DEFAULT 'pending'
12
+ );
13
+ CREATE INDEX webhook_outbox_due ON webhook_outbox(state,next_attempt,claim_until);
14
+ ALTER TABLE threads ADD COLUMN snapshot_status TEXT;
@@ -101,6 +101,11 @@ export async function sendWebhookPinned(
101
101
  lookup: pinnedLookup(address),
102
102
  },
103
103
  (response) => {
104
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
105
+ response.destroy();
106
+ finish(new Error("Webhook returned a non-success status."));
107
+ return;
108
+ }
104
109
  // Read and discard a bounded amount. Never follow redirects or log bodies.
105
110
  let read = 0;
106
111
  response.on("data", (chunk: Buffer) => {
@@ -27,6 +27,7 @@ export type AuthOptions = {
27
27
  signInPrefix: string;
28
28
  requireSessionForReads: boolean;
29
29
  allowGuests: boolean;
30
+ proxyAccessTokens: boolean;
30
31
  /** Session lifetime cap in milliseconds; undefined keeps the default. */
31
32
  sessionMaxAge?: number;
32
33
  };
@@ -95,6 +96,7 @@ function resolve(env: OptionEnv): AuthOptions {
95
96
  const allowGuests = flag(env, "PALETTE_ALLOW_GUESTS", true);
96
97
  return {
97
98
  proxy,
99
+ proxyAccessTokens: flag(env, "PALETTE_PROXY_ACCESS_TOKENS", false),
98
100
  signInPrefix,
99
101
  requireSessionForReads: !!proxy || requireSessionForReads,
100
102
  allowGuests: !proxy && allowGuests,
@@ -20,6 +20,8 @@ export interface ServerEnv {
20
20
  PALETTE_PAUSED?: string;
21
21
  /** Trusted identity proxy that signs reviewers in, e.g. "gcp-iap". Off unless set. */
22
22
  PALETTE_PROXY_AUTH?: string;
23
+ /** Explicitly allow bounded, project-scoped access tokens behind a proxy. */
24
+ PALETTE_PROXY_ACCESS_TOKENS?: string;
23
25
  /** The audience the proxy signs its assertions for. */
24
26
  PALETTE_PROXY_AUDIENCE?: string;
25
27
  /** Older name: setting it alone turns on "gcp-iap" with this audience. */
@@ -53,6 +55,8 @@ export interface ServerEnv {
53
55
  KOMO_PAUSED?: string;
54
56
  /** @deprecated Read when PALETTE_PROXY_AUTH is unset. */
55
57
  KOMO_PROXY_AUTH?: string;
58
+ /** @deprecated Read when PALETTE_PROXY_ACCESS_TOKENS is unset. */
59
+ KOMO_PROXY_ACCESS_TOKENS?: string;
56
60
  /** @deprecated Read when PALETTE_PROXY_AUDIENCE is unset. */
57
61
  KOMO_PROXY_AUDIENCE?: string;
58
62
  /** @deprecated Read when PALETTE_IAP_AUDIENCE is unset. */