@lengkapp/edge 0.0.22 → 0.0.23

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.22
2
+ * @lengkapp/edge v0.0.23
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -30,4 +30,4 @@
30
30
  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31
31
  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
32
  */
33
- !function(){"use strict";var e=document,t=window,r="ax-app-style",a=["@keyframes ax-spin{to{transform:rotate(360deg)}}","@keyframes ax-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}","@keyframes ax-fade-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}",".ax-state,.ax-state *,.ax-state *::before,.ax-state *::after{box-sizing:border-box;margin:0;padding:0}",".ax-state{","display:flex;flex-direction:column;align-items:center;justify-content:center;","gap:calc(var(--pad,1) * 12px);","width:100%;min-width:min(240px,100%);min-height:240px;","padding:calc(var(--pad,1) * 20px);","border:1px dashed var(--bd,#e4e4e7);border-radius:var(--br,10px);","background:inherit;","color:var(--fg2,#71717a);","font-family:var(--font,system-ui,-apple-system,sans-serif);","font-size:var(--fs,14px);line-height:1.45;text-align:center;","transition:var(--t,all .25s ease);","-webkit-user-select:none;user-select:none;","}",".ax-state--loading,.ax-state--skeleton{animation:ax-fade-in 220ms ease-out both}",".ax-state svg{display:block;flex:0 0 auto;width:24px;height:24px}",".ax-state--loading svg{color:var(--p,#6d28d9);animation:ax-spin .85s linear infinite}",".ax-state--skeleton{align-items:stretch}",".ax-skel{display:flex;flex-direction:column;gap:calc(var(--pad,1) * 10px);width:100%}",".ax-skel-line{","height:12px;border-radius:calc(var(--br,10px) / 2);","background-color:transparent;","background-image:linear-gradient(90deg,","var(--sk1,#e4e4e7) 25%,var(--sk2,#f4f4f5) 37%,var(--sk1,#e4e4e7) 63%);","background-size:400% 100%;","animation:ax-shimmer 1.4s ease-in-out infinite;","}",".ax-state-text{font-size:var(--fs,14px);color:var(--fg2,#71717a);max-width:34ch}",".ax-state--skeleton .ax-state-text{align-self:center}",".ax-state--error{border-style:solid;color:var(--fg,#18181b);animation:ax-fade-in 200ms ease-out both}",".ax-state--error .ax-state-text{color:var(--err,#ef4444)}",".ax-state-btn{","display:inline-flex;align-items:center;justify-content:center;","width:44px;height:44px;padding:0;font:inherit;color:var(--fg,#18181b);","background:var(--card,#fff);border:1px solid var(--bd,#e4e4e7);","border-radius:calc(var(--br,10px) - 2px);","box-shadow:var(--sh1,0 1px 2px rgba(0,0,0,.04));","cursor:pointer;transition:var(--t,all .25s ease);","}",".ax-state-btn:hover{color:var(--p,#6d28d9);border-color:var(--p,#6d28d9);box-shadow:var(--sh2,0 4px 12px rgba(0,0,0,.08))}",".ax-state-btn:focus-visible{outline:2px solid var(--p,#6d28d9);outline-offset:2px}",".ax-state-btn:active{transform:scale(.96)}",".ax-state-btn svg{transition:transform .25s ease}",".ax-state-btn:hover svg{transform:rotate(-45deg)}",".ax-hide{display:none !important}","@media (prefers-reduced-motion:reduce){",".ax-state,.ax-state--loading,.ax-state--skeleton,.ax-state--error{animation:none}",".ax-state--loading svg{animation-duration:2s}",".ax-skel-line{animation:none;background-image:none;background-color:var(--sk1,#e4e4e7)}",".ax-state-btn:hover svg{transform:none}","::view-transition-old(root),::view-transition-new(root){animation:none}","}"].join("\n");!function(){if(!e.getElementById(r)){var t=e.createElement("style");t.id=r,t.appendChild(e.createTextNode(a)),(e.head||e.documentElement).appendChild(t)}}();var n,i=new Set,o=new WeakSet,s=new WeakMap,l=new WeakMap,c="[_get],[_post]",u=["_in","_out","_before","_after"];function f(t){return new URL(t,e.baseURI).href}function d(e){return new Promise(function(t){var r=new FileReader;r.onload=function(){t({name:e.name,type:e.type||"application/octet-stream",size:e.size,data:r.result})},r.onerror=function(){t({name:e.name,type:e.type,size:e.size,data:null})},r.readAsDataURL(e)})}function v(e){if(e.disabled)return Promise.resolve(void 0);var t=e.type;if("file"===t){var r=e.files;return r&&r.length?e.multiple?Promise.all(Array.prototype.map.call(r,d)):d(r[0]):Promise.resolve(e.multiple?[]:null)}return"checkbox"===t?Promise.resolve(e.checked?e.value||"on":null):"radio"===t?Promise.resolve(e.checked?e.value:null):"SELECT"===e.tagName&&e.multiple?Promise.resolve(Array.prototype.map.call(e.selectedOptions,function(e){return e.value})):Promise.resolve(e.value)}function x(t){var r,a=null,n=null;for(r=0;r<u.length;r++)if(t.hasAttribute(u[r])){a=u[r],n=t.getAttribute(u[r]);break}if(!a)return null;var i=t;if(n&&"this"!==n)try{i=e.querySelector(n)}catch(e){return null}return i&&i.parentNode?{mode:a,el:i,parent:i.parentNode}:null}function p(e,t){"_before"===e.mode||"_out"===e.mode?e.parent.insertBefore(t,e.el):e.parent.insertBefore(t,e.el.nextSibling)}function m(e){var t=l.get(e);t&&(t.node&&t.node.parentNode&&t.node.parentNode.removeChild(t.node),t.target&&t.target.removeAttribute("aria-busy"),l.delete(e))}function g(e,t){var r=e.querySelector(".ax-state-btn");r&&r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),k(t)})}function h(t){var r=e.createElement("div");return r.className="ax-state ax-state--error",r.setAttribute("role","alert"),r.innerHTML='<button type="button" class="ax-state-btn" aria-label="Retry"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw preview-icon" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg></button><span class="ax-state-text">'+String(t).replace(/[&<>"']/g,function(e){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[e]})+"</span>",r}function b(t){if("function"==typeof e.startViewTransition)try{return void e.startViewTransition(t)}catch(e){}t()}function k(t){var r=t.hasAttribute("_get"),a=t.getAttribute(r?"_get":"_post");if(null!==a&&x(t)){var n=s.get(t),o=new AbortController;s.set(t,o),n&&n.abort(),m(t);var u=0,d=setTimeout(function(){s.get(t)===o&&(function(t){var r=x(t);if(!r)return null;var a=(t.getAttribute("_loader")||"spinner").toLowerCase();if("none"===a||"off"===a||"false"===a)return null;r.el.classList.add("ax-hide"),r.el.setAttribute("aria-busy","true");var n=function(t){var r="skeleton"===t,a=e.createElement("div");return a.className="ax-state ax-state--"+(r?"skeleton":"loading"),a.setAttribute("role","status"),a.setAttribute("aria-live","polite"),a.setAttribute("aria-busy","true"),a.innerHTML=r?'<div class="ax-skel" aria-hidden="true"><span class="ax-skel-line" style="width:48%"></span><span class="ax-skel-line"></span><span class="ax-skel-line" style="width:86%"></span><span class="ax-skel-line" style="width:64%"></span></div><span class="ax-state-text">Loading…</span>':'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle preview-icon" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg><span class="ax-state-text">Loading…</span>',a}(a);p(r,n),l.set(t,{node:n,target:r.el})}(t),u=performance.now())},100),k=parseInt(t.getAttribute("_timeout"),10);k>0||(k=2e4);var w={method:r?"GET":"POST",credentials:"same-origin",headers:{"X-Requested-With":"XMLHttpRequest"},redirect:"follow",referrerPolicy:"same-origin",signal:o.signal},_=setTimeout(function(){o.abort()},k),P=Promise.resolve();if(!r)if(t.hasAttribute("_json"))P=function(t){var r=(t.getAttribute("_json")||"").trim();if(!r)return Promise.resolve(null);var a=r.split(",").map(function(e){return e.trim()}).filter(Boolean);if(!a.length)return Promise.resolve(null);var n=t.closest("form")||e;return Promise.all(a.map(function(t){return function(t,r){var a=e.getElementById(r);if(a&&t.contains(a))return v(a);var n=function(e,t){for(var r=e.querySelectorAll("input[name],select[name],textarea[name]"),a=[],n=0;n<r.length;n++)r[n].name===t&&a.push(r[n]);return a}(t,r);if(!n.length)return Promise.resolve(void 0);if(1===n.length)return v(n[0]);var i=n[0];if("radio"===i.type){for(var o=0;o<n.length;o++)if(n[o].checked)return Promise.resolve(n[o].value);return Promise.resolve(null)}if("checkbox"===i.type){for(var s=[],l=0;l<n.length;l++)n[l].checked&&!n[l].disabled&&s.push(n[l].value||"on");return Promise.resolve(s)}return v(i)}(n,t).then(function(e){return[t,e]})})).then(function(e){for(var t={},r=0;r<e.length;r++)void 0!==e[r][1]&&(t[e[r][0]]=e[r][1]);return JSON.stringify(t)})}(t).then(function(e){null!==e&&(w.headers["Content-Type"]="application/json",w.body=e)});else if(t.hasAttribute("_form")){var T=function(t){var r,a=t.getAttribute("_form");try{r=e.querySelector(a)||e.getElementById(a)}catch(e){r=null}return r?new FormData(r):null}(t);T&&(w.body=T)}P.then(function(){return fetch(a,w)}).then(function(e){if(clearTimeout(_),!e.ok){var t=new Error("HTTP "+e.status);throw t.status=e.status,t}return e.text()}).then(function(r){clearTimeout(d),M(function(){var a=function(){m(t),r&&function(t,r){var a=x(t);if(a){var n=e.createElement("template");n.innerHTML=r;var o,s,l=n.content,u=[],d=[];for(s=l.querySelectorAll("script"),o=0;o<s.length;o++)u.push(s[o]),s[o].parentNode.removeChild(s[o]);for(s=l.querySelectorAll(c),o=0;o<s.length;o++)d.push(s[o]);var v=a.el,p=a.parent;if("_in"===a.mode){for(;v.firstChild;)v.removeChild(v.firstChild);v.appendChild(l),v.classList.remove("ax-hide")}else"_out"===a.mode?p.replaceChild(l,v):"_before"===a.mode?(p.insertBefore(l,v),v.classList.remove("ax-hide")):(p.insertBefore(l,v.nextSibling),v.classList.remove("ax-hide"));for(o=0;o<d.length;o++)y(d[o]);!function(t){for(var r=0;r<t.length;r++){var a=t[r],n=a.getAttribute("src");if(n){if(n=f(n),i.has(n))continue;i.add(n)}for(var o=e.createElement("script"),s=0;s<a.attributes.length;s++)o.setAttribute(a.attributes[s].name,a.attributes[s].value);n||(o.text=a.text),o.async=!1,e.head.appendChild(o)}}(u)}}(t,r)};l.get(t)?b(a):a()})}).catch(function(e){var r;clearTimeout(_),clearTimeout(d),s.get(t)===o&&(r=e&&"AbortError"===e.name?"Request timed out":e&&e.message?e.message:"Request failed",e&&"AbortError"!==e.name&&console.error("[app]",e),M(function(){!function(e,t){var r=l.get(e),a=function(){if(r&&r.node&&r.node.parentNode){var a=r.node,n=h(t);a.parentNode.replaceChild(n,a),r.target&&r.target.removeAttribute("aria-busy"),l.set(e,{node:n,target:r.target}),g(n,e)}else{var i=x(e);if(!i)return;i.el.classList.add("ax-hide"),i.el.setAttribute("aria-busy","true");var o=h(t);p(i,o),l.set(e,{node:o,target:i.el}),g(o,e)}};r?b(a):a()}(t,r)}))})}function M(e){if(0!==u){var r=Math.max(0,240-(performance.now()-u));0===r?s.get(t)===o&&e():setTimeout(function(){s.get(t)===o&&e()},r)}else s.get(t)===o&&e()}}function w(e){var t=e.getAttribute("_trigger");"load"===t||"visible"===t?k(e):e.addEventListener(t,function(t){if("click"===t.type){var r=e.tagName;"BUTTON"!==r&&"A"!==r||t.preventDefault()}k(e)})}function y(e){o.has(e)||(o.add(e),"click"!==(e.getAttribute("_trigger")||"click")&&("IntersectionObserver"in t?(n||(n=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++)e[t].isIntersecting&&(n.unobserve(e[t].target),w(e[t].target))},{rootMargin:"300px"})),n.observe(e)):w(e)))}function _(e){for(var t=e.querySelectorAll(c),r=0;r<t.length;r++)y(t[r])}e.addEventListener("click",function(e){if(!e.target.closest(".ax-state")){var t=e.target.closest(c);if(t&&"click"===(t.getAttribute("_trigger")||"click")){var r=t.tagName;"BUTTON"!==r&&"A"!==r||e.preventDefault(),k(t)}}},!0),function(){for(var t=e.querySelectorAll("script[src]"),r=0;r<t.length;r++)i.add(f(t[r].src));"loading"===e.readyState?e.addEventListener("DOMContentLoaded",function(){_(e)}):_(e)}()}();
33
+ !function(){"use strict";var e=document,t=window,r="ax-app-style",a=["@keyframes ax-spin{to{transform:rotate(360deg)}}","@keyframes ax-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}","@keyframes ax-fade-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}",".ax-state,.ax-state *,.ax-state *::before,.ax-state *::after{box-sizing:border-box;margin:0;padding:0}",".ax-state{","display:flex;flex-direction:column;align-items:center;justify-content:center;","gap:calc(var(--pad,1) * 12px);","width:100%;min-width:min(240px,100%);min-height:240px;","padding:calc(var(--pad,1) * 20px);","border:1px dashed var(--bd,#e4e4e7);border-radius:var(--br,10px);","background:inherit;","color:var(--fg2,#71717a);","font-family:var(--font,system-ui,-apple-system,sans-serif);","font-size:var(--fs,14px);line-height:1.45;text-align:center;","transition:var(--t,all .25s ease);","-webkit-user-select:none;user-select:none;","}",".ax-state--loading,.ax-state--skeleton{animation:ax-fade-in 220ms ease-out both}",".ax-state svg{display:block;flex:0 0 auto;width:24px;height:24px}",".ax-state--loading svg{color:var(--p,#6d28d9);animation:ax-spin .85s linear infinite}",".ax-state--skeleton{align-items:stretch}",".ax-skel{display:flex;flex-direction:column;gap:calc(var(--pad,1) * 10px);width:100%}",".ax-skel-line{","height:12px;border-radius:calc(var(--br,10px) / 2);","background-color:transparent;","background-image:linear-gradient(90deg,","var(--sk1,#e4e4e7) 25%,var(--sk2,#f4f4f5) 37%,var(--sk1,#e4e4e7) 63%);","background-size:400% 100%;","animation:ax-shimmer 1.4s ease-in-out infinite;","}",".ax-state-text{font-size:var(--fs,14px);color:var(--fg2,#71717a);max-width:34ch}",".ax-state--skeleton .ax-state-text{align-self:center}",".ax-state--error{border-style:solid;color:var(--fg,#18181b);animation:ax-fade-in 200ms ease-out both}",".ax-state--error .ax-state-text{color:var(--err,#ef4444)}",".ax-state-btn{","display:inline-flex;align-items:center;justify-content:center;","width:44px;height:44px;padding:0;font:inherit;color:var(--fg,#18181b);","background:var(--card,#fff);border:1px solid var(--bd,#e4e4e7);","border-radius:calc(var(--br,10px) - 2px);","box-shadow:var(--sh1,0 1px 2px rgba(0,0,0,.04));","cursor:pointer;transition:var(--t,all .25s ease);","}",".ax-state-btn:hover{color:var(--p,#6d28d9);border-color:var(--p,#6d28d9);box-shadow:var(--sh2,0 4px 12px rgba(0,0,0,.08))}",".ax-state-btn:focus-visible{outline:2px solid var(--p,#6d28d9);outline-offset:2px}",".ax-state-btn:active{transform:scale(.96)}",".ax-state-btn svg{transition:transform .25s ease}",".ax-state-btn:hover svg{transform:rotate(-45deg)}",".ax-hide{display:none !important}","@media (prefers-reduced-motion:reduce){",".ax-state,.ax-state--loading,.ax-state--skeleton,.ax-state--error{animation:none}",".ax-state--loading svg{animation-duration:2s}",".ax-skel-line{animation:none;background-image:none;background-color:var(--sk1,#e4e4e7)}",".ax-state-btn:hover svg{transform:none}","::view-transition-old(root),::view-transition-new(root){animation:none}","}"].join("\n");!function(){if(!e.getElementById(r)){var t=e.createElement("style");t.id=r,t.appendChild(e.createTextNode(a)),(e.head||e.documentElement).appendChild(t)}}();var n,i=new Set,o=new WeakSet,s=new WeakMap,l=new WeakMap,u="[_get],[_post],[_go],[_open]",c=["_in","_out","_before","_after"];function f(t){return new URL(t,e.baseURI).href}function d(e){return new Promise(function(t){var r=new FileReader;r.onload=function(){t({name:e.name,type:e.type||"application/octet-stream",size:e.size,data:r.result})},r.onerror=function(){t({name:e.name,type:e.type,size:e.size,data:null})},r.readAsDataURL(e)})}function v(e){if(e.disabled)return Promise.resolve(void 0);var t=e.type;if("file"===t){var r=e.files;return r&&r.length?e.multiple?Promise.all(Array.prototype.map.call(r,d)):d(r[0]):Promise.resolve(e.multiple?[]:null)}return"checkbox"===t?Promise.resolve(e.checked?e.value||"on":null):"radio"===t?Promise.resolve(e.checked?e.value:null):"SELECT"===e.tagName&&e.multiple?Promise.resolve(Array.prototype.map.call(e.selectedOptions,function(e){return e.value})):Promise.resolve(e.value)}function p(t){var r,a=null,n=null;for(r=0;r<c.length;r++)if(t.hasAttribute(c[r])){a=c[r],n=t.getAttribute(c[r]);break}if(!a)return null;var i=t;if(n&&"this"!==n)try{i=e.querySelector(n)}catch(e){return null}return i&&i.parentNode?{mode:a,el:i,parent:i.parentNode}:null}function x(e,t){"_before"===e.mode||"_out"===e.mode?e.parent.insertBefore(t,e.el):e.parent.insertBefore(t,e.el.nextSibling)}function m(e){var t=l.get(e);t&&(t.node&&t.node.parentNode&&t.node.parentNode.removeChild(t.node),t.target&&t.target.removeAttribute("aria-busy"),l.delete(e))}function g(e,t){var r=e.querySelector(".ax-state-btn");r&&r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),k(t)})}function h(t){var r=e.createElement("div");return r.className="ax-state ax-state--error",r.setAttribute("role","alert"),r.innerHTML='<button type="button" class="ax-state-btn" aria-label="Retry"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw preview-icon" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg></button><span class="ax-state-text">'+String(t).replace(/[&<>"']/g,function(e){return{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[e]})+"</span>",r}function b(t){if("function"==typeof e.startViewTransition)try{return void e.startViewTransition(t)}catch(e){}t()}function k(r){if(function(e){return e.hasAttribute("_go")||e.hasAttribute("_open")}(r))!function(e){var r=function(e){var t=e.hasAttribute("_go")?e.getAttribute("_go"):e.getAttribute("_open");if(!(t=(t||"").trim())||"this"===t){var r=e.getAttribute("href");r&&(t=r)}return t?f(t):""}(e);if(r)if(e.hasAttribute("_go"))t.location.assign(r);else{var a=t.open(r,"_blank","noopener,noreferrer");a&&(a.opener=null)}}(r);else{var a=r.hasAttribute("_get"),n=r.getAttribute(a?"_get":"_post");if(null!==n&&p(r)){var o=s.get(r),c=new AbortController;s.set(r,c),o&&o.abort(),m(r);var d=0,k=setTimeout(function(){s.get(r)===c&&(function(t){var r=p(t);if(!r)return null;var a=(t.getAttribute("_loader")||"spinner").toLowerCase();if("none"===a||"off"===a||"false"===a)return null;r.el.classList.add("ax-hide"),r.el.setAttribute("aria-busy","true");var n=function(t){var r="skeleton"===t,a=e.createElement("div");return a.className="ax-state ax-state--"+(r?"skeleton":"loading"),a.setAttribute("role","status"),a.setAttribute("aria-live","polite"),a.setAttribute("aria-busy","true"),a.innerHTML=r?'<div class="ax-skel" aria-hidden="true"><span class="ax-skel-line" style="width:48%"></span><span class="ax-skel-line"></span><span class="ax-skel-line" style="width:86%"></span><span class="ax-skel-line" style="width:64%"></span></div><span class="ax-state-text">Loading…</span>':'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle preview-icon" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg><span class="ax-state-text">Loading…</span>',a}(a);x(r,n),l.set(t,{node:n,target:r.el})}(r),d=performance.now())},100),w=parseInt(r.getAttribute("_timeout"),10);w>0||(w=2e4);var _={method:a?"GET":"POST",credentials:"same-origin",headers:{"X-Requested-With":"XMLHttpRequest"},redirect:"follow",referrerPolicy:"same-origin",signal:c.signal},P=setTimeout(function(){c.abort()},w),T=Promise.resolve();if(!a)if(r.hasAttribute("_json"))T=function(t){var r=(t.getAttribute("_json")||"").trim();if(!r)return Promise.resolve(null);var a=r.split(",").map(function(e){return e.trim()}).filter(Boolean);if(!a.length)return Promise.resolve(null);var n=t.closest("form")||e;return Promise.all(a.map(function(t){return function(t,r){var a=e.getElementById(r);if(a&&t.contains(a))return v(a);var n=function(e,t){for(var r=e.querySelectorAll("input[name],select[name],textarea[name]"),a=[],n=0;n<r.length;n++)r[n].name===t&&a.push(r[n]);return a}(t,r);if(!n.length)return Promise.resolve(void 0);if(1===n.length)return v(n[0]);var i=n[0];if("radio"===i.type){for(var o=0;o<n.length;o++)if(n[o].checked)return Promise.resolve(n[o].value);return Promise.resolve(null)}if("checkbox"===i.type){for(var s=[],l=0;l<n.length;l++)n[l].checked&&!n[l].disabled&&s.push(n[l].value||"on");return Promise.resolve(s)}return v(i)}(n,t).then(function(e){return[t,e]})})).then(function(e){for(var t={},r=0;r<e.length;r++)void 0!==e[r][1]&&(t[e[r][0]]=e[r][1]);return JSON.stringify(t)})}(r).then(function(e){null!==e&&(_.headers["Content-Type"]="application/json",_.body=e)});else if(r.hasAttribute("_form")){var M=function(t){var r,a=t.getAttribute("_form");try{r=e.querySelector(a)||e.getElementById(a)}catch(e){r=null}return r?new FormData(r):null}(r);M&&(_.body=M)}T.then(function(){return fetch(n,_)}).then(function(e){if(clearTimeout(P),!e.ok){var t=new Error("HTTP "+e.status);throw t.status=e.status,t}return e.text()}).then(function(t){clearTimeout(k),A(function(){var a=function(){m(r),t&&function(t,r){var a=p(t);if(a){var n=e.createElement("template");n.innerHTML=r;var o,s,l=n.content,c=[],d=[];for(s=l.querySelectorAll("script"),o=0;o<s.length;o++)c.push(s[o]),s[o].parentNode.removeChild(s[o]);for(s=l.querySelectorAll(u),o=0;o<s.length;o++)d.push(s[o]);var v=a.el,x=a.parent;if("_in"===a.mode){for(;v.firstChild;)v.removeChild(v.firstChild);v.appendChild(l),v.classList.remove("ax-hide")}else"_out"===a.mode?x.replaceChild(l,v):"_before"===a.mode?(x.insertBefore(l,v),v.classList.remove("ax-hide")):(x.insertBefore(l,v.nextSibling),v.classList.remove("ax-hide"));for(o=0;o<d.length;o++)y(d[o]);!function(t){for(var r=0;r<t.length;r++){var a=t[r],n=a.getAttribute("src");if(n){if(n=f(n),i.has(n))continue;i.add(n)}for(var o=e.createElement("script"),s=0;s<a.attributes.length;s++)o.setAttribute(a.attributes[s].name,a.attributes[s].value);n||(o.text=a.text),o.async=!1,e.head.appendChild(o)}}(c)}}(r,t)};l.get(r)?b(a):a()})}).catch(function(e){var t;clearTimeout(P),clearTimeout(k),s.get(r)===c&&(t=e&&"AbortError"===e.name?"Request timed out":e&&e.message?e.message:"Request failed",e&&"AbortError"!==e.name&&console.error("[app]",e),A(function(){!function(e,t){var r=l.get(e),a=function(){if(r&&r.node&&r.node.parentNode){var a=r.node,n=h(t);a.parentNode.replaceChild(n,a),r.target&&r.target.removeAttribute("aria-busy"),l.set(e,{node:n,target:r.target}),g(n,e)}else{var i=p(e);if(!i)return;i.el.classList.add("ax-hide"),i.el.setAttribute("aria-busy","true");var o=h(t);x(i,o),l.set(e,{node:o,target:i.el}),g(o,e)}};r?b(a):a()}(r,t)}))})}}function A(e){if(0!==d){var t=Math.max(0,240-(performance.now()-d));0===t?s.get(r)===c&&e():setTimeout(function(){s.get(r)===c&&e()},t)}else s.get(r)===c&&e()}}function w(e){var t=e.getAttribute("_trigger");"load"===t||"visible"===t?k(e):e.addEventListener(t,function(t){if("click"===t.type){var r=e.tagName;"BUTTON"!==r&&"A"!==r||t.preventDefault()}k(e)})}function y(e){o.has(e)||(o.add(e),"click"!==(e.getAttribute("_trigger")||"click")&&("IntersectionObserver"in t?(n||(n=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++)e[t].isIntersecting&&(n.unobserve(e[t].target),w(e[t].target))},{rootMargin:"300px"})),n.observe(e)):w(e)))}function _(e){for(var t=e.querySelectorAll(u),r=0;r<t.length;r++)y(t[r])}e.addEventListener("click",function(e){if(!e.target.closest(".ax-state")){var t=e.target.closest(u);if(t&&"click"===(t.getAttribute("_trigger")||"click")&&!(t.hasAttribute("_go")&&"A"===t.tagName&&t.getAttribute("href")&&(0!==e.button||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey))){var r=t.tagName;"BUTTON"!==r&&"A"!==r||e.preventDefault(),k(t)}}},!0),function(){for(var t=e.querySelectorAll("script[src]"),r=0;r<t.length;r++)i.add(f(t[r].src));"loading"===e.readyState?e.addEventListener("DOMContentLoaded",function(){_(e)}):_(e)}()}();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.22
2
+ * @lengkapp/edge v0.0.23
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -116,6 +116,16 @@ export class Context<E = any> {
116
116
  status?: number,
117
117
  headers?: Record<string, string>,
118
118
  ): Response;
119
+
120
+ /**
121
+ * Send a redirect. Preserves any `Set-Cookie` headers and other
122
+ * headers already set on this Context, unlike the global
123
+ * `Response.redirect()`.
124
+ *
125
+ * @param location Absolute or relative URL to redirect to.
126
+ * @param status HTTP status code (default 302).
127
+ */
128
+ redirect(location: string, status?: number): Response;
119
129
  }
120
130
 
121
131
  /* ---------------- Edge ---------------- */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.22
2
+ * @lengkapp/edge v0.0.23
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -30,4 +30,4 @@
30
30
  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31
31
  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
32
  */
33
- var t=Object.defineProperty,e=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,o=(e,n)=>{for(var r in n)t(e,r,{get:n[r],enumerable:!0})},i=(o,i,s,a)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let c of n(i))r.call(o,c)||c===s||t(o,c,{get:()=>i[c],enumerable:!(a=e(i,c))||a.enumerable});return o},s=e=>i(t({},"__esModule",{value:!0}),e),a={};function c(t){const e=[],n=t.length;let r=1;for(let o=1;o<=n;o++)o!==n&&47!==t.charCodeAt(o)||(o>r&&e.push(t.slice(r,o)),r=o+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}o(a,{Context:()=>T,Edge:()=>Edge,Fragment:()=>Fragment,jsx:()=>jsx,renderToString:()=>renderToString}),module.exports=s(a);var u=new Set(["__proto__","constructor","prototype"]);function f(t){return"string"==typeof t&&!u.has(t)}function h(t){const e=JSON.parse(t);if(e&&"object"==typeof e)for(const t of Object.keys(e))if(!f(t))throw new SyntaxError("Forbidden key in JSON payload");return e}function p(t){const e=t.length;let n=0;for(;n<e;){const e=t.charCodeAt(n);if(38===e||60===e||62===e||34===e||39===e)break;n++}if(n===e)return t;let r=t.slice(0,n),o=n;for(;n<e;n++){const e=t.charCodeAt(n);let i;if(38===e)i="&amp;";else if(60===e)i="&lt;";else if(62===e)i="&gt;";else if(34===e)i="&quot;";else{if(39!==e)continue;i="&#039;"}n>o&&(r+=t.slice(o,n)),r+=i,o=n+1}return o<e&&(r+=t.slice(o)),r}var Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n,__isJSX:!0}}var d=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),y=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]),m=new Set(["animation-iteration-count","border-image-outset","border-image-slice","border-image-width","box-flex","box-flex-group","box-ordinal-group","column-count","columns","flex","flex-grow","flex-positive","flex-shrink","flex-negative","flex-order","grid-row","grid-row-end","grid-row-span","grid-row-start","grid-column","grid-column-end","grid-column-span","grid-column-start","font-weight","line-clamp","line-height","opacity","order","orphans","tab-size","widows","z-index","zoom","fill-opacity","flood-opacity","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width"]),g=new Map,w=2e3;function b(t){const e=g.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return g.size>=w&&g.clear(),g.set(t,n),n}function A(t){if(!t||"object"!=typeof t)return"";let e="";for(const n in t){if(!Object.prototype.hasOwnProperty.call(t,n))continue;const r=b(n);let o=t[n];"number"!=typeof o||m.has(r)||(o+="px"),e&&(e+=";"),e+=r+":"+o}return e}var C=new Set(["href","src","xlink:href","action","formaction"]);function renderToString(t){if(null==t||"boolean"==typeof t)return"";const e=typeof t;if("string"===e)return p(t);if("number"===e)return p(String(t));if(Array.isArray(t)){let e="";for(let n=0;n<t.length;n++)e+=renderToString(t[n]);return e}if(!t.__isJSX)return p(String(t));const{type:n,props:r,children:o}=t;if(n===Fragment){let t="";for(let e=0;e<o.length;e++)t+=renderToString(o[e]);return t}if("symbol"==typeof n)return"";if("function"==typeof n)return renderToString(n({...r,children:o}));if("string"!=typeof n||!/^[A-Za-z][A-Za-z0-9-]*$/.test(n))return"";const i=d.has(n);let s="";for(const t in r){if(!Object.prototype.hasOwnProperty.call(r,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=r[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let n=t;if("className"===t||"class"===t?n="class":"htmlFor"!==t&&"for"!==t||(n="for"),f(n)&&!/^on/i.test(n)&&/^[A-Za-z_:][A-Za-z0-9_:.-]*$/.test(n)){if(C.has(n)){const t=String(e).replace(/[\u0000-\u0020]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("vbscript:")||t.startsWith("data:text/html"))continue}if(y.has(n))!0===e&&(s+=" "+n);else if(!0!==e){if("style"===n&&"object"==typeof e){const t=A(e);t&&(s+=' style="'+p(t)+'"');continue}s+=" "+n+'="'+p(String(e))+'"'}else s+=" "+n+'="true"'}}let a="";const c=r.dangerouslySetInnerHTML;if(c&&null!=c.__html)a=c.__html;else for(let t=0;t<o.length;t++)a+=renderToString(o[t]);return i?"<"+n+s+"/>":"<"+n+s+">"+a+"</"+n+">"}var T=class{constructor(t,e,n,r={},o=null){this.req=t,this.env=e,this.executionCtx=n,this.params=r,this.status=200,this.headers=new Headers,this.headers.set("X-Content-Type-Options","nosniff"),this.headers.set("X-Frame-Options","DENY"),this.headers.set("Referrer-Policy","strict-origin-when-cross-origin"),this.headers.set("Permissions-Policy","geolocation=(), microphone=(), camera=()"),this.t=t.headers.get("Cookie")||"",this.o=null,this.url=o||new URL(t.url)}i(){if(null!==this.o)return this.o;const t=Object.create(null),e=this.t;if(e){let n=0;const r=e.length;for(;n<r;){let o=e.indexOf(";",n);-1===o&&(o=r);let i=n,s=o;for(;i<s&&32===e.charCodeAt(i);)i++;for(;s>i&&32===e.charCodeAt(s-1);)s--;if(s>i){const n=e.indexOf("=",i);if(n>i&&n<s)try{const r=decodeURIComponent(e.slice(i,n));f(r)&&(t[r]=decodeURIComponent(e.slice(n+1,s)))}catch{}}n=o+1}}return this.o=t,t}getCookie(t){const e=this.i()[t];return void 0===e?null:e}get query(){return this.url.searchParams}setCookie(t,e,n={}){if("string"!=typeof t||!f(t)||/[\r\n;=]/.test(t))return;let r=encodeURIComponent(t)+"="+encodeURIComponent(e);n.path&&(r+="; Path="+n.path),n.domain&&(r+="; Domain="+n.domain),void 0!==n.maxAge&&(r+="; Max-Age="+n.maxAge),n.expires&&(r+="; Expires="+n.expires.toUTCString()),n.secure&&(r+="; Secure"),n.httpOnly&&(r+="; HttpOnly"),n.sameSite&&(r+="; SameSite="+n.sameSite),this.headers.append("Set-Cookie",r)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}l(t){if(!t)return this.headers;let e=!1;for(const n in t){e=!0;break}if(!e)return this.headers;const n=new Headers(this.headers);for(const e in t)Object.prototype.hasOwnProperty.call(t,e)&&n.set(e,t[e]);return n}text(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","text/plain; charset=utf-8"),new Response(t,{status:e,headers:r})}json(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","application/json; charset=utf-8"),new Response(JSON.stringify(t),{status:e,headers:r})}html(t,e=this.status,n={}){const r=this.l(n);r.has("Content-Type")||r.set("Content-Type","text/html; charset=utf-8");const o="string"==typeof t?t:t&&t.__isJSX||Array.isArray(t)?renderToString(t):null==t?"":String(t);return new Response(o,{status:e,headers:r})}},_=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},k=class{constructor(){this.root=new _}add(t,e,n){let r=this.root;const o=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0)){const t=n.slice(1);o.push(f(t)?t:"_p"+e),r.paramChild||(r.paramChild=new _),r=r.paramChild}else{r.children||(r.children=new Map);let t=r.children.get(n);t||(t=new _,r.children.set(n,t)),r=t}}r.handler={handler:e,options:n,paramNames:o}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:r,options:o,paramNames:i}=n,s=Object.create(null);for(let t=0;t<i.length;t++)s[i[t]]=e[t];return{handler:r,options:o,params:s}}u(t,e,n,r){if(n===e.length)return t.handler;const o=e[n],i=t.children;if(i){const t=i.get(o);if(t){const o=this.u(t,e,n+1,r);if(o)return o}}const s=t.paramChild;if(s){r.push(o);const t=this.u(s,e,n+1,r);if(t)return t;r.pop()}return null}},O=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],S=new Map;function E(t,e,n){const r=t+"|"+(e||"")+"|"+(n||""),o=Date.now(),i=S.get(r);if(i&&o-i<6e4)return!1;if(S.set(r,o),S.size>5e3)for(const[t,e]of S)o-e>6e4&&S.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(let t=0;t<O.length;t++)this.staticRoutes.set(O[t],new Map);this.dynamicTries={};for(let t=0;t<O.length;t++)this.dynamicTries[O[t]]=new k;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null,this.security={hashAuthTokens:!1,logSecurityEvents:!0,extraHeaders:null,trustedProxies:null}}h(t,e,n,r){if("boolean"==typeof n?n={auth:n}:"function"==typeof n&&(r=n,n={}),"function"!=typeof r)throw new Error("Handler for "+t+" "+e+" must be a function");const o={...this.defaults,...n||{}};-1===e.indexOf(":")?this.staticRoutes.get(t).set(l(e),{handler:r,options:o}):this.dynamicTries[t].add(c(e),r,o)}get(t,e,n){this.h("GET",t,e,n)}post(t,e,n){this.h("POST",t,e,n)}put(t,e,n){this.h("PUT",t,e,n)}delete(t,e,n){this.h("DELETE",t,e,n)}patch(t,e,n){this.h("PATCH",t,e,n)}options(t,e,n){this.h("OPTIONS",t,e,n)}head(t,e,n){this.h("HEAD",t,e,n)}scheduled(t){this.scheduledHandler=t}p(t,e,n){if(this.security.logSecurityEvents)try{const r=t.req.headers.get("CF-Connecting-IP")||"",o=t.url?t.url.pathname:"";if(!E(e,r,o))return;console.log(JSON.stringify({ts:(new Date).toISOString(),kind:"security",event:e,detail:"string"==typeof n?n:void 0,ip:r||void 0,method:t.req.method,path:o,ua:t.req.headers.get("User-Agent")||void 0}))}catch{}}async m(t,e){if(!e)return{ok:!0};const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return{ok:!1,reason:"missing_token"};const r=t.env[this.authKvBinding];if(!r)return{ok:!1,reason:"kv_unavailable"};let o,i=n;if(this.security.hashAuthTokens&&globalThis.crypto&&crypto.subtle){const t=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n)),e=new Uint8Array(t);let r="";for(let t=0;t<e.length;t++)r+=e[t].toString(16).padStart(2,"0");i="tok:"+r}try{o=await r.get(i)}catch{return{ok:!1,reason:"kv_error"}}if(!o)return{ok:!1,reason:"invalid_token"};if("object"==typeof e&&null!==e){let t;try{t=h(o)}catch{return{ok:!1,reason:"malformed_payload"}}if(t&&"object"==typeof t){if(null!=t.exp&&"number"==typeof t.exp&&t.exp<Date.now()/1e3)return{ok:!1,reason:"expired_token"};if(e.role&&t.role!==e.role)return{ok:!1,reason:"insufficient_role"};if(e.scopes){const n=Array.isArray(t.scopes)?t.scopes:[];for(let t=0;t<e.scopes.length;t++)if(-1===n.indexOf(e.scopes[t]))return{ok:!1,reason:"insufficient_scopes"}}}}return{ok:!0,payload:o}}async A(t,e){if(!e)return!0;const n=!0===e?{}:e,r=n.max??100,o=n.window??60,i="rl:"+(t.req.headers.get("CF-Connecting-IP")||"unknown"),s=t.env[this.rateLimitKvBinding];if(!s)return!0;let a;try{a=await s.get(i,"json")}catch{return!0}if(("number"!=typeof a||!isFinite(a)||a<0)&&(a=0),a>=r)return!1;a++;try{await s.put(i,JSON.stringify(a),{expirationTtl:o})}catch{}return!0}C(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors,r=t.req.headers.get("Origin"),o=n.origin;"*"===o?t.headers.set("Access-Control-Allow-Origin","*"):Array.isArray(o)?r&&-1!==o.indexOf(r)&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")):"string"==typeof o&&r===o&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")),t.headers.set("Access-Control-Allow-Methods",n.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"),t.headers.set("Access-Control-Allow-Headers",n.headers||"Content-Type, Authorization, X-CSRF-Token"),t.headers.set("Access-Control-Max-Age","86400")}T(t,e){e&&console.log(t.req.method+" "+t.req.url+" - "+t.status)}_(t,e){const n=t.headers.get("Accept-Encoding")||"";let r=null;if(-1!==n.indexOf("gzip")?r="gzip":-1!==n.indexOf("deflate")&&(r="deflate"),!r||!e.body)return e;const o=e.body.pipeThrough(new CompressionStream(r)),i=new Headers(e.headers);return i.set("Content-Encoding",r),i.set("Vary","Accept-Encoding"),i.delete("Content-Length"),new Response(o,{status:e.status,statusText:e.statusText,headers:i})}async k(t,e){return!e||"function"!=typeof e||!!await e(t)}async O(t){return await caches.default.match(t)||null}async S(t,e,n){if(!n)return;const r=!0===n?{}:n,o=r.ttl??3600,i=r.staleWhileRevalidate??0,s=e.clone();s.headers.set("Cache-Control","max-age="+o+(i>0?", stale-while-revalidate="+i:"")),s.headers.delete("Set-Cookie"),await caches.default.put(t,s)}async fetch(t,e,n){const r=new URL(t.url),o=l(r.pathname),i=t.method,s=this.staticRoutes.get(i);if(s){const i=s.get(o);if(i)return this.v(i.handler,i.options,t,e,n,Object.create(null),r)}const a=this.dynamicTries[i];if(a){const i=a.match(c(o));if(i)return this.v(i.handler,i.options,t,e,n,i.params,r)}return new Response("Not Found",{status:404})}async v(t,e,n,r,o,i,s){const a=new T(n,r,o,i,s),c=this.security.extraHeaders;if(c)for(const t in c)Object.prototype.hasOwnProperty.call(c,t)&&a.headers.set(t,c[t]);let l,u=!1;try{u=await this.k(a,e.validate)}catch(t){this.p(a,"validation_error",t&&t.message),u=!1}if(!u)return a.text("Validation failed",400);try{l=await this.m(a,e.auth)}catch(t){this.p(a,"auth_error",t&&t.message),l={ok:!1,reason:"auth_error"}}if(!("boolean"==typeof l?l:l&&l.ok))return this.p(a,"auth_failure",l&&l.reason||"denied"),a.text("Unauthorized",401);let f,h=!1;try{h=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),h=!1}if(!h)return this.p(a,"rate_limit_exceeded",null),a.text("Too Many Requests",429,{"Retry-After":"60"});if(e.cache&&"GET"===n.method){const t=await this.O(n);if(t)return this.C(a,e.cors),this.T(a,e.log),t}this.C(a,e.cors);try{const e=await t(a);f=e instanceof Response?e:e&&e.__isJSX?a.html(e):a.text("OK"),a.status=f.status}catch(t){this.p(a,"handler_error",t&&t.message),f=a.text("Internal Server Error",500),a.status=500}return e.cache&&"GET"===n.method&&200===f.status&&a.executionCtx.waitUntil(this.S(n,f.clone(),e.cache)),e.compress&&(f=this._(n,f)),this.T(a,e.log),f}};
33
+ var t=Object.defineProperty,e=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,o=(e,n)=>{for(var r in n)t(e,r,{get:n[r],enumerable:!0})},i=(o,i,s,a)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let c of n(i))r.call(o,c)||c===s||t(o,c,{get:()=>i[c],enumerable:!(a=e(i,c))||a.enumerable});return o},s=e=>i(t({},"__esModule",{value:!0}),e),a={};function c(t){const e=[],n=t.length;let r=1;for(let o=1;o<=n;o++)o!==n&&47!==t.charCodeAt(o)||(o>r&&e.push(t.slice(r,o)),r=o+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}o(a,{Context:()=>T,Edge:()=>Edge,Fragment:()=>Fragment,jsx:()=>jsx,renderToString:()=>renderToString}),module.exports=s(a);var u=new Set(["__proto__","constructor","prototype"]);function f(t){return"string"==typeof t&&!u.has(t)}function h(t){const e=JSON.parse(t);if(e&&"object"==typeof e)for(const t of Object.keys(e))if(!f(t))throw new SyntaxError("Forbidden key in JSON payload");return e}function p(t){const e=t.length;let n=0;for(;n<e;){const e=t.charCodeAt(n);if(38===e||60===e||62===e||34===e||39===e)break;n++}if(n===e)return t;let r=t.slice(0,n),o=n;for(;n<e;n++){const e=t.charCodeAt(n);let i;if(38===e)i="&amp;";else if(60===e)i="&lt;";else if(62===e)i="&gt;";else if(34===e)i="&quot;";else{if(39!==e)continue;i="&#039;"}n>o&&(r+=t.slice(o,n)),r+=i,o=n+1}return o<e&&(r+=t.slice(o)),r}var Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n,__isJSX:!0}}var d=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),y=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]),g=new Set(["animation-iteration-count","border-image-outset","border-image-slice","border-image-width","box-flex","box-flex-group","box-ordinal-group","column-count","columns","flex","flex-grow","flex-positive","flex-shrink","flex-negative","flex-order","grid-row","grid-row-end","grid-row-span","grid-row-start","grid-column","grid-column-end","grid-column-span","grid-column-start","font-weight","line-clamp","line-height","opacity","order","orphans","tab-size","widows","z-index","zoom","fill-opacity","flood-opacity","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width"]),m=new Map,w=2e3;function b(t){const e=m.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return m.size>=w&&m.clear(),m.set(t,n),n}function A(t){if(!t||"object"!=typeof t)return"";let e="";for(const n in t){if(!Object.prototype.hasOwnProperty.call(t,n))continue;const r=b(n);let o=t[n];"number"!=typeof o||g.has(r)||(o+="px"),e&&(e+=";"),e+=r+":"+o}return e}var C=new Set(["href","src","xlink:href","action","formaction"]);function renderToString(t){if(null==t||"boolean"==typeof t)return"";const e=typeof t;if("string"===e)return p(t);if("number"===e)return p(String(t));if(Array.isArray(t)){let e="";for(let n=0;n<t.length;n++)e+=renderToString(t[n]);return e}if(!t.__isJSX)return p(String(t));const{type:n,props:r,children:o}=t;if(n===Fragment){let t="";for(let e=0;e<o.length;e++)t+=renderToString(o[e]);return t}if("symbol"==typeof n)return"";if("function"==typeof n)return renderToString(n({...r,children:o}));if("string"!=typeof n||!/^[A-Za-z][A-Za-z0-9-]*$/.test(n))return"";const i=d.has(n);let s="";for(const t in r){if(!Object.prototype.hasOwnProperty.call(r,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=r[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let n=t;if("className"===t||"class"===t?n="class":"htmlFor"!==t&&"for"!==t||(n="for"),f(n)&&!/^on/i.test(n)&&/^[A-Za-z_:][A-Za-z0-9_:.-]*$/.test(n)){if(C.has(n)){const t=String(e).replace(/[\u0000-\u0020]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("vbscript:")||t.startsWith("data:text/html"))continue}if(y.has(n))!0===e&&(s+=" "+n);else if(!0!==e){if("style"===n&&"object"==typeof e){const t=A(e);t&&(s+=' style="'+p(t)+'"');continue}s+=" "+n+'="'+p(String(e))+'"'}else s+=" "+n+'="true"'}}let a="";const c=r.dangerouslySetInnerHTML;if(c&&null!=c.__html)a=c.__html;else for(let t=0;t<o.length;t++)a+=renderToString(o[t]);return i?"<"+n+s+"/>":"<"+n+s+">"+a+"</"+n+">"}var T=class{constructor(t,e,n,r={},o=null){this.req=t,this.env=e,this.executionCtx=n,this.params=r,this.status=200,this.headers=new Headers,this.headers.set("X-Content-Type-Options","nosniff"),this.headers.set("X-Frame-Options","DENY"),this.headers.set("Referrer-Policy","strict-origin-when-cross-origin"),this.headers.set("Permissions-Policy","geolocation=(), microphone=(), camera=()"),this.t=t.headers.get("Cookie")||"",this.o=null,this.url=o||new URL(t.url)}i(){if(null!==this.o)return this.o;const t=Object.create(null),e=this.t;if(e){let n=0;const r=e.length;for(;n<r;){let o=e.indexOf(";",n);-1===o&&(o=r);let i=n,s=o;for(;i<s&&32===e.charCodeAt(i);)i++;for(;s>i&&32===e.charCodeAt(s-1);)s--;if(s>i){const n=e.indexOf("=",i);if(n>i&&n<s)try{const r=decodeURIComponent(e.slice(i,n));f(r)&&(t[r]=decodeURIComponent(e.slice(n+1,s)))}catch{}}n=o+1}}return this.o=t,t}getCookie(t){const e=this.i()[t];return void 0===e?null:e}get query(){return this.url.searchParams}setCookie(t,e,n={}){if("string"!=typeof t||!f(t)||/[\r\n;=]/.test(t))return;let r=encodeURIComponent(t)+"="+encodeURIComponent(e);n.path&&(r+="; Path="+n.path),n.domain&&(r+="; Domain="+n.domain),void 0!==n.maxAge&&(r+="; Max-Age="+n.maxAge),n.expires&&(r+="; Expires="+n.expires.toUTCString()),n.secure&&(r+="; Secure"),n.httpOnly&&(r+="; HttpOnly"),n.sameSite&&(r+="; SameSite="+n.sameSite),this.headers.append("Set-Cookie",r)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}l(t){if(!t)return this.headers;let e=!1;for(const n in t){e=!0;break}if(!e)return this.headers;const n=new Headers(this.headers);for(const e in t)Object.prototype.hasOwnProperty.call(t,e)&&n.set(e,t[e]);return n}text(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","text/plain; charset=utf-8"),new Response(t,{status:e,headers:r})}json(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","application/json; charset=utf-8"),new Response(JSON.stringify(t),{status:e,headers:r})}html(t,e=this.status,n={}){const r=this.l(n);r.has("Content-Type")||r.set("Content-Type","text/html; charset=utf-8");const o="string"==typeof t?t:t&&t.__isJSX||Array.isArray(t)?renderToString(t):null==t?"":String(t);return new Response(o,{status:e,headers:r})}redirect(t,e=302){const n=new Headers(this.headers);return n.set("Location",String(t)),n.delete("Content-Type"),new Response(null,{status:e,headers:n})}},_=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},k=class{constructor(){this.root=new _}add(t,e,n){let r=this.root;const o=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0)){const t=n.slice(1);o.push(f(t)?t:"_p"+e),r.paramChild||(r.paramChild=new _),r=r.paramChild}else{r.children||(r.children=new Map);let t=r.children.get(n);t||(t=new _,r.children.set(n,t)),r=t}}r.handler={handler:e,options:n,paramNames:o}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:r,options:o,paramNames:i}=n,s=Object.create(null);for(let t=0;t<i.length;t++)s[i[t]]=e[t];return{handler:r,options:o,params:s}}u(t,e,n,r){if(n===e.length)return t.handler;const o=e[n],i=t.children;if(i){const t=i.get(o);if(t){const o=this.u(t,e,n+1,r);if(o)return o}}const s=t.paramChild;if(s){r.push(o);const t=this.u(s,e,n+1,r);if(t)return t;r.pop()}return null}},S=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],O=new Map;function E(t,e,n){const r=t+"|"+(e||"")+"|"+(n||""),o=Date.now(),i=O.get(r);if(i&&o-i<6e4)return!1;if(O.set(r,o),O.size>5e3)for(const[t,e]of O)o-e>6e4&&O.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(let t=0;t<S.length;t++)this.staticRoutes.set(S[t],new Map);this.dynamicTries={};for(let t=0;t<S.length;t++)this.dynamicTries[S[t]]=new k;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null,this.security={hashAuthTokens:!1,logSecurityEvents:!0,extraHeaders:null,trustedProxies:null}}h(t,e,n,r){if("boolean"==typeof n?n={auth:n}:"function"==typeof n&&(r=n,n={}),"function"!=typeof r)throw new Error("Handler for "+t+" "+e+" must be a function");const o={...this.defaults,...n||{}};-1===e.indexOf(":")?this.staticRoutes.get(t).set(l(e),{handler:r,options:o}):this.dynamicTries[t].add(c(e),r,o)}get(t,e,n){this.h("GET",t,e,n)}post(t,e,n){this.h("POST",t,e,n)}put(t,e,n){this.h("PUT",t,e,n)}delete(t,e,n){this.h("DELETE",t,e,n)}patch(t,e,n){this.h("PATCH",t,e,n)}options(t,e,n){this.h("OPTIONS",t,e,n)}head(t,e,n){this.h("HEAD",t,e,n)}scheduled(t){this.scheduledHandler=t}p(t,e,n){if(this.security.logSecurityEvents)try{const r=t.req.headers.get("CF-Connecting-IP")||"",o=t.url?t.url.pathname:"";if(!E(e,r,o))return;console.log(JSON.stringify({ts:(new Date).toISOString(),kind:"security",event:e,detail:"string"==typeof n?n:void 0,ip:r||void 0,method:t.req.method,path:o,ua:t.req.headers.get("User-Agent")||void 0}))}catch{}}async m(t,e){if(!e)return{ok:!0};const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return{ok:!1,reason:"missing_token"};const r=t.env[this.authKvBinding];if(!r)return{ok:!1,reason:"kv_unavailable"};let o,i=n;if(this.security.hashAuthTokens&&globalThis.crypto&&crypto.subtle){const t=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n)),e=new Uint8Array(t);let r="";for(let t=0;t<e.length;t++)r+=e[t].toString(16).padStart(2,"0");i="tok:"+r}try{o=await r.get(i)}catch{return{ok:!1,reason:"kv_error"}}if(!o)return{ok:!1,reason:"invalid_token"};if("object"==typeof e&&null!==e){let t;try{t=h(o)}catch{return{ok:!1,reason:"malformed_payload"}}if(t&&"object"==typeof t){if(null!=t.exp&&"number"==typeof t.exp&&t.exp<Date.now()/1e3)return{ok:!1,reason:"expired_token"};if(e.role&&t.role!==e.role)return{ok:!1,reason:"insufficient_role"};if(e.scopes){const n=Array.isArray(t.scopes)?t.scopes:[];for(let t=0;t<e.scopes.length;t++)if(-1===n.indexOf(e.scopes[t]))return{ok:!1,reason:"insufficient_scopes"}}}}return{ok:!0,payload:o}}async A(t,e){if(!e)return!0;const n=!0===e?{}:e,r=n.max??100,o=n.window??60,i="rl:"+(t.req.headers.get("CF-Connecting-IP")||"unknown"),s=t.env[this.rateLimitKvBinding];if(!s)return!0;let a;try{a=await s.get(i,"json")}catch{return!0}if(("number"!=typeof a||!isFinite(a)||a<0)&&(a=0),a>=r)return!1;a++;try{await s.put(i,JSON.stringify(a),{expirationTtl:o})}catch{}return!0}C(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors,r=t.req.headers.get("Origin"),o=n.origin;"*"===o?t.headers.set("Access-Control-Allow-Origin","*"):Array.isArray(o)?r&&-1!==o.indexOf(r)&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")):"string"==typeof o&&r===o&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")),t.headers.set("Access-Control-Allow-Methods",n.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"),t.headers.set("Access-Control-Allow-Headers",n.headers||"Content-Type, Authorization, X-CSRF-Token"),t.headers.set("Access-Control-Max-Age","86400")}T(t,e){e&&console.log(t.req.method+" "+t.req.url+" - "+t.status)}_(t,e){const n=t.headers.get("Accept-Encoding")||"";let r=null;if(-1!==n.indexOf("gzip")?r="gzip":-1!==n.indexOf("deflate")&&(r="deflate"),!r||!e.body)return e;const o=e.body.pipeThrough(new CompressionStream(r)),i=new Headers(e.headers);return i.set("Content-Encoding",r),i.set("Vary","Accept-Encoding"),i.delete("Content-Length"),new Response(o,{status:e.status,statusText:e.statusText,headers:i})}async k(t,e){return!e||"function"!=typeof e||!!await e(t)}async S(t){return await caches.default.match(t)||null}async O(t,e,n){if(!n)return;const r=!0===n?{}:n,o=r.ttl??3600,i=r.staleWhileRevalidate??0,s=e.clone();s.headers.set("Cache-Control","max-age="+o+(i>0?", stale-while-revalidate="+i:"")),s.headers.delete("Set-Cookie"),await caches.default.put(t,s)}async fetch(t,e,n){const r=new URL(t.url),o=l(r.pathname),i=t.method,s=this.staticRoutes.get(i);if(s){const i=s.get(o);if(i)return this.v(i.handler,i.options,t,e,n,Object.create(null),r)}const a=this.dynamicTries[i];if(a){const i=a.match(c(o));if(i)return this.v(i.handler,i.options,t,e,n,i.params,r)}return new Response("Not Found",{status:404})}async v(t,e,n,r,o,i,s){const a=new T(n,r,o,i,s),c=this.security.extraHeaders;if(c)for(const t in c)Object.prototype.hasOwnProperty.call(c,t)&&a.headers.set(t,c[t]);let l,u=!1;try{u=await this.k(a,e.validate)}catch(t){this.p(a,"validation_error",t&&t.message),u=!1}if(!u)return a.text("Validation failed",400);try{l=await this.m(a,e.auth)}catch(t){this.p(a,"auth_error",t&&t.message),l={ok:!1,reason:"auth_error"}}if(!("boolean"==typeof l?l:l&&l.ok))return this.p(a,"auth_failure",l&&l.reason||"denied"),a.text("Unauthorized",401);let f,h=!1;try{h=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),h=!1}if(!h)return this.p(a,"rate_limit_exceeded",null),a.text("Too Many Requests",429,{"Retry-After":"60"});if(e.cache&&"GET"===n.method){const t=await this.S(n);if(t)return this.C(a,e.cors),this.T(a,e.log),t}this.C(a,e.cors);try{const e=await t(a);f=e instanceof Response?e:e&&e.__isJSX?a.html(e):a.text("OK"),a.status=f.status}catch(t){this.p(a,"handler_error",t&&t.message),f=a.text("Internal Server Error",500),a.status=500}return e.cache&&"GET"===n.method&&200===f.status&&a.executionCtx.waitUntil(this.O(n,f.clone(),e.cache)),e.compress&&(f=this._(n,f)),this.T(a,e.log),f}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Edge framework used by Lengkapp",
5
5
  "main": "dist/edge-server.js",
6
6
  "types": "dist/edge-server.d.ts",
package/readme.md CHANGED
@@ -2,37 +2,59 @@
2
2
 
3
3
  A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
4
4
 
5
- Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**.
5
+ Inspired by Hono, `@lengkapp/edge` aims for the same class of performance while shipping with zero runtime dependencies.
6
6
 
7
- > **Security:** Server and client are hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls. The client sanitizes every response, works under `require-trusted-types-for 'script'` CSP, is nonce-aware for script execution, and sends CSRF tokens on state-changing requests. See [Security](#security-controls).
7
+ **Security:** The server is hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls — prototype-safe params, cookies, JSON bodies and JSX attributes; CSP-friendly response headers; fail-closed middleware; strict CORS allowlist; and structured security logging. See [Security](#security).
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [Features](#features)
14
+ - [Installation](#installation)
15
+ - [Quick Start](#quick-start)
16
+ - [Server API](#server-api)
17
+ - [JSX Support](#jsx-support)
18
+ - [Client (Declarative Partial Updates)](#client-declarative-partial-updates)
19
+ - [CSRF Protection](#csrf-protection)
20
+ - [Security Controls](#security-controls)
21
+ - [OWASP 2025 Coverage](#owasp-2025-coverage)
22
+ - [Scheduled Tasks](#scheduled-tasks)
23
+ - [Configuration](#configuration)
24
+ - [Performance](#performance)
25
+ - [Security Posture Summary](#security-posture-summary)
26
+ - [License](#license)
8
27
 
9
28
  ---
10
29
 
11
30
  ## Features
12
31
 
13
32
  ### Server
14
- - **Trie-based routing** – static & dynamic routes (`/users/:id`).
15
- - **JSX support** – pass JSX straight to `ctx.html(<Card />)`; no build step, no manual `renderToString` call.
16
- - **Middleware** – auth, rate limiting, CORS, logging, caching, compression, validation.
17
- - **Cookie helpers** with validation.
18
- - **Scheduled tasks** via Cron triggers.
19
- - **Zero dependencies.**
33
+
34
+ - **Trie-based routing** – static & dynamic routes (`/users/:id`)
35
+ - **JSX support** – pass JSX straight to `ctx.html(<Card />)`; no build step, no manual `renderToString` call
36
+ - **Middleware** – auth, rate limiting, CORS, logging, caching, compression, validation
37
+ - **Cookie helpers** with validation
38
+ - **Scheduled tasks** via Cron triggers
39
+ - **Zero dependencies**
20
40
 
21
41
  ### Client
22
- - **Declarative partial updates** via `_get`, `_post`, `_target`, `_trigger`.
23
- - **Trusted Types aware** – routes all `innerHTML` through an `edge-policy` sanitizer.
24
- - **Nonce-aware script execution** – response scripts must match the page's CSP nonce when one exists.
25
- - **Built-in sanitizer** – strips `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and `javascript:`/`vbscript:`/`data:text/html` URLs.
26
- - **CSRF-aware** – sends `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
27
- - **Content-Type guarded** – only `text/html` and `text/plain` responses are inserted.
28
-
29
- ### Security (both)
30
- - **Prototype-pollution safe** – route params, cookies, JSON bodies, JSX attributes.
31
- - **XSS-hardened JSX** – no `on*` attributes, no `javascript:` URLs, no malformed tag names.
32
- - **Structured security logging** – throttled JSON events for auth/rate-limit/handler failures.
33
- - **Fail-closed middleware** – validation, auth, and rate limit errors deny by default.
34
- - **Opt-in token hashing** – SHA-256 before KV lookup.
35
- - **Strict CORS allowlist** – per-origin reflection with `Vary: Origin`.
42
+
43
+ - **Declarative partial updates** via `_get` / `_post` and the placement modes `_in`, `_out`, `_before`, `_after`
44
+ - **Event, load, and visibility triggers** – `click` (default), `load`, `visible`, or any DOM event name
45
+ - **JSON and form bodies** – `_json="a,b,c"` or `_form="#signup"`
46
+ - **Built-in loading and error states** – deferred spinner, skeleton loader, abortable requests, one-click retry
47
+ - **View Transitions aware** – swaps run inside `document.startViewTransition` when available
48
+ - **Zero dependencies**
49
+
50
+ ### Security
51
+
52
+ - **Prototype-pollution safe** – route params, cookies, JSON bodies, JSX attributes
53
+ - **XSS-hardened JSX** – no `on*` attributes, no `javascript:` URLs, no malformed tag names
54
+ - **Structured security logging** – throttled JSON events for auth/rate-limit/handler failures
55
+ - **Fail-closed middleware** – validation, auth, and rate limit errors deny by default
56
+ - **Opt-in token hashing** – SHA-256 before KV lookup
57
+ - **Strict CORS allowlist** – per-origin reflection with `Vary: Origin`
36
58
 
37
59
  ---
38
60
 
@@ -42,9 +64,11 @@ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of
42
64
  npm install @lengkapp/edge
43
65
  ```
44
66
 
67
+ ---
68
+
45
69
  ## Quick Start
46
70
 
47
- `worker.js`:
71
+ **`worker.js`:**
48
72
 
49
73
  ```ts
50
74
  import { Edge } from '@lengkapp/edge';
@@ -57,9 +81,9 @@ app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
57
81
  export default app;
58
82
  ```
59
83
 
60
- or if you will use jsx `worker.tsx`:
84
+ Or, if you will use JSX, **`worker.tsx`:**
61
85
 
62
- ```ts
86
+ ```tsx
63
87
  import { Edge, jsx, Fragment } from '@lengkapp/edge';
64
88
 
65
89
  const app = new Edge();
@@ -70,20 +94,19 @@ const Card = () => (
70
94
 
71
95
  const LandingPage = () => (
72
96
  <>
73
- <h1>
74
- hello world
75
- </h1>
97
+ <h1>hello world</h1>
76
98
  <Card />
77
99
  </>
78
100
  );
101
+
79
102
  app.get('/', () => <LandingPage />);
80
103
 
81
104
  export default app;
82
105
  ```
83
106
 
84
- if you use typescript `tsconfig.json`:
107
+ If you use TypeScript, **`tsconfig.json`:**
85
108
 
86
- ```typescript
109
+ ```json
87
110
  {
88
111
  "compilerOptions": {
89
112
  "jsx": "react",
@@ -101,7 +124,7 @@ if you use typescript `tsconfig.json`:
101
124
  }
102
125
  ```
103
126
 
104
- `wrangler.jsonc`:
127
+ **`wrangler.jsonc`:**
105
128
 
106
129
  ```jsonc
107
130
  {
@@ -112,7 +135,7 @@ if you use typescript `tsconfig.json`:
112
135
  }
113
136
  ```
114
137
 
115
- or `wrangler.toml`:
138
+ Or **`wrangler.toml`:**
116
139
 
117
140
  ```toml
118
141
  name = "my-edge-app"
@@ -134,13 +157,13 @@ wrangler deploy
134
157
 
135
158
  | Member | Description |
136
159
  |---|---|
137
- | `ctx.req` | Incoming Request |
160
+ | `ctx.req` | Incoming `Request` |
138
161
  | `ctx.env` | Environment bindings |
139
- | `ctx.executionCtx` | ExecutionContext |
162
+ | `ctx.executionCtx` | `ExecutionContext` |
140
163
  | `ctx.params` | Prototype-safe route params object |
141
164
  | `ctx.status` | Default response status (200) |
142
- | `ctx.headers` | Response Headers |
143
- | `ctx.query` | URLSearchParams |
165
+ | `ctx.headers` | Response `Headers` |
166
+ | `ctx.query` | `URLSearchParams` |
144
167
  | `ctx.getCookie(name)` | Read a cookie |
145
168
  | `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
146
169
  | `ctx.deleteCookie(name, options)` | Delete a cookie |
@@ -162,17 +185,18 @@ app.get('/api', { cors: true }, handler);
162
185
  app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
163
186
  ```
164
187
 
165
- **Auth:** Tokens are looked up in `AUTH_KV`. Send via `Authorization: Bearer <token>` or the `auth_token` cookie. Payloads with an `exp` field (Unix seconds) are rejected when expired.
188
+ - **Auth:** Tokens are looked up in `AUTH_KV`. Send via `Authorization: Bearer <token>` or the `auth_token` cookie. Payloads with an `exp` field (Unix seconds) are rejected when expired.
189
+ - **CORS:** The default is `origin: '*'`. For production, use a strict allowlist:
166
190
 
167
- **CORS:** The default is `origin: '*'`. For production, use a strict allowlist:
191
+ ```ts
192
+ app.defaults.cors.origin = ['https://app.example.com'];
193
+ ```
168
194
 
169
- ```ts
170
- app.defaults.cors.origin = ['https://app.example.com'];
171
- ```
195
+ - **Rate limit:** `429` responses carry `Retry-After: 60`.
172
196
 
173
- **Rate limit:** `429` responses carry `Retry-After: 60`.
197
+ ---
174
198
 
175
- ### JSX Support
199
+ ## JSX Support
176
200
 
177
201
  `ctx.html()` accepts JSX directly — it detects JSX nodes and arrays and renders them automatically. Raw strings are passed through untouched, so you can still serve pre-rendered HTML.
178
202
 
@@ -199,10 +223,11 @@ const html = renderToString(<Card title="Hello" />);
199
223
  ```
200
224
 
201
225
  **`renderToString` hardening:**
226
+
202
227
  - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
203
228
  - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
204
229
  - `on*` attributes never serialize.
205
- - `href`/`src`/`action`/`formaction`/`xlink:href` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
230
+ - `href` / `src` / `action` / `formaction` / `xlink:href` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
206
231
  - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
207
232
  - All string values are HTML-escaped.
208
233
 
@@ -218,67 +243,93 @@ const html = renderToString(<Card title="Hello" />);
218
243
 
219
244
  | Attribute | Description |
220
245
  |---|---|
221
- | `_get` / `_post` / `_put` / `_patch` / `_delete` | URL + method |
222
- | `_target` | CSS selector, or `"this"` |
223
- | `_trigger` | Event list: `click`, `load`, `visible`, `intersect`, etc. |
224
- | `_form` | Form ID to serialize as URL-encoded body |
225
- | `_json` | Comma-separated input names to send as JSON |
226
- | `_skeleton` | `"false"` disables the loading skeleton |
227
- | `_retry` | `"false"` disables the retry button |
246
+ | `_get` / `_post` | Request URL and HTTP method. Only `GET` and `POST` are supported. |
247
+ | `_in` | Replace the target's children with the response. |
248
+ | `_out` | Replace the target element itself. |
249
+ | `_before` | Insert the response before the target. |
250
+ | `_after` | Insert the response after the target. |
251
+ | `_trigger` | `click` (default), `load`, `visible`, or any DOM event name. |
252
+ | `_form` | CSS selector or element ID of a form to serialize as the body. |
253
+ | `_json` | Comma-separated field names to send as a JSON body. |
254
+ | `_loader` | `spinner` (default), `skeleton`, or `none` / `off` / `false` to disable. |
255
+ | `_timeout` | Request timeout in milliseconds (default `20000`). |
256
+
257
+ **Targets.** Exactly one placement attribute (`_in`, `_out`, `_before`, `_after`) should be present. Its value is a CSS selector, the literal string `this`, or empty — the last two both resolve to the element that carries the attribute.
258
+
259
+ **Field scope.** `_json` reads values from the closest enclosing `<form>`, or from the document if there is none. It also accepts fields by `id` first, then by `name` (grouped radio/checkbox inputs are handled).
228
260
 
229
261
  ### Examples
230
262
 
231
263
  ```html
232
- <button _get="/more-posts" _target="#posts">Load More</button>
264
+ <!-- replace the children of #posts with the response -->
265
+ <button _get="/more-posts" _in="#posts">Load More</button>
266
+
267
+ <!-- replace this element with the response -->
268
+ <div _get="/user-profile" _out="this"></div>
269
+
270
+ <!-- POST JSON built from form fields, replace the children of #status -->
271
+ <button _post="/login" _json="username,password" _in="#status">Login</button>
233
272
 
234
- <div _get="/user-profile" _target="this"></div>
273
+ <!-- POST a whole form -->
274
+ <form id="signup">…</form>
275
+ <button _post="/signup" _form="#signup" _in="#result">Sign up</button>
235
276
 
236
- <button _post="/login" _json="username,password" _target="#status">Login</button>
277
+ <!-- fetch lazily when the element scrolls into view -->
278
+ <div _get="/lazy" _in="this" _trigger="visible"></div>
237
279
 
238
- <div _get="/lazy" _target="this" _trigger="visible"></div>
280
+ <!-- skeleton loader with a 5-second timeout -->
281
+ <div _get="/feed" _in="this" _loader="skeleton" _timeout="5000"></div>
239
282
  ```
240
283
 
284
+ ### Triggers
285
+
286
+ - **`click`** (default) — handled by a single delegated document listener.
287
+ - **`load` / `visible`** — the element is observed with `IntersectionObserver` (300px root margin) and the request fires the first time it enters the viewport.
288
+ - **Any other value** — treated as a DOM event name. The listener is attached the first time the element becomes visible, then fires normally.
289
+
241
290
  ### How Content Is Inserted
242
291
 
243
- 1. Response is fetched with `credentials: 'same-origin'` and `X-Requested-With: XMLHttpRequest`.
244
- 2. `Content-Type` is verified to be `text/html` or `text/plain`.
245
- 3. `res.headers` may contain `x-css-required` / `x-js-required` (with optional `x-css-integrity` / `x-js-integrity` for SRI) — matching resources are injected once.
246
- 4. The HTML is run through `sanitizeHtml`, which removes `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and neutralizes `javascript:` / `vbscript:` / `data:text/html` URLs.
247
- 5. The sanitized HTML is assigned via `innerHTML` (through a Trusted Types policy named `edge-policy` when available).
248
- 6. Each `<script>` in the result is rebuilt in place so the browser executes it. If the page has a CSP nonce, only scripts carrying the matching nonce run; otherwise all scripts run.
292
+ 1. The request is sent with `credentials: 'same-origin'` and `X-Requested-With: XMLHttpRequest`.
293
+ 2. For `_post`, the body is JSON (`_json`), a `FormData` object (`_form`), or empty.
294
+ 3. The response text is parsed into a `<template>`.
295
+ 4. Any `<script>` elements are lifted out, then re-created and appended to `<head>` so the browser executes them. External scripts are de-duplicated by absolute URL.
296
+ 5. Newly inserted `[_get]` / `[_post]` elements are scanned and bound.
297
+ 6. Placement depends on the target mode:
298
+ - `_in` — the target's existing children are removed, then the fragment is appended.
299
+ - `_out` — the target itself is replaced.
300
+ - `_before` / `_after` — the fragment is inserted adjacent to the target.
301
+
302
+ > **Note:** No content-type check, HTML sanitization, or CSRF token is applied by the client — it trusts the server's response and inserts it as-is. Treat the partial-HTML endpoints you point `_get` / `_post` at as part of your trusted surface.
303
+
304
+ ### Loader & Error States
305
+
306
+ - The loader is deferred by 100ms: if the response lands before then, no loader is shown at all.
307
+ - Once shown, the loader stays for at least 240ms before the content swaps in, so it never flashes.
308
+ - `_loader="spinner"` (default), `_loader="skeleton"` for a shimmering skeleton, or `_loader="none"` to disable.
309
+ - A new request on the same element aborts the previous one via `AbortController`.
310
+ - On failure — non-2xx, network error, or `_timeout` — the loader is replaced in place by an error box with a retry button that re-issues the request.
311
+ - Loaders and error states use `role="status"` / `role="alert"` with `aria-busy` set on the target while in flight.
312
+
313
+ ### View Transitions
314
+
315
+ When `document.startViewTransition` is available, loader → content and loader → error swaps are wrapped in a view transition. The injected stylesheet disables the animation under `prefers-reduced-motion: reduce`.
249
316
 
250
317
  ---
251
318
 
252
319
  ## CSRF Protection
253
320
 
254
- The client reads a token from `<meta name="csrf-token" content="...">` or the `XSRF-TOKEN` cookie and sends it as `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
321
+ The client does not attach a CSRF token automatically. Include the token as a form field or as part of the JSON payload built by `_json`, then validate it on the server:
255
322
 
256
323
  ```ts
257
324
  app.post('/submit', {
258
- validate: (ctx) => {
259
- const sent = ctx.req.headers.get('X-CSRF-Token');
260
- return sent && sent === ctx.getCookie('csrf_token');
325
+ validate: async (ctx) => {
326
+ const body = await ctx.req.json().catch(() => ({}));
327
+ return body.csrf_token && body.csrf_token === ctx.getCookie('csrf_token');
261
328
  }
262
329
  }, handler);
263
330
  ```
264
331
 
265
- ---
266
-
267
- ## Trusted Types / CSP
268
-
269
- If the page enforces `require-trusted-types-for 'script'`, the client creates a policy named `edge-policy` and routes every `innerHTML` through it. The policy runs the built-in sanitizer. To allowlist the policy in CSP:
270
-
271
- ```
272
- Content-Security-Policy:
273
- default-src 'self';
274
- script-src 'self';
275
- style-src 'self';
276
- img-src 'self' data:;
277
- require-trusted-types-for 'script';
278
- trusted-types edge-policy;
279
- frame-ancestors 'none';
280
- base-uri 'self';
281
- ```
332
+ For `_form`-based submissions, read the field from the parsed form data using the same pattern.
282
333
 
283
334
  ---
284
335
 
@@ -298,15 +349,15 @@ app.security.extraHeaders = {
298
349
  };
299
350
  ```
300
351
 
301
- ### OWASP 2025 Coverage
352
+ ## OWASP 2025 Coverage
302
353
 
303
354
  | Category | Mitigation |
304
355
  |---|---|
305
356
  | A01 Broken Access Control | Auth reasons, expiry enforcement, scope/role checks, strict target resolution |
306
357
  | A02 Security Misconfiguration | Safe default headers, strict CORS allowlist, `Vary: Origin` |
307
- | A03 Supply Chain | Zero deps, SRI honored on injected resources |
358
+ | A03 Supply Chain | Zero deps |
308
359
  | A04 Crypto Failures | Optional SHA-256 token hashing |
309
- | A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization, Trusted Types policy, response sanitizer |
360
+ | A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization |
310
361
  | A06 Insecure Design | Numeric-sanitized rate limiter, `Retry-After` |
311
362
  | A07 Auth Failures | Structured failure reasons, no swallowed errors |
312
363
  | A08 Data Integrity | Prototype-safe JSON, `Set-Cookie` stripped from cache |
@@ -363,34 +414,34 @@ Zero runtime dependencies. On a local `wrangler dev` benchmark with `autocannon`
363
414
 
364
415
  | Framework | `/text` req/s | `/json` req/s |
365
416
  |---|---|---|
366
- | @lengkapp/edge | 503 | 500 |
417
+ | `@lengkapp/edge` | 503 | 500 |
367
418
  | Hono | 506 | 495 |
368
419
  | Hono (tiny) | 505 | 498 |
369
420
  | Native Workers | 498 | 500 |
370
421
 
371
- The security hardening adds only cheap operations to the request path: prototype-safe objects, two regex checks per JSX attribute, a sanitizer template parse per response (browser-native parser), and a throttled logger. No new async boundaries, no added dependencies, no hashing on the hot path.
422
+ The security hardening adds only cheap operations to the request path: prototype-safe objects, two regex checks per JSX attribute, and a throttled logger. No new async boundaries, no added dependencies, no hashing on the hot path.
372
423
 
373
424
  The JSX renderer itself is tuned for throughput: single-pass escaping (`charCodeAt` scan with a fast no-op path), direct string concatenation instead of intermediate arrays, cached camelCase → kebab-case conversions for style objects, and a `Set`-based URL-attribute lookup on the hot path. `ctx.html(<Card />)` skips the redundant string-identity check that a manual `renderToString(<Card />)` call would still hit.
374
425
 
426
+ The client is equally lean: it injects a single stylesheet, binds each element exactly once via a `WeakSet`, and uses one delegated `click` listener for all `_trigger="click"` elements. An in-flight request on an element is aborted when a new one starts, and the loader is deferred by 100ms / held for at least 240ms so fast endpoints never flash UI.
427
+
375
428
  ---
376
429
 
377
430
  ## Security Posture Summary
378
431
 
379
432
  | Layer | Mechanism |
380
433
  |---|---|
381
- | HTML insertion | `sanitizeHtml` → `innerHTML` via Trusted Types policy `edge-policy` |
382
- | Dangerous elements | `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, `form[action^="javascript:"]` removed |
383
- | Dangerous URLs | `javascript:`, `vbscript:`, `data:text/html` removed from `href`/`src`/`action`/`formaction`/`xlink:href`/`data` |
384
- | Script execution | Nonce-gated when the page uses CSP nonces; deduplicated by `src`; inline scripts block-wrapped |
385
- | CSRF | `X-CSRF-Token` from meta or cookie on state-changing methods |
434
+ | HTML insertion (client) | Response text parsed into a `<template>`; placement driven by `_in` / `_out` / `_before` / `_after` |
435
+ | Script execution (client) | Extracted `<script>` elements re-created and appended to `<head>`; external scripts de-duplicated by absolute URL |
436
+ | Request hygiene (client) | `credentials: 'same-origin'`, `X-Requested-With: XMLHttpRequest`, abortable via `AbortController`, `_timeout` (default 20s) |
386
437
  | Cookies | `credentials: 'same-origin'`; names validated on the server |
387
438
  | Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
388
439
  | Server middleware | Fail-closed; structured security logs; strict CORS allowlist; optional token hashing |
389
- | Prototype pollution | `Object.create(null)` + forbidden-key filtering in params, cookies, JSON, JSX |
440
+ | Prototype pollution | `Object.create(null)` + forbidden-key filtering in params, cookies, JSON, JSX (server) |
390
441
  | Build | Reserved exports/properties, `keep_quoted: "strict"`, per-bundle post-minification self-test |
391
442
 
392
443
  ---
393
444
 
394
445
  ## License
395
446
 
396
- See LICENSE. © LengkApp — Yasir Haris
447
+ See [LICENSE](./LICENSE). © LengkApp — Yasir Haris