@lengkapp/edge 0.0.11 → 0.0.13

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.10
2
+ * @lengkapp/edge v0.0.13
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(){if(!window.__edge){window.__edge=!0;var e=document,n=e.head,t=e.documentElement,r=["get","post","put","patch","delete"].map(function(e){return"_"+e}),i={post:1,put:1,patch:1},o=r.map(function(e){return"["+e+"]"}).join(","),a=new Set(["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","contextmenu","focusin","focusout","keydown","keyup","keypress","change","input","submit","reset","wheel","touchstart","touchend","touchmove"]),u=new WeakSet,c=new WeakSet,s=new WeakMap,f=new WeakMap,d=new Map,l=new Set,h=new Set,v=e.createElement("style");v.textContent=".aj-skel-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;max-width:100%;max-height:600px;display:flex;align-items:center;justify-content:center}.aj-skel-wrap svg{width:1.75em;height:1.75em;color:#c7c7c7;animation:aj-pulse 1.4s ease-in-out infinite}@keyframes aj-pulse{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:1;transform:scale(1)}}.aj-retry-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;max-height:600px;display:flex;align-items:center;justify-content:center}.aj-retry{width:2.5em;height:2.5em;padding:0;cursor:pointer;background:#007bff;color:#fff;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease}.aj-retry:hover{background:#0056b3}.aj-retry:hover svg{transform:rotate(-90deg)}.aj-retry svg{width:1.25em;height:1.25em;transition:transform .3s ease}",n.appendChild(v);var w=!1,p=new Set,m=new Set,g=!1;new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){1===e.nodeType&&p.add(e)}),e.removedNodes.forEach(function(e){1===e.nodeType&&m.add(e)})}),g||(g=!0,queueMicrotask(_))}).observe(t,{childList:!0,subtree:!0}),"loading"===e.readyState?e.addEventListener("DOMContentLoaded",C,{once:!0}):C()}function k(e){for(var n=null,t=0;t<r.length;t++)if(e.hasAttribute(r[t])){n=r[t];break}if(!n)return null;var o=n.slice(1);return{url:e.getAttribute(n),method:o.toUpperCase(),hasBody:!!i[o],target:e.getAttribute("_target"),trigger:e.getAttribute("_trigger"),form:e.getAttribute("_form"),json:e.getAttribute("_json"),skeleton:"false"!==e.getAttribute("_skeleton"),retry:"false"!==e.getAttribute("_retry")}}function y(e){return(e.trigger||("this"===e.target?"load":"click")).split(",").map(function(e){return e.trim().toLowerCase()})}function b(e){return e?e.replace(/^\[|\]$/g,"").split(",").map(function(e){return e.trim().replace(/^['"]|['"]$/g,"")}).filter(Boolean):[]}function x(t,r){if(!c.has(t)){c.add(t);var i=function(n,t){if(!t.target||"this"===t.target)return n;var r=s.get(n);if(r&&r.isConnected)return r;var i=e.querySelector(t.target);return i&&s.set(n,i),i}(t,r);if(i){r.skeleton&&function(e){e.innerHTML='<div class="aj-skel-wrap"><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" aria-hidden="true"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg></div>'}(i);var o={method:r.method};if(r.hasBody)if(r.json){var a={};r.json.split(",").forEach(function(n){n=n.trim();var t=e.querySelector('[name="'+n+'"]');a[n]=t?t.value:void 0}),o.headers={"Content-Type":"application/json"},o.body=JSON.stringify(a)}else if(r.form){var u=e.getElementById(r.form);o.headers={"Content-Type":"application/x-www-form-urlencoded"},o.body=u?new URLSearchParams(new FormData(u)).toString():""}fetch(r.url,o).then(function(t){if(!t.ok)throw new Error("HTTP "+t.status);var r;return b((r=t.headers).get("x-css-required")).forEach(function(t){if(!l.has(t)){l.add(t);var r=e.createElement("link");r.rel="stylesheet",r.href=t,n.appendChild(r)}}),b(r.get("x-js-required")).forEach(function(t){if(!l.has(t)){l.add(t);var r=e.createElement("script");r.src=t,n.appendChild(r)}}),t.text()}).then(function(n){!function(n,t){var r,i,o;n.innerHTML=t,r=n,i=Array.prototype.slice.call(r.querySelectorAll("script")),o=0,function n(){if(!(o>=i.length)){for(var t=i[o],r=e.createElement("script"),a=0;a<t.attributes.length;a++)r.setAttribute(t.attributes[a].name,t.attributes[a].value);if(t.nonce&&(r.nonce=t.nonce),t.src){if(h.has(t.src))return t.remove(),o++,void n();h.add(t.src),r.onload=r.onerror=function(){o++,n()},t.replaceWith(r)}else r.textContent="{\n"+t.textContent+"\n}",t.replaceWith(r),o++,n()}}(),M(n)}(i,n)}).catch(function(n){console.error(n),r.retry&&function(n,t){n.innerHTML="";var r=e.createElement("div");r.className="aj-retry-wrap";var i=e.createElement("button");i.type="button",i.className="aj-retry",i.setAttribute("aria-label","Retry"),i.innerHTML='<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" 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>',i.addEventListener("click",t,{once:!0}),r.appendChild(i),n.appendChild(r)}(i,function(){x(t,r)})}).finally(function(){c.delete(t)})}else c.delete(t)}}function j(e){if(!u.has(e)){u.add(e);var n=k(e);n&&y(n).forEach(function(t){if("load"===t||"domcontentloaded"===t||"ready"===t)queueMicrotask(function(){x(e,n)});else if("visible"===t||"intersect"===t){f.set(e,!1);var r=new IntersectionObserver(function(t){t.forEach(function(t){if(!t.target.isConnected)return r.disconnect(),void d.delete(e);var i=t.isIntersecting&&function(e){var n=getComputedStyle(e);if("none"===n.display||"hidden"===n.visibility||"0"===n.opacity)return!1;var t=e.getBoundingClientRect();return t.width>0&&t.height>0}(e);i&&!f.get(e)&&x(e,n),f.set(e,i)})});r.observe(e),d.set(e,r)}else a.has(t)||e.addEventListener(t,function(){x(e,n)})})}}function M(e){1===e.nodeType&&(e.matches&&e.matches(o)&&j(e),e.querySelectorAll&&e.querySelectorAll(o).forEach(j))}function S(e){d.size&&d.forEach(function(n,t){(e===t||e.contains(t))&&(n.disconnect(),d.delete(t),u.delete(t))})}function _(){g=!1;var e=Array.from(p);p.clear();var n=Array.from(m);m.clear(),e.forEach(M),n.forEach(S)}function C(){w||(w=!0,a.forEach(function(n){var t="click"===n||"submit"===n;e.addEventListener(n,function(e){var r=e.target;if(r instanceof Element){var i=r.closest(o);if(i){var a=k(i);a&&-1!==y(a).indexOf(n)&&(t&&e.preventDefault(),x(i,a))}}},!t&&{passive:!0})})),M(t)}}();
33
+ !function(){if(!window.__edgeSecure){window.__edgeSecure=!0;var n=document,e=n.head,t=n.documentElement;window.trustedTypes&&window.trustedTypes.createPolicy&&window.trustedTypes.createPolicy("aj-policy",{createHTML:function(n){return n}});var r=["get","post","put","patch","delete"].map(function(n){return"_"+n}),o={post:1,put:1,patch:1},i=r.map(function(n){return"["+n+"]"}).join(","),u=new Set(["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","contextmenu","focusin","focusout","keydown","keyup","keypress","change","input","submit","reset","wheel","touchstart","touchend","touchmove"]),a=new WeakSet,c=new WeakSet,s=new WeakMap,f=new WeakMap,l=new Map,v=new Set,d=new Set(["__proto__","constructor","prototype"]),w=!1,p=new Set,h=new Set,m=!1;new MutationObserver(function(n){n.forEach(function(n){n.addedNodes.forEach(function(n){1===n.nodeType&&p.add(n)}),n.removedNodes.forEach(function(n){1===n.nodeType&&h.add(n)})}),m||(m=!0,queueMicrotask(j))}).observe(t,{childList:!0,subtree:!0}),"loading"===n.readyState?n.addEventListener("DOMContentLoaded",_,{once:!0}):_()}function g(n){return(n.trigger||("this"===n.target?"load":"click")).split(",").map(function(n){return n.trim().toLowerCase()})}function k(n){return n?n.replace(/^\[|\]$/g,"").split(",").map(function(n){return n.trim().replace(/^['"]|['"]$/g,"")}).filter(Boolean):[]}function y(t,r){if(!c.has(t)){c.add(t);var i=function(e,t){if(!t.target||"this"===t.target)return e;if(!/^[#.]?[\w-]+$/.test(t.target))return e;var r=s.get(e);if(r&&r.isConnected)return r;var o=n.querySelector(t.target);return o&&s.set(e,o),o}(t,r);if(i){r.skeleton&&function(e){var t=n.createElement("div");t.className="aj-skel-wrap";var r=(new DOMParser).parseFromString('<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" aria-hidden="true"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>',"image/svg+xml").documentElement;t.appendChild(n.importNode(r,!0)),e.textContent="",e.appendChild(t)}(i);var u={method:r.method,credentials:"same-origin",headers:{"X-Requested-With":"XMLHttpRequest"}};if(o[r.method.toLowerCase()]){var a=function(){var e=n.querySelector('meta[name="csrf-token"]');if(e)return e.getAttribute("content");var t=n.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);return t?decodeURIComponent(t[1]):null}();a&&(u.headers["X-CSRF-Token"]=a)}if(r.hasBody)if(r.json){var f=function(e){var t=Object.create(null);return e.forEach(function(e){if((e=e.trim())&&!d.has(e)){var r=n.querySelector('[name="'+CSS.escape(e)+'"]');t[e]=r?r.value:void 0}}),t}(r.json.split(","));u.headers["Content-Type"]="application/json",u.body=JSON.stringify(f)}else if(r.form){var l=n.getElementById(r.form);u.headers["Content-Type"]="application/x-www-form-urlencoded",u.body=l?new URLSearchParams(new FormData(l)).toString():""}fetch(r.url,u).then(function(t){if(!t.ok)throw new Error("HTTP "+t.status);var r,o=t.headers.get("content-type")||"";if(!o.includes("text/html")&&!o.includes("text/plain"))throw new Error("Unsupported content type: "+o);return k((r=t.headers).get("x-css-required")).forEach(function(t){if(!v.has(t)){v.add(t);var o=n.createElement("link");o.rel="stylesheet",o.href=t;var i=r.get("x-css-integrity");i&&(o.integrity=i,o.crossOrigin="anonymous"),e.appendChild(o)}}),k(r.get("x-js-required")).forEach(function(t){if(!v.has(t)){v.add(t);var o=n.createElement("script");o.src=t,o.async=!1;var i=r.get("x-js-integrity");i&&(o.integrity=i,o.crossOrigin="anonymous"),e.appendChild(o)}}),t.text()}).then(function(e){!function(e,t){e.textContent="";var r=(new DOMParser).parseFromString(t,"text/html");r.querySelectorAll("script").forEach(function(n){n.remove()});for(var o=n.createDocumentFragment(),i=r.body.childNodes,u=0;u<i.length;u++)o.appendChild(n.importNode(i[u],!0));e.appendChild(o),S(e)}(i,e)}).catch(function(e){console.error(e),r.retry&&function(e,t){e.textContent="";var r=n.createElement("div");r.className="aj-retry-wrap";var o=n.createElement("button");o.type="button",o.className="aj-retry",o.setAttribute("aria-label","Retry");var i=(new DOMParser).parseFromString('<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" 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>',"image/svg+xml");o.appendChild(n.importNode(i.documentElement,!0)),o.addEventListener("click",t,{once:!0}),r.appendChild(o),e.appendChild(r)}(i,function(){y(t,r)})}).finally(function(){c.delete(t)})}else c.delete(t)}}function x(n){for(var e=null,t=0;t<r.length;t++)if(n.hasAttribute(r[t])){e=r[t];break}if(!e)return null;var i=e.slice(1);return{url:n.getAttribute(e),method:i.toUpperCase(),hasBody:!!o[i],target:n.getAttribute("_target"),trigger:n.getAttribute("_trigger"),form:n.getAttribute("_form"),json:n.getAttribute("_json"),skeleton:"false"!==n.getAttribute("_skeleton"),retry:"false"!==n.getAttribute("_retry")}}function M(n){if(!a.has(n)){a.add(n);var e=x(n);e&&g(e).forEach(function(t){if("load"===t||"domcontentloaded"===t||"ready"===t)queueMicrotask(function(){y(n,e)});else if("visible"===t||"intersect"===t){f.set(n,!1);var r=new IntersectionObserver(function(t){t.forEach(function(t){if(!t.target.isConnected)return r.disconnect(),void l.delete(n);var o=t.isIntersecting&&function(n){var e=getComputedStyle(n);if("none"===e.display||"hidden"===e.visibility||"0"===e.opacity)return!1;var t=n.getBoundingClientRect();return t.width>0&&t.height>0}(n);o&&!f.get(n)&&y(n,e),f.set(n,o)})});r.observe(n),l.set(n,r)}else u.has(t)||n.addEventListener(t,function(){y(n,e)})})}}function S(n){1===n.nodeType&&(n.matches&&n.matches(i)&&M(n),n.querySelectorAll&&n.querySelectorAll(i).forEach(M))}function b(n){l.size&&l.forEach(function(e,t){(n===t||n.contains(t))&&(e.disconnect(),l.delete(t),a.delete(t))})}function j(){m=!1;var n=Array.from(p);p.clear();var e=Array.from(h);h.clear(),n.forEach(S),e.forEach(b)}function _(){w||(w=!0,u.forEach(function(e){var t="click"===e||"submit"===e;n.addEventListener(e,function(n){var r=n.target;if(r instanceof Element){var o=r.closest(i);if(o){var u=x(o);u&&-1!==g(u).indexOf(e)&&(t&&n.preventDefault(),y(o,u))}}},!t&&{passive:!0})})),S(t)}}();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.10
2
+ * @lengkapp/edge v0.0.13
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -109,7 +109,15 @@ interface RateLimitOptions {
109
109
  }
110
110
 
111
111
  interface CorsOptions {
112
- origin?: string;
112
+ /**
113
+ * Allowed origin(s).
114
+ * - `'*'` (default) preserves legacy wildcard behavior.
115
+ * - A specific string matches that origin and enables
116
+ * `Access-Control-Allow-Credentials: true`.
117
+ * - An array is treated as a strict allowlist; the request `Origin`
118
+ * is reflected back only when it matches, and `Vary: Origin` is set.
119
+ */
120
+ origin?: string | string[];
113
121
  methods?: string;
114
122
  headers?: string;
115
123
  }
@@ -129,14 +137,75 @@ interface RouteOptions {
129
137
  log?: boolean;
130
138
  }
131
139
 
140
+ /**
141
+ * Security controls for the Edge instance. All fields are optional and
142
+ * default to values that preserve legacy runtime behavior.
143
+ */
144
+ interface SecurityOptions {
145
+ /**
146
+ * When `true`, tokens are SHA-256 hashed (hex, prefixed with `tok:`) before
147
+ * being used as the KV key in `_processAuth`. Enable only after migrating
148
+ * existing tokens to hashed keys.
149
+ *
150
+ * @default false
151
+ */
152
+ hashAuthTokens?: boolean;
153
+
154
+ /**
155
+ * When `true`, structured JSON security events (auth failures, rate limit
156
+ * hits, handler errors, etc.) are emitted to `console.log`. Events are
157
+ * deduplicated per `(event, ip, path)` over a 60-second window to avoid
158
+ * log flooding.
159
+ *
160
+ * @default true
161
+ */
162
+ logSecurityEvents?: boolean;
163
+
164
+ /**
165
+ * Additional response headers applied to every `Context` before the
166
+ * handler runs. Useful for HSTS, COOP/COEP, CORP, etc.
167
+ *
168
+ * @default null
169
+ */
170
+ extraHeaders?: Record<string, string> | null;
171
+
172
+ /**
173
+ * Reserved for future IP-trust configuration. Currently unused.
174
+ *
175
+ * @default null
176
+ */
177
+ trustedProxies?: string[] | null;
178
+ }
179
+
132
180
  type RouteHandler = (ctx: Context) => Response | JSXElement | void | Promise<Response | JSXElement | void>;
133
181
 
182
+ type ScheduledHandler = (
183
+ controller: ScheduledController,
184
+ env: any,
185
+ ctx: ExecutionContext
186
+ ) => void | Promise<void>;
187
+
134
188
  export class Edge {
135
189
  constructor();
136
190
 
137
191
  authKvBinding: string;
138
192
  rateLimitKvBinding: string;
139
- scheduledHandler: ((controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>) | null;
193
+
194
+ /**
195
+ * Defaults applied to every route unless overridden at registration time.
196
+ * `cors.origin` accepts a string or string[] — see `CorsOptions`.
197
+ */
198
+ defaults: {
199
+ cors: CorsOptions;
200
+ };
201
+
202
+ scheduledHandler: ScheduledHandler | null;
203
+
204
+ /**
205
+ * Security controls. Mutate after construction to opt into hardened
206
+ * behavior; see `SecurityOptions`.
207
+ */
208
+ security: Required<SecurityOptions>;
140
209
 
141
210
  get(path: string, handler: RouteHandler): void;
142
211
  get(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
@@ -159,7 +228,7 @@ export class Edge {
159
228
  head(path: string, handler: RouteHandler): void;
160
229
  head(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
161
230
 
162
- scheduled(handler: (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>): void;
231
+ scheduled(handler: ScheduledHandler): void;
163
232
 
164
233
  fetch(request: Request, env: any, executionCtx: ExecutionContext): Promise<Response>;
165
234
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.10
2
+ * @lengkapp/edge v0.0.13
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,o=Object.prototype.hasOwnProperty,r=(e,n)=>{for(var o in n)t(e,o,{get:n[o],enumerable:!0})},s=(r,s,i,c)=>{if(s&&"object"==typeof s||"function"==typeof s)for(let a of n(s))o.call(r,a)||a===i||t(r,a,{get:()=>s[a],enumerable:!(c=e(s,a))||c.enumerable});return r},i=e=>s(t({},"__esModule",{value:!0}),e),c={};function a(t){const e=[],n=t.length;let o=1;for(let r=1;r<=n;r++)r!==n&&47!==t.charCodeAt(r)||(r>o&&e.push(t.slice(o,r)),o=r+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}r(c,{Edge:()=>Edge,Fragment:()=>Fragment,jsx:()=>jsx,renderToString:()=>renderToString}),module.exports=i(c);var u=class{constructor(t,e,n,o={},r=null){this.req=t,this.env=e,this.executionCtx=n,this.params=o,this.status=200,this.headers=new Headers,this.t=t.headers.get("Cookie")||"",this.o=null,this.url=r||new URL(t.url)}i(){if(null===this.o){const t=Object.create(null);if(this.t)for(const e of this.t.split(";")){const n=e.trim();if(!n)continue;const o=n.indexOf("=");if(o>0){const e=decodeURIComponent(n.slice(0,o)),r=decodeURIComponent(n.slice(o+1));t[e]=r}}this.o=t}return this.o}getCookie(t){return this.i()[t]??null}get query(){return this.url.searchParams}setCookie(t,e,n={}){let o=`${encodeURIComponent(t)}=${encodeURIComponent(e)}`;n.path&&(o+=`; Path=${n.path}`),n.domain&&(o+=`; Domain=${n.domain}`),void 0!==n.maxAge&&(o+=`; Max-Age=${n.maxAge}`),n.expires&&(o+=`; Expires=${n.expires.toUTCString()}`),n.secure&&(o+="; Secure"),n.httpOnly&&(o+="; HttpOnly"),n.sameSite&&(o+=`; SameSite=${n.sameSite}`),this.headers.append("Set-Cookie",o)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}text(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/plain"),new Response(t,{status:e,headers:o})}json(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:o})}html(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/html"),new Response(t,{status:e,headers:o})}l(t){if(0===Object.keys(t).length)return this.headers;const e=new Headers(this.headers);for(const n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.set(n,t[n]);return e}},Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n.flat(1/0),__isJSX:!0}}var f=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),h=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"]),d=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"]),p={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},m=/[&<>"']/g;function w(t){return m.lastIndex=0,m.test(t)?t.replace(m,t=>p[t]):t}var y=new Map,g=2e3;function T(t){const e=y.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return y.size>=g&&y.clear(),y.set(t,n),n}function b(t){if(!t||"object"!=typeof t)return"";const e=Object.entries(t);return 0===e.length?"":e.map(([t,e])=>{let n=T(t);return"number"!=typeof e||d.has(n)||(e=`${e}px`),`${n}:${e}`}).join(";")}function renderToString(t){if(null==t||"boolean"==typeof t)return"";if("string"==typeof t||"number"==typeof t)return w(String(t));if(Array.isArray(t)){const e=[];for(let n=0;n<t.length;n++)e.push(renderToString(t[n]));return e.join("")}if(!t.__isJSX)return w(String(t));const{type:e,props:n,children:o}=t;if(e===Fragment){const t=[];for(let e=0;e<o.length;e++)t.push(renderToString(o[e]));return t.join("")}if("symbol"==typeof e)return"";if("function"==typeof e)return renderToString(e({...n,children:o}));const r=f.has(e),s=[];for(const t in n){if(!Object.prototype.hasOwnProperty.call(n,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=n[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let o=t;if("className"===t||"class"===t?o="class":"htmlFor"!==t&&"for"!==t||(o="for"),h.has(o))!0===e&&s.push(` ${o}`);else if(!0!==e){if("style"===o&&"object"==typeof e){const t=b(e);t&&s.push(` style="${w(t)}"`);continue}s.push(` ${o}="${w(String(e))}"`)}else s.push(` ${o}="true"`)}const i=s.join("");let c="";if(n&&n.dangerouslySetInnerHTML&&null!=n.dangerouslySetInnerHTML.__html)c=n.dangerouslySetInnerHTML.__html;else{const t=[];for(let e=0;e<o.length;e++)t.push(renderToString(o[e]));c=t.join("")}return r?`<${e}${i}/>`:`<${e}${i}>${c}</${e}>`}var C=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},$=class{constructor(){this.root=new C}add(t,e,n){let o=this.root;const r=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0))r.push(n.slice(1)),o.paramChild||(o.paramChild=new C),o=o.paramChild;else{o.children||(o.children=new Map);let t=o.children.get(n);t||(t=new C,o.children.set(n,t)),o=t}}o.handler={handler:e,options:n,paramNames:r}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:o,options:r,paramNames:s}=n,i={};for(let t=0;t<s.length;t++)i[s[t]]=e[t];return{handler:o,options:r,params:i}}u(t,e,n,o){if(n===e.length)return t.handler;const r=e[n];if(t.children){const s=t.children.get(r);if(s){const t=this.u(s,e,n+1,o);if(t)return t}}if(t.paramChild){o.push(r);const s=this.u(t.paramChild,e,n+1,o);if(s)return s;o.pop()}return null}},E=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],Edge=class{constructor(){this.staticRoutes=new Map;for(const t of E)this.staticRoutes.set(t,new Map);this.dynamicTries={};for(const t of E)this.dynamicTries[t]=new $;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null}h(t,e,n,o){if("boolean"==typeof n&&(n={auth:n}),"function"==typeof n&&(o=n,n={}),"function"!=typeof o)throw new Error(`Handler for ${t} ${e} must be a function`);const r={...this.defaults,...n||{}};if(-1===e.indexOf(":")){const n=l(e);this.staticRoutes.get(t).set(n,{handler:o,options:r})}else{const n=a(e);this.dynamicTries[t].add(n,o,r)}}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}async p(t,e){if(!e)return!0;const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return!1;const o=t.env[this.authKvBinding];if(!o)return!1;const r=await o.get(n);if(!r)return!1;if("object"==typeof e)try{const t=JSON.parse(r);if(e.role&&t.role!==e.role)return!1;if(e.scopes){const n=t.scopes||[];if(!e.scopes.every(t=>n.includes(t)))return!1}}catch{}return!0}async m(t,e){if(!e)return!0;const n=!0===e?{}:e,{max:o=100,window:r=60}=n,s=`rl:${t.req.headers.get("CF-Connecting-IP")||"unknown"}`,i=t.env[this.rateLimitKvBinding];if(!i)return!0;let c=await i.get(s,"json")||0;return!(c>=o||(c++,await i.put(s,JSON.stringify(c),{expirationTtl:r}),0))}T(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors;t.headers.set("Access-Control-Allow-Origin",n.origin||"*"),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"),t.headers.set("Access-Control-Max-Age","86400")}C(t,e){e&&console.log(`${t.req.method} ${t.req.url} - ${t.status}`)}$(t,e){const n=t.headers.get("Accept-Encoding")||"";let o=null;if(n.includes("gzip")?o="gzip":n.includes("deflate")&&(o="deflate"),!o||!e.body)return e;const r=e.body.pipeThrough(new CompressionStream(o)),s=new Headers(e.headers);return s.set("Content-Encoding",o),s.set("Vary","Accept-Encoding"),s.delete("Content-Length"),new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async S(t,e){if(!e)return!0;if("function"==typeof e)try{return!!await e(t)}catch{return!1}return!0}async A(t){const e=caches.default;return await e.match(t)||null}async O(t,e,n){if(!n)return;const o=!0===n?{}:n,{ttl:r=3600,staleWhileRevalidate:s=0}=o,i=caches.default,c=e.clone();c.headers.set("Cache-Control",`max-age=${r}${s>0?`, stale-while-revalidate=${s}`:""}`),c.headers.delete("Set-Cookie"),await i.put(t,c)}async fetch(t,e,n){const o=new URL(t.url),r=l(o.pathname),s=t.method,i=this.staticRoutes.get(s);if(i){const s=i.get(r);if(s)return this._(s.handler,s.options,t,e,n,{},o)}const c=this.dynamicTries[s];if(c){const s=a(r),i=c.match(s);if(i)return this._(i.handler,i.options,t,e,n,i.params,o)}return new Response("Not Found",{status:404})}async _(t,e,n,o,r,s,i){const c=new u(n,o,r,s,i);if(!await this.S(c,e.validate))return c.text("Validation failed",400);if(!await this.p(c,e.auth))return c.text("Unauthorized",401);if(!await this.m(c,e.rateLimit))return c.text("Too Many Requests",429);let a,l=null;if(e.cache&&"GET"===n.method&&(l=await this.A(n),l))return this.T(c,e.cors),this.C(c,e.log),l;this.T(c,e.cors);try{const e=await t(c);a=e&&e.__isJSX?c.html(renderToString(e)):e instanceof Response?e:c.text("OK"),c.status=a.status}catch(t){console.error(t),a=c.text("Internal Server Error",500),c.status=500}return e.cache&&"GET"===n.method&&200===a.status&&c.executionCtx.waitUntil(this.O(n,a.clone(),e.cache)),e.compress&&(a=this.$(n,a)),this.C(c,e.log),a}};
33
+ var t=Object.defineProperty,e=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.prototype.hasOwnProperty,r=(e,n)=>{for(var o in n)t(e,o,{get:n[o],enumerable:!0})},i=(r,i,s,a)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let c of n(i))o.call(r,c)||c===s||t(r,c,{get:()=>i[c],enumerable:!(a=e(i,c))||a.enumerable});return r},s=e=>i(t({},"__esModule",{value:!0}),e),a={};function c(t){const e=[],n=t.length;let o=1;for(let r=1;r<=n;r++)r!==n&&47!==t.charCodeAt(r)||(r>o&&e.push(t.slice(o,r)),o=r+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}r(a,{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}var p=class{constructor(t,e,n,o={},r=null){this.req=t,this.env=e,this.executionCtx=n,this.params=o,this.status=200,this.headers=new Headers,this.headers.set("X-Content-Type-Options","nosniff"),this.headers.set("Referrer-Policy","strict-origin-when-cross-origin"),this.t=t.headers.get("Cookie")||"",this.o=null,this.url=r||new URL(t.url)}i(){if(null===this.o){const t=Object.create(null);if(this.t)for(const e of this.t.split(";")){const n=e.trim();if(!n)continue;const o=n.indexOf("=");if(o>0)try{const e=decodeURIComponent(n.slice(0,o)),r=decodeURIComponent(n.slice(o+1));f(e)&&(t[e]=r)}catch{}}this.o=t}return this.o}getCookie(t){return this.i()[t]??null}get query(){return this.url.searchParams}setCookie(t,e,n={}){if("string"!=typeof t||!f(t)||/[\r\n;=]/.test(t))return;let o=`${encodeURIComponent(t)}=${encodeURIComponent(e)}`;n.path&&(o+=`; Path=${n.path}`),n.domain&&(o+=`; Domain=${n.domain}`),void 0!==n.maxAge&&(o+=`; Max-Age=${n.maxAge}`),n.expires&&(o+=`; Expires=${n.expires.toUTCString()}`),n.secure&&(o+="; Secure"),n.httpOnly&&(o+="; HttpOnly"),n.sameSite&&(o+=`; SameSite=${n.sameSite}`),this.headers.append("Set-Cookie",o)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}text(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/plain; charset=utf-8"),new Response(t,{status:e,headers:o})}json(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","application/json; charset=utf-8"),new Response(JSON.stringify(t),{status:e,headers:o})}html(t,e=this.status,n={}){const o=this.l(n);return o.has("Content-Type")||o.set("Content-Type","text/html; charset=utf-8"),new Response(t,{status:e,headers:o})}l(t){if(0===Object.keys(t).length)return this.headers;const e=new Headers(this.headers);for(const n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.set(n,t[n]);return e}},Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n.flat(1/0),__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"]),w=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={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},m=/[&<>"']/g;function b(t){return m.lastIndex=0,m.test(t)?t.replace(m,t=>g[t]):t}var A=new Map,_=2e3;function T(t){const e=A.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return A.size>=_&&A.clear(),A.set(t,n),n}function C(t){if(!t||"object"!=typeof t)return"";const e=Object.entries(t);return 0===e.length?"":e.map(([t,e])=>{let n=T(t);return"number"!=typeof e||w.has(n)||(e=`${e}px`),`${n}:${e}`}).join(";")}function renderToString(t){if(null==t||"boolean"==typeof t)return"";if("string"==typeof t||"number"==typeof t)return b(String(t));if(Array.isArray(t)){const e=[];for(let n=0;n<t.length;n++)e.push(renderToString(t[n]));return e.join("")}if(!t.__isJSX)return b(String(t));const{type:e,props:n,children:o}=t;if(e===Fragment){const t=[];for(let e=0;e<o.length;e++)t.push(renderToString(o[e]));return t.join("")}if("symbol"==typeof e)return"";if("function"==typeof e)return renderToString(e({...n,children:o}));if("string"!=typeof e||!/^[A-Za-z][A-Za-z0-9-]*$/.test(e))return"";const r=d.has(e),i=[];for(const t in n){if(!Object.prototype.hasOwnProperty.call(n,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=n[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let o=t;if("className"===t||"class"===t?o="class":"htmlFor"!==t&&"for"!==t||(o="for"),f(o)&&!/^on/i.test(o)&&/^[A-Za-z_:][A-Za-z0-9_:.-]*$/.test(o)){if("href"===o||"src"===o||"xlink:href"===o||"action"===o||"formaction"===o){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(o))!0===e&&i.push(` ${o}`);else if(!0!==e){if("style"===o&&"object"==typeof e){const t=C(e);t&&i.push(` style="${b(t)}"`);continue}i.push(` ${o}="${b(String(e))}"`)}else i.push(` ${o}="true"`)}}const s=i.join("");let a="";if(n&&n.dangerouslySetInnerHTML&&null!=n.dangerouslySetInnerHTML.__html)a=n.dangerouslySetInnerHTML.__html;else{const t=[];for(let e=0;e<o.length;e++)t.push(renderToString(o[e]));a=t.join("")}return r?`<${e}${s}/>`:`<${e}${s}>${a}</${e}>`}var k=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},O=class{constructor(){this.root=new k}add(t,e,n){let o=this.root;const r=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0)){const t=n.slice(1);r.push(f(t)?t:`_p${e}`),o.paramChild||(o.paramChild=new k),o=o.paramChild}else{o.children||(o.children=new Map);let t=o.children.get(n);t||(t=new k,o.children.set(n,t)),o=t}}o.handler={handler:e,options:n,paramNames:r}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:o,options:r,paramNames:i}=n,s=Object.create(null);for(let t=0;t<i.length;t++)s[i[t]]=e[t];return{handler:o,options:r,params:s}}u(t,e,n,o){if(n===e.length)return t.handler;const r=e[n];if(t.children){const i=t.children.get(r);if(i){const t=this.u(i,e,n+1,o);if(t)return t}}if(t.paramChild){o.push(r);const i=this.u(t.paramChild,e,n+1,o);if(i)return i;o.pop()}return null}},$=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],S=new Map;function x(t,e,n){const o=`${t}|${e||""}|${n||""}`,r=Date.now(),i=S.get(o);if(i&&r-i<6e4)return!1;if(S.set(o,r),S.size>5e3)for(const[t,e]of S)r-e>6e4&&S.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(const t of $)this.staticRoutes.set(t,new Map);this.dynamicTries={};for(const t of $)this.dynamicTries[t]=new O;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,o){if("boolean"==typeof n&&(n={auth:n}),"function"==typeof n&&(o=n,n={}),"function"!=typeof o)throw new Error(`Handler for ${t} ${e} must be a function`);const r={...this.defaults,...n||{}};if(-1===e.indexOf(":")){const n=l(e);this.staticRoutes.get(t).set(n,{handler:o,options:r})}else{const n=c(e);this.dynamicTries[t].add(n,o,r)}}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 o=t.req.headers.get("CF-Connecting-IP")||"",r=t.url?t.url.pathname:"";if(!x(e,o,r))return;const i={ts:(new Date).toISOString(),kind:"security",event:e,detail:"string"==typeof n?n:void 0,ip:o||void 0,method:t.req.method,path:r,ua:t.req.headers.get("User-Agent")||void 0};console.log(JSON.stringify(i))}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 o=t.env[this.authKvBinding];if(!o)return{ok:!1,reason:"kv_unavailable"};let r,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 o="";for(let t=0;t<e.length;t++)o+=e[t].toString(16).padStart(2,"0");i="tok:"+o}try{r=await o.get(i)}catch(t){return{ok:!1,reason:"kv_error"}}if(!r)return{ok:!1,reason:"invalid_token"};if("object"==typeof e&&null!==e){let t;try{t=h(r)}catch{return{ok:!1,reason:"malformed_payload"}}if(t&&"object"==typeof t){if(null!=t.exp){const e=Date.now()/1e3;if("number"==typeof t.exp&&t.exp<e)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:r}}async A(t,e){if(!e)return!0;const n=!0===e?{}:e,{max:o=100,window:r=60}=n,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>=o)return!1;a++;try{await s.put(i,JSON.stringify(a),{expirationTtl:r})}catch{}return!0}_(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors,o=t.req.headers.get("Origin"),r=n.origin;"*"===r?t.headers.set("Access-Control-Allow-Origin","*"):Array.isArray(r)?o&&-1!==r.indexOf(o)&&(t.headers.set("Access-Control-Allow-Origin",o),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")):"string"==typeof r&&o===r&&(t.headers.set("Access-Control-Allow-Origin",o),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"),t.headers.set("Access-Control-Max-Age","86400")}T(t,e){e&&console.log(`${t.req.method} ${t.req.url} - ${t.status}`)}C(t,e){const n=t.headers.get("Accept-Encoding")||"";let o=null;if(n.includes("gzip")?o="gzip":n.includes("deflate")&&(o="deflate"),!o||!e.body)return e;const r=e.body.pipeThrough(new CompressionStream(o)),i=new Headers(e.headers);return i.set("Content-Encoding",o),i.set("Vary","Accept-Encoding"),i.delete("Content-Length"),new Response(r,{status:e.status,statusText:e.statusText,headers:i})}async k(t,e){return!e||("function"!=typeof e||!!await e(t))}async O(t){const e=caches.default;return await e.match(t)||null}async $(t,e,n){if(!n)return;const o=!0===n?{}:n,{ttl:r=3600,staleWhileRevalidate:i=0}=o,s=caches.default,a=e.clone();a.headers.set("Cache-Control",`max-age=${r}${i>0?`, stale-while-revalidate=${i}`:""}`),a.headers.delete("Set-Cookie"),await s.put(t,a)}async fetch(t,e,n){const o=new URL(t.url),r=l(o.pathname),i=t.method,s=this.staticRoutes.get(i);if(s){const i=s.get(r);if(i)return this.S(i.handler,i.options,t,e,n,Object.create(null),o)}const a=this.dynamicTries[i];if(a){const i=c(r),s=a.match(i);if(s)return this.S(s.handler,s.options,t,e,n,s.params,o)}return new Response("Not Found",{status:404})}async S(t,e,n,o,r,i,s){const a=new p(n,o,r,i,s);if(this.security.extraHeaders)for(const t in this.security.extraHeaders)Object.prototype.hasOwnProperty.call(this.security.extraHeaders,t)&&a.headers.set(t,this.security.extraHeaders[t]);let c,l=!1;try{l=await this.k(a,e.validate)}catch(t){this.p(a,"validation_error",t&&t.message),l=!1}if(!l)return a.text("Validation failed",400);try{c=await this.m(a,e.auth)}catch(t){this.p(a,"auth_error",t&&t.message),c={ok:!1,reason:"auth_error"}}if(!("boolean"==typeof c?c:c&&c.ok)){const t=c&&c.reason||"denied";return this.p(a,"auth_failure",t),a.text("Unauthorized",401)}let u=!1;try{u=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),u=!1}if(!u)return this.p(a,"rate_limit_exceeded",null),a.text("Too Many Requests",429,{"Retry-After":"60"});let f,h=null;if(e.cache&&"GET"===n.method&&(h=await this.O(n),h))return this._(a,e.cors),this.T(a,e.log),h;this._(a,e.cors);try{const e=await t(a);f=e&&e.__isJSX?a.html(renderToString(e)):e instanceof Response?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.$(n,f.clone(),e.cache)),e.compress&&(f=this.C(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.11",
3
+ "version": "0.0.13",
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
@@ -4,19 +4,35 @@ A minimal, high-performance framework for Cloudflare Workers with built-in serve
4
4
 
5
5
  Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
6
6
 
7
+ > **Security:** This release hardens both the server and the client against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls. See [Security](#security-controls) for the full breakdown of what's protected and how to opt into the strongest settings.
8
+
7
9
  ---
8
10
 
9
11
  ## Features
10
12
 
13
+ ### Server
11
14
  - **Trie-based routing** – static & dynamic routes (`/users/:id`) with fast lookups.
12
15
  - **JSX support** – render JSX components to HTML without a build step (using `jsx` and `renderToString`).
13
16
  - **Middleware options** – authentication, rate limiting, CORS, logging, caching, compression, and custom validation.
14
17
  - **Cookie helpers** – set, get, and delete cookies with flexible options.
15
- - **Declarative client** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically, with skeleton loaders and retry buttons.
16
- - **Resource injection** – automatically load CSS/JS files from response headers.
17
18
  - **Scheduled tasks** – built-in support for Cron triggers.
18
19
  - **Zero dependencies** – lightweight and fast.
19
20
 
21
+ ### Client
22
+ - **Declarative partial updates** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically.
23
+ - **Skeleton loaders & retry UI** – built-in loading and error states.
24
+ - **Safe DOM updates** – server responses are parsed with `DOMParser` and inserted via `importNode`; scripts are stripped and never re-executed.
25
+ - **CSRF-aware** – state-changing requests automatically send `X-CSRF-Token` from a `<meta name="csrf-token">` tag or an `XSRF-TOKEN` cookie.
26
+ - **Trusted Types aware** – plays nicely with a `require-trusted-types-for 'script'` CSP.
27
+
28
+ ### Security (both)
29
+ - **Prototype-pollution safe** – route params, cookies, JSON payloads, and JSX attributes all use forbidden-key filtering.
30
+ - **XSS-hardened JSX** – event handlers (`on*`), malformed tag/attr names, and `javascript:`/`data:text/html` URIs are blocked.
31
+ - **Structured security logging** – auth failures, rate-limit hits, and handler errors emit throttled JSON events.
32
+ - **Fail-closed middleware** – validation, auth, and rate-limit errors deny by default.
33
+ - **Opt-in token hashing** – SHA-256 hash auth tokens before KV lookup.
34
+ - **Strict CORS allowlist** – supports per-origin reflection with `Vary: Origin` and credentials.
35
+
20
36
  ---
21
37
 
22
38
  ## Installation
@@ -25,11 +41,9 @@ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of
25
41
  npm install @lengkapp/edge
26
42
  ```
27
43
 
28
- ---
29
-
30
44
  ## Quick Start
31
45
 
32
- **1. Create a worker script (`worker.js` or `src/index.ts`):**
46
+ **1. Create a worker script** (`worker.js` or `src/index.ts`):
33
47
 
34
48
  ```ts
35
49
  import { Edge, renderToString } from '@lengkapp/edge';
@@ -49,7 +63,7 @@ export default {
49
63
 
50
64
  **2. Create `wrangler.jsonc`:**
51
65
 
52
- Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
66
+ > Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
53
67
 
54
68
  ```jsonc
55
69
  {
@@ -105,16 +119,16 @@ Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
105
119
  Each handler receives a `Context` object (`ctx`) with the following members:
106
120
 
107
121
  | Property / Method | Description |
108
- | --- | --- |
122
+ |---|---|
109
123
  | `ctx.req` | The original `Request` object |
110
124
  | `ctx.env` | The environment bindings (KV, secrets, etc.) |
111
125
  | `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
112
- | `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
113
- | `ctx.status` | HTTP status code (default `200`) |
126
+ | `ctx.params` | Route parameters (prototype-safe object) |
127
+ | `ctx.status` | HTTP status code (default 200) |
114
128
  | `ctx.headers` | `Headers` object for the response |
115
129
  | `ctx.query` | `URLSearchParams` (lazy) |
116
130
  | `ctx.getCookie(name)` | Get a cookie value |
117
- | `ctx.setCookie(name, value, options)` | Set a cookie |
131
+ | `ctx.setCookie(name, value, options)` | Set a cookie (name is validated) |
118
132
  | `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
119
133
  | `ctx.text(data, status?, headers?)` | Return plain text |
120
134
  | `ctx.json(data, status?, headers?)` | Return JSON |
@@ -122,6 +136,13 @@ Each handler receives a `Context` object (`ctx`) with the following members:
122
136
 
123
137
  Cookie options: `path`, `domain`, `maxAge`, `expires`, `secure`, `httpOnly`, `sameSite`.
124
138
 
139
+ Every response automatically includes these safe headers:
140
+
141
+ - `X-Content-Type-Options: nosniff`
142
+ - `Referrer-Policy: strict-origin-when-cross-origin`
143
+
144
+ You can add more via `app.security.extraHeaders` (see [Security Controls](#security-controls)).
145
+
125
146
  ### Returning Responses
126
147
 
127
148
  You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
@@ -153,9 +174,11 @@ app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
153
174
 
154
175
  The client must send the token via `Authorization: Bearer <token>` or a cookie named `auth_token`.
155
176
 
177
+ If the KV payload includes an `exp` field (Unix seconds), expired tokens are rejected with a `401`. Malformed JSON payloads are rejected with `401` and logged as a security event.
178
+
156
179
  #### Rate Limiting (`rateLimit`)
157
180
 
158
- Requires a `RATE_LIMIT_KV` binding.
181
+ Requires a `RATE_LIMIT_KV` binding. Rate-limit responses include a `Retry-After: 60` header.
159
182
 
160
183
  ```ts
161
184
  app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
@@ -171,26 +194,36 @@ app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
171
194
 
172
195
  #### CORS (`cors`)
173
196
 
174
- Enables CORS headers.
197
+ Enables CORS headers. The default (`origin: '*'`) preserves legacy behavior. For production, use a strict allowlist:
175
198
 
176
199
  ```ts
200
+ // Legacy wildcard (default)
177
201
  app.get('/api', { cors: true }, handler);
178
202
 
179
- // Custom origin / methods
203
+ // Single origin
180
204
  app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
205
+
206
+ // Strict allowlist — reflects matching Origin, sets Vary: Origin and credentials
207
+ app.get('/api', {
208
+ cors: {
209
+ origin: ['https://app.example.com', 'https://admin.example.com'],
210
+ methods: 'GET,POST,PUT,DELETE',
211
+ headers: 'Content-Type, Authorization, X-CSRF-Token',
212
+ },
213
+ }, handler);
181
214
  ```
182
215
 
183
216
  #### Logging (`log`)
184
217
 
185
- Logs method, URL, and status to the console.
218
+ Logs method, URL, and status to the console. Separately, security events (auth failures, rate-limit hits, handler errors) are logged as structured JSON when `app.security.logSecurityEvents` is `true` (the default). Events are deduplicated per `(event, IP, path)` over a 60-second window to prevent log flooding.
186
219
 
187
220
  #### Compression (`compress`)
188
221
 
189
- Compresses the response using gzip/deflate/br if the client supports it.
222
+ Compresses the response using gzip/deflate if the client supports it.
190
223
 
191
224
  #### Custom Validation (`validate`)
192
225
 
193
- A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
226
+ A function returning `true`/`false` (or a `Promise`) to allow/deny the request. Validation errors fail closed.
194
227
 
195
228
  ```ts
196
229
  app.post('/submit', {
@@ -200,6 +233,48 @@ app.post('/submit', {
200
233
 
201
234
  ---
202
235
 
236
+ ## Security Controls
237
+
238
+ All controls are optional and default to behavior that preserves previous releases. Enable them after construction:
239
+
240
+ ```ts
241
+ const app = new Edge();
242
+
243
+ // 1. Strict CORS allowlist (replaces the default '*')
244
+ app.defaults.cors.origin = ['https://app.example.com', 'https://admin.example.com'];
245
+
246
+ // 2. Hash auth tokens with SHA-256 before KV lookup.
247
+ // Only enable after migrating existing tokens to `tok:<sha256-hex>` keys.
248
+ app.security.hashAuthTokens = true;
249
+
250
+ // 3. Add HSTS, COOP, COEP, CORP at the edge.
251
+ app.security.extraHeaders = {
252
+ 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
253
+ 'Cross-Origin-Opener-Policy': 'same-origin',
254
+ 'Cross-Origin-Resource-Policy': 'same-origin',
255
+ };
256
+
257
+ // 4. Disable structured security logs if you ship logs elsewhere.
258
+ // app.security.logSecurityEvents = false;
259
+ ```
260
+
261
+ ### What's Protected
262
+
263
+ | OWASP 2025 Category | Protection in @lengkapp/edge |
264
+ |---|---|
265
+ | A01: Broken Access Control | Structured auth result with reasons; expiry enforcement when `exp` is present; role/scope checks; strict target resolution on the client. |
266
+ | A02: Security Misconfiguration | Safe default headers (`nosniff`, `Referrer-Policy`); strict CORS allowlist support; per-origin reflection with `Vary: Origin`. |
267
+ | A03: Software Supply Chain | Zero runtime dependencies; SRI attributes honored on the client for `x-js-required` / `x-css-required`. |
268
+ | A04: Cryptographic Failures | Optional SHA-256 token hashing before KV lookup (`app.security.hashAuthTokens`). |
269
+ | A05: Injection / XSS / Prototype Pollution | `Object.create(null)` for params, cookies, and JSON; forbidden-key filtering in route segments, JSON payloads, cookie names, and JSX attributes; event-handler attributes blocked; `javascript:`/`data:text/html` URIs rejected; client uses `DOMParser` + `importNode` and never executes inline scripts. |
270
+ | A06: Insecure Design | Numeric-sanitized rate limiter; `Retry-After` on 429; sliding counter per IP. |
271
+ | A07: Authentication Failures | Specific failure reasons logged; no swallowed catches in the auth path. |
272
+ | A08: Data Integrity | Prototype-safe JSON parsing for KV payloads; `Set-Cookie` stripped from cached responses. |
273
+ | A09: Logging Failures | Structured JSON security events with 60-second dedupe window. |
274
+ | A10: Exceptional Conditions | Validation, auth, and rate-limit middleware fail closed on errors; handler errors never leak internals. |
275
+
276
+ ---
277
+
203
278
  ## JSX Support
204
279
 
205
280
  The package includes a minimal JSX runtime. Write components as functions returning JSX.
@@ -226,6 +301,17 @@ If your handler returns a JSX element directly, Edge will automatically render i
226
301
  app.get('/auto', (ctx) => <h1>Auto rendered</h1>);
227
302
  ```
228
303
 
304
+ ### JSX Hardening
305
+
306
+ `renderToString` applies the following protections automatically:
307
+
308
+ - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$` — anything else is dropped.
309
+ - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$` and are rejected if they start with `on` (event handlers never serialize).
310
+ - `href`, `src`, `xlink:href`, `action`, and `formaction` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
311
+ - All string values are HTML-escaped (`&`, `<`, `>`, `"`, `'`).
312
+ - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected as attribute names.
313
+ - `dangerouslySetInnerHTML` remains available but is opt-in and never applied to attribute values.
314
+
229
315
  ---
230
316
 
231
317
  ## Scheduled Tasks
@@ -262,30 +348,33 @@ Then use HTML attributes to make elements fetch content asynchronously.
262
348
  ### Attributes
263
349
 
264
350
  | Attribute | Description |
265
- | --- | --- |
351
+ |---|---|
266
352
  | `_get` | URL to fetch via GET |
267
353
  | `_post` | URL to fetch via POST |
268
- | `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
354
+ | `_put`, `_patch`, `_delete` | Other HTTP methods |
355
+ | `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. Only simple selectors (`#id`, `.class`, `tag`) are accepted for safety. |
269
356
  | `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
270
357
  | `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
271
358
  | `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
359
+ | `_skeleton` | Set to `"false"` to disable the loading skeleton. |
360
+ | `_retry` | Set to `"false"` to disable the retry button on error. |
272
361
 
273
362
  ### Examples
274
363
 
275
- **Load content on click:**
364
+ Load content on click:
276
365
 
277
366
  ```html
278
367
  <button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
279
368
  <div id="posts"></div>
280
369
  ```
281
370
 
282
- **Auto-load on page load:**
371
+ Auto-load on page load:
283
372
 
284
373
  ```html
285
374
  <div _get="/user-profile" _target="this"></div>
286
375
  ```
287
376
 
288
- **Post a form via AJAX:**
377
+ Post a form via AJAX:
289
378
 
290
379
  ```html
291
380
  <form id="contact-form">
@@ -295,7 +384,7 @@ Then use HTML attributes to make elements fetch content asynchronously.
295
384
  <div id="result"></div>
296
385
  ```
297
386
 
298
- **Send JSON data:**
387
+ Send JSON data:
299
388
 
300
389
  ```html
301
390
  <input name="username" />
@@ -303,7 +392,7 @@ Then use HTML attributes to make elements fetch content asynchronously.
303
392
  <button _post="/login" _json="username,password" _target="#status">Login</button>
304
393
  ```
305
394
 
306
- **Load when element becomes visible (IntersectionObserver):**
395
+ Load when element becomes visible (IntersectionObserver):
307
396
 
308
397
  ```html
309
398
  <div _get="/lazy-content" _target="this" _trigger="visible"></div>
@@ -311,11 +400,40 @@ Then use HTML attributes to make elements fetch content asynchronously.
311
400
 
312
401
  ### Skeleton Loading
313
402
 
314
- While the request is in progress, the target container is filled with a skeleton loader (three shimmer bars). On error, a "Retry" button is shown.
403
+ While the request is in progress, the target container is filled with a skeleton loader (a pulsing ring icon). On error, a "Retry" button is shown; clicking it re-runs the same request once.
404
+
405
+ ### Safe DOM Updates
406
+
407
+ Server responses are handled as follows:
408
+
409
+ - Fetched with `credentials: 'same-origin'` and `X-Requested-With: XMLHttpRequest`.
410
+ - `Content-Type` is validated — only `text/html` and `text/plain` are accepted.
411
+ - The HTML is parsed in a detached `DOMParser` document.
412
+ - All `<script>` elements are removed before insertion.
413
+ - Nodes are imported with `document.importNode()` — never `innerHTML`.
414
+ - Any new AJAX-enabled elements in the response are bound to the same delegation system.
415
+
416
+ ### CSRF Protection
417
+
418
+ For state-changing methods (POST, PUT, PATCH, DELETE), the client reads a CSRF token from:
419
+
420
+ 1. `<meta name="csrf-token" content="...">` (preferred), or
421
+ 2. The `XSRF-TOKEN` cookie.
422
+
423
+ The token is sent as `X-CSRF-Token` on the request. Your server handler can verify it:
424
+
425
+ ```ts
426
+ app.post('/submit', {
427
+ validate: (ctx) => {
428
+ const sent = ctx.req.headers.get('X-CSRF-Token');
429
+ return sent && sent === ctx.getCookie('csrf_token');
430
+ }
431
+ }, handler);
432
+ ```
315
433
 
316
434
  ### Resource Injection
317
435
 
318
- If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page.
436
+ If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page. Each URL can optionally include a matching `x-css-integrity` or `x-js-integrity` header containing a Subresource Integrity hash.
319
437
 
320
438
  Example server route:
321
439
 
@@ -323,15 +441,21 @@ Example server route:
323
441
  app.get('/widget', (ctx) => {
324
442
  ctx.headers.set('x-css-required', '["/widget.css"]');
325
443
  ctx.headers.set('x-js-required', '["/widget.js"]');
444
+ // Optional SRI — applies to every URL in the corresponding list
445
+ ctx.headers.set('x-js-integrity', 'sha384-...');
326
446
  return ctx.html('<div class="widget">...</div>');
327
447
  });
328
448
  ```
329
449
 
450
+ ### Trusted Types
451
+
452
+ If your page enforces `require-trusted-types-for 'script'`, the client detects `window.trustedTypes` and creates an `aj-policy`. Only hardcoded UI strings (skeleton, retry) pass through the policy; server content never does.
453
+
330
454
  ---
331
455
 
332
456
  ## Configuration
333
457
 
334
- ### `wrangler.jsonc`
458
+ ### wrangler.jsonc
335
459
 
336
460
  If you use authentication or rate limiting, you need KV namespaces. Create them first:
337
461
 
@@ -389,7 +513,7 @@ crons = ["*/5 * * * *"]
389
513
 
390
514
  </details>
391
515
 
392
- Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
516
+ > Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
393
517
 
394
518
  ### Environment Variables
395
519
 
@@ -400,6 +524,23 @@ app.authKvBinding = 'CUSTOM_AUTH_KV';
400
524
  app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
401
525
  ```
402
526
 
527
+ ### Recommended CSP
528
+
529
+ The client and JSX runtime are compatible with a strict Content Security Policy. A reasonable starting point:
530
+
531
+ ```text
532
+ Content-Security-Policy:
533
+ default-src 'self';
534
+ script-src 'self';
535
+ style-src 'self';
536
+ img-src 'self' data:;
537
+ require-trusted-types-for 'script';
538
+ frame-ancestors 'none';
539
+ base-uri 'self';
540
+ ```
541
+
542
+ Set this via `app.security.extraHeaders` or at your edge/CDN layer.
543
+
403
544
  ---
404
545
 
405
546
  ## Full Example
@@ -410,6 +551,16 @@ import { HomePage } from './Page.jsx';
410
551
 
411
552
  const app = new Edge();
412
553
 
554
+ // Strict CORS allowlist for production
555
+ app.defaults.cors.origin = ['https://app.example.com'];
556
+
557
+ // HSTS + isolation headers
558
+ app.security.extraHeaders = {
559
+ 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
560
+ 'Cross-Origin-Opener-Policy': 'same-origin',
561
+ 'Cross-Origin-Resource-Policy': 'same-origin',
562
+ };
563
+
413
564
  // Basic text
414
565
  app.get('/', (ctx) => ctx.text('Hello from Edge!'));
415
566
 
@@ -436,7 +587,7 @@ app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
436
587
 
437
588
  // Cookies
438
589
  app.get('/set-cookie', (ctx) => {
439
- ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
590
+ ctx.setCookie('session', 'abc123', { httpOnly: true, secure: true, sameSite: 'Lax', path: '/' });
440
591
  return ctx.text('Cookie set');
441
592
  });
442
593
 
@@ -460,52 +611,60 @@ export default {
460
611
 
461
612
  ## Performance
462
613
 
463
- @lengkapp/edge is inspired by [Hono](https://hono.dev) — one of the fastest Workers frameworks around — but ships with **zero dependencies**. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
614
+ @lengkapp/edge is inspired by Hono — one of the fastest Workers frameworks around — but ships with zero dependencies. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
464
615
 
465
616
  ### Test setup
466
617
 
467
- - **Tool:** `autocannon` — 10 connections, 10s run per route
618
+ - **Tool:** autocannon — 10 connections, 10s run per route
468
619
  - **Target:** local `wrangler dev` server
469
620
  - **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
470
- - **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
621
+ - **Contenders:** @lengkapp/edge, hono, hono/tiny, and a native Workers handler with no framework
471
622
 
472
623
  > This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
473
624
 
474
625
  ### Results
475
626
 
476
627
  | Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
477
- | --- | --- | --- | --- | --- | --- |
478
- | **@lengkapp/edge** | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
479
- | **@lengkapp/edge** | `/json` | **500.4** | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
480
- | Hono | `/text` | **506.0** | 19.27 ms | 5,000 / 10.03s | 455 kB |
628
+ |---|---|---|---|---|---|
629
+ | @lengkapp/edge | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
630
+ | @lengkapp/edge | `/json` | 500.4 | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
631
+ | Hono | `/text` | 506.0 | 19.27 ms | 5,000 / 10.03s | 455 kB |
481
632
  | Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
482
633
  | Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
483
634
  | Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
484
635
  | Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
485
636
  | Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
486
637
 
487
- ```mermaid
488
- xychart-beta
489
- title "Avg requests/sec — GET /text"
490
- x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
491
- y-axis "Req/sec" 490 --> 510
492
- bar [503.1, 506.0, 505.5, 498.4]
493
- ```
494
-
495
- ```mermaid
496
- xychart-beta
497
- title "Avg requests/sec — GET /json"
498
- x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
499
- y-axis "Req/sec" 490 --> 505
500
- bar [500.4, 495.2, 497.9, 499.7]
501
- ```
502
-
503
638
  ### Takeaways
504
639
 
505
640
  - On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
506
641
  - On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
507
- - All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with **zero runtime dependencies**, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
642
+ - All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with zero runtime dependencies, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
508
643
  - @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
644
+ - The 2025 security hardening adds only cheap operations to the request path: prototype-safe objects (`Object.create(null)`), two regex checks per JSX attribute, and a throttled logger that skips duplicate events within 60 seconds. No new async boundaries, no hashing on the hot path (`hashAuthTokens` defaults to `false`), and no added dependencies.
645
+
646
+ ---
647
+
648
+ ## Migration Notes
649
+
650
+ ### Upgrading from ≤ 0.x
651
+
652
+ The security release is backward-compatible. Existing apps continue to work without code changes. New defaults:
653
+
654
+ - Every response now includes `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin`.
655
+ - Rate-limited responses include `Retry-After: 60`.
656
+ - Route params, cookie stores, and JSON payloads use prototype-safe objects. If you were relying on `ctx.params.hasOwnProperty(...)`, use `Object.prototype.hasOwnProperty.call(ctx.params, ...)` or `'x' in ctx.params`.
657
+ - The JSX renderer now drops `on*` attributes. Handlers relying on server-rendered inline event handlers must be moved to the client.
658
+ - Security events are logged as JSON. To disable: `app.security.logSecurityEvents = false`.
659
+
660
+ ### Client upgrade
661
+
662
+ If you were using the old client that executed `<script>` tags in responses, replace those scripts with either:
663
+
664
+ - DOM-based initialization (event delegation, `IntersectionObserver`, etc.), or
665
+ - An explicit `<script src="...">` injected via `x-js-required` on the response.
666
+
667
+ The new client refuses to run inline scripts for security reasons and will not execute `<script>` elements found in response bodies.
509
668
 
510
669
  ---
511
670