@hazem3500/palette 0.9.111 → 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/NODE.md +65 -0
- package/dist/chunk-CF4GJN3B.js +1 -0
- package/dist/index.js +1 -1
- package/dist/node.mjs +147 -77
- package/dist/postgres/008_webhook_outbox.sql +14 -0
- package/dist/react.js +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +141 -77
- package/dist/setup.js +1 -1
- package/dist/{snapshot-3PSQKXUV.js → snapshot-YYM5GW6I.js} +1 -1
- package/dist/snapshot.d.ts +3 -2
- package/package.json +1 -1
- package/server/index.ts +70 -71
- package/server/migrations/0022_webhook_outbox.sql +14 -0
- package/server/node-webhooks.ts +5 -0
- package/server/setup-client.txt +52 -52
- package/server/webhooks.ts +98 -13
- package/dist/chunk-DDYHX4RZ.js +0 -1
package/dist/setup.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as n,d as o}from"./chunk-
|
|
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};
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -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;
|
|
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
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
|
-
|
|
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.
|
|
211
|
-
? ["
|
|
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,
|
|
@@ -1149,6 +1152,7 @@ async function route(
|
|
|
1149
1152
|
tokens: !auth.proxy || auth.proxyAccessTokens,
|
|
1150
1153
|
// Element snapshots are opt-in on the client; guests need project consent.
|
|
1151
1154
|
snapshots: {
|
|
1155
|
+
readiness: true,
|
|
1152
1156
|
guests: auth.allowGuests && config.allowGuestSnapshots === true,
|
|
1153
1157
|
maxImageBytes: SNAPSHOT_LIMITS.image,
|
|
1154
1158
|
maxHtmlBytes: SNAPSHOT_LIMITS.html,
|
|
@@ -1537,6 +1541,8 @@ async function route(
|
|
|
1537
1541
|
}
|
|
1538
1542
|
const id = crypto.randomUUID();
|
|
1539
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";
|
|
1540
1546
|
await env.DB.batch([
|
|
1541
1547
|
// Page-only threads rely on the column default, as before the scope column existed.
|
|
1542
1548
|
scope === "all"
|
|
@@ -1549,18 +1555,8 @@ async function route(
|
|
|
1549
1555
|
env.DB.prepare(
|
|
1550
1556
|
"INSERT INTO comments(id,thread_id,user_id,body,created_at) VALUES(?,?,?,?,?)"
|
|
1551
1557
|
).bind(firstCommentId, id, user.id, text, now),
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
project,
|
|
1555
|
-
user.id,
|
|
1556
|
-
config.retainedCommentsPerUser
|
|
1557
|
-
),
|
|
1558
|
-
]);
|
|
1559
|
-
ctx.waitUntil(
|
|
1560
|
-
dispatchWebhook(
|
|
1561
|
-
env,
|
|
1562
|
-
project,
|
|
1563
|
-
webhookEvent(
|
|
1558
|
+
env.DB.prepare("UPDATE threads SET snapshot_status=? WHERE id=?").bind(snapshotStatus, id),
|
|
1559
|
+
enqueueWebhook(env.DB, project, { ...webhookEvent(
|
|
1564
1560
|
"comment.created",
|
|
1565
1561
|
project,
|
|
1566
1562
|
repo,
|
|
@@ -1571,9 +1567,15 @@ async function route(
|
|
|
1571
1567
|
user.name,
|
|
1572
1568
|
text,
|
|
1573
1569
|
origin || url.origin
|
|
1574
|
-
)
|
|
1575
|
-
|
|
1576
|
-
|
|
1570
|
+
), ...(snapshotStatus ? { snapshotStatus } : {}) }, firstCommentId),
|
|
1571
|
+
...retainReviewerComments(
|
|
1572
|
+
env.DB,
|
|
1573
|
+
project,
|
|
1574
|
+
user.id,
|
|
1575
|
+
config.retainedCommentsPerUser
|
|
1576
|
+
),
|
|
1577
|
+
]);
|
|
1578
|
+
ctx.waitUntil(drainWebhooks(env));
|
|
1577
1579
|
return json({ id, commentId: firstCommentId }, 201);
|
|
1578
1580
|
}
|
|
1579
1581
|
const snapshotRoute = url.pathname.match(
|
|
@@ -1582,12 +1584,18 @@ async function route(
|
|
|
1582
1584
|
if (snapshotRoute) {
|
|
1583
1585
|
const [, threadId, pathKind] = snapshotRoute;
|
|
1584
1586
|
const thread = await env.DB.prepare(
|
|
1585
|
-
"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=?"
|
|
1586
1588
|
)
|
|
1587
1589
|
.bind(threadId, project, repo, branch)
|
|
1588
|
-
.first<{ id: string }>();
|
|
1590
|
+
.first<{ id: string; snapshot_status: string | null; created_at: number }>();
|
|
1589
1591
|
check(thread, 404, "Thread not found on this branch.");
|
|
1590
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
|
+
}
|
|
1591
1599
|
const kind = snapshotKind(pathKind);
|
|
1592
1600
|
const row = await env.DB.prepare(
|
|
1593
1601
|
"SELECT mime,bytes FROM snapshots WHERE thread_id=? AND kind=?"
|
|
@@ -1635,13 +1643,20 @@ async function route(
|
|
|
1635
1643
|
threadId,
|
|
1636
1644
|
kind
|
|
1637
1645
|
),
|
|
1638
|
-
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,
|
|
1639
1648
|
now,
|
|
1640
1649
|
threadId
|
|
1641
1650
|
),
|
|
1642
1651
|
]);
|
|
1643
1652
|
return json({ ok: true, deleted: deleted.meta.changes });
|
|
1644
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
|
+
}
|
|
1645
1660
|
await limit(env, `${project}:snapshot:${user.id}`, 60, 600);
|
|
1646
1661
|
const snapshot = await readSnapshot(
|
|
1647
1662
|
request,
|
|
@@ -1657,8 +1672,9 @@ async function route(
|
|
|
1657
1672
|
snapshotCap(config.maxSnapshotBytes),
|
|
1658
1673
|
true
|
|
1659
1674
|
),
|
|
1660
|
-
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(
|
|
1661
1676
|
now,
|
|
1677
|
+
threadId,
|
|
1662
1678
|
threadId
|
|
1663
1679
|
),
|
|
1664
1680
|
]);
|
|
@@ -1774,35 +1790,20 @@ async function route(
|
|
|
1774
1790
|
);
|
|
1775
1791
|
check(typeof data.resolved === "boolean", 400, "Invalid resolved state.");
|
|
1776
1792
|
const becameResolved = !thread.resolved && data.resolved;
|
|
1777
|
-
|
|
1778
|
-
"
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
project,
|
|
1792
|
-
webhookEvent(
|
|
1793
|
-
"comment.resolved",
|
|
1794
|
-
project,
|
|
1795
|
-
repo,
|
|
1796
|
-
branch,
|
|
1797
|
-
threadId,
|
|
1798
|
-
thread.page,
|
|
1799
|
-
thread.anchor,
|
|
1800
|
-
user.name,
|
|
1801
|
-
null,
|
|
1802
|
-
origin || url.origin
|
|
1803
|
-
)
|
|
1804
|
-
).catch(() => {})
|
|
1805
|
-
);
|
|
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));
|
|
1806
1807
|
return json({ ok: true });
|
|
1807
1808
|
}
|
|
1808
1809
|
if (
|
|
@@ -1820,6 +1821,18 @@ async function route(
|
|
|
1820
1821
|
now,
|
|
1821
1822
|
threadId
|
|
1822
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),
|
|
1823
1836
|
...retainReviewerComments(
|
|
1824
1837
|
env.DB,
|
|
1825
1838
|
project,
|
|
@@ -1832,24 +1845,7 @@ async function route(
|
|
|
1832
1845
|
409,
|
|
1833
1846
|
"This thread has reached 200 comments. Start another thread."
|
|
1834
1847
|
);
|
|
1835
|
-
ctx.waitUntil(
|
|
1836
|
-
dispatchWebhook(
|
|
1837
|
-
env,
|
|
1838
|
-
project,
|
|
1839
|
-
webhookEvent(
|
|
1840
|
-
"comment.replied",
|
|
1841
|
-
project,
|
|
1842
|
-
repo,
|
|
1843
|
-
branch,
|
|
1844
|
-
threadId,
|
|
1845
|
-
thread.page,
|
|
1846
|
-
thread.anchor,
|
|
1847
|
-
user.name,
|
|
1848
|
-
text,
|
|
1849
|
-
origin || url.origin
|
|
1850
|
-
)
|
|
1851
|
-
).catch(() => {})
|
|
1852
|
-
);
|
|
1848
|
+
ctx.waitUntil(drainWebhooks(env));
|
|
1853
1849
|
return json({ ok: true, id: replyId }, 201);
|
|
1854
1850
|
}
|
|
1855
1851
|
check(commentId, 405, "Method not allowed.");
|
|
@@ -1954,6 +1950,7 @@ const settingsFor = new WeakMap<Env, Env>();
|
|
|
1954
1950
|
export default {
|
|
1955
1951
|
async scheduled(_event, env, ctx) {
|
|
1956
1952
|
ctx.waitUntil(maintain(env.DB, true));
|
|
1953
|
+
ctx.waitUntil(drainWebhooks(paletteEnv(env)));
|
|
1957
1954
|
},
|
|
1958
1955
|
async fetch(request, settings, ctx) {
|
|
1959
1956
|
// Deployments configured before the rename set KOMO_ names. One copy per
|
|
@@ -2044,3 +2041,5 @@ export default {
|
|
|
2044
2041
|
return new Response(response.body, { status: response.status, headers });
|
|
2045
2042
|
},
|
|
2046
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;
|
package/server/node-webhooks.ts
CHANGED
|
@@ -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) => {
|