@lengkapp/edge 0.0.21 → 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.
- package/dist/edge-client.js +2 -2
- package/dist/edge-server.d.ts +11 -1
- package/dist/edge-server.js +2 -2
- package/package.json +1 -1
- package/readme.md +145 -94
package/dist/edge-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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{"&":"&","<":"<",">":">",'"':""","'":"'"}[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{"&":"&","<":"<",">":">",'"':""","'":"'"}[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)}()}();
|
package/dist/edge-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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 ---------------- */
|
package/dist/edge-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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="&";else if(60===e)i="<";else if(62===e)i=">";else if(34===e)i=""";else{if(39!==e)continue;i="'"}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="&";else if(60===e)i="<";else if(62===e)i=">";else if(34===e)i=""";else{if(39!==e)continue;i="'"}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
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
|
|
5
|
+
Inspired by Hono, `@lengkapp/edge` aims for the same class of performance while shipping with zero runtime dependencies.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
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
|
-
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
Or, if you will use JSX, **`worker.tsx`:**
|
|
61
85
|
|
|
62
|
-
```
|
|
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
|
-
|
|
107
|
+
If you use TypeScript, **`tsconfig.json`:**
|
|
85
108
|
|
|
86
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
```ts
|
|
192
|
+
app.defaults.cors.origin = ['https://app.example.com'];
|
|
193
|
+
```
|
|
168
194
|
|
|
169
|
-
|
|
170
|
-
app.defaults.cors.origin = ['https://app.example.com'];
|
|
171
|
-
```
|
|
195
|
+
- **Rate limit:** `429` responses carry `Retry-After: 60`.
|
|
172
196
|
|
|
173
|
-
|
|
197
|
+
---
|
|
174
198
|
|
|
175
|
-
|
|
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
|
|
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`
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
273
|
+
<!-- POST a whole form -->
|
|
274
|
+
<form id="signup">…</form>
|
|
275
|
+
<button _post="/signup" _form="#signup" _in="#result">Sign up</button>
|
|
235
276
|
|
|
236
|
-
|
|
277
|
+
<!-- fetch lazily when the element scrolls into view -->
|
|
278
|
+
<div _get="/lazy" _in="this" _trigger="visible"></div>
|
|
237
279
|
|
|
238
|
-
|
|
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.
|
|
244
|
-
2. `
|
|
245
|
-
3.
|
|
246
|
-
4.
|
|
247
|
-
5.
|
|
248
|
-
6.
|
|
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
|
|
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
|
|
260
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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,
|
|
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 | `
|
|
382
|
-
|
|
|
383
|
-
|
|
|
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
|