@lengkapp/edge 0.0.16 → 0.0.18
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 +26 -59
- package/dist/edge-server.js +2 -2
- package/package.json +1 -1
- package/readme.md +173 -375
package/dist/edge-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
2
|
+
* @lengkapp/edge v0.0.18
|
|
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
|
|
33
|
+
!function(){if(!window.__edge){window.__edge=!0;var t=document,e=t.head,n=t.documentElement,r=["get","post","put","patch","delete"].map(function(t){return"_"+t}),i={post:1,put:1,patch:1},o={post:1,put:1,patch:1,delete:1},a=r.map(function(t){return"["+t+"]"}).join(","),c=new Set(["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","contextmenu","focusin","focusout","keydown","keyup","keypress","change","input","submit","reset","wheel","touchstart","touchend","touchmove"]),u={__proto__:1,constructor:1,prototype:1},s=["href","src","action","formaction","xlink:href","data"],f=/^\s*(?:javascript|vbscript|data:text\/html)/i,l=new WeakSet,d=new WeakSet,h=new WeakMap,v=new WeakMap,p=new Map,m=new Set,w=new Set,g=null;if(window.trustedTypes&&window.trustedTypes.createPolicy)try{g=window.trustedTypes.createPolicy("edge-policy",{createHTML:function(e){return function(e){var n=t.createElement("template");n.innerHTML=e;for(var r=n.content,i=r.querySelectorAll('base,meta[http-equiv="refresh" i],iframe[srcdoc],object,embed,form[action^="javascript:" i]'),o=i.length-1;o>=0;o--)i[o].remove();for(var a=r.querySelectorAll("*"),c=0;c<a.length;c++)for(var u=a[c],l=0;l<s.length;l++){var d=u.getAttribute(s[l]);d&&f.test(d)&&u.removeAttribute(s[l])}return n.innerHTML}(String(e))}})}catch(t){g=null}var y=t.createElement("style");y.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}",e.appendChild(y);var k=!1,b=new Set,x=new Set,j=!1;new MutationObserver(function(t){t.forEach(function(t){t.addedNodes.forEach(function(t){1===t.nodeType&&b.add(t)}),t.removedNodes.forEach(function(t){1===t.nodeType&&x.add(t)})}),j||(j=!0,queueMicrotask(W))}).observe(n,{childList:!0,subtree:!0}),"loading"===t.readyState?t.addEventListener("DOMContentLoaded",B,{once:!0}):B()}function M(t,e){t.innerHTML=g?g.createHTML(e):e}function S(t){for(var e=null,n=0;n<r.length;n++)if(t.hasAttribute(r[n])){e=r[n];break}if(!e)return null;var o=e.slice(1);return{url:t.getAttribute(e),method:o.toUpperCase(),hasBody:!!i[o],target:t.getAttribute("_target"),trigger:t.getAttribute("_trigger"),form:t.getAttribute("_form"),json:t.getAttribute("_json"),skeleton:"false"!==t.getAttribute("_skeleton"),retry:"false"!==t.getAttribute("_retry")}}function _(t){return(t.trigger||("this"===t.target?"load":"click")).split(",").map(function(t){return t.trim().toLowerCase()})}function q(t){return t?t.replace(/^\[|\]$/g,"").split(",").map(function(t){return t.trim().replace(/^['"]|['"]$/g,"")}).filter(Boolean):[]}function L(n,r){if(!d.has(n)){d.add(n);var i=function(e,n){if(!n.target||"this"===n.target)return e;var r=h.get(e);if(r&&r.isConnected)return r;var i=null;try{i=t.querySelector(n.target)}catch(t){i=null}return i&&h.set(e,i),i}(n,r);if(i){r.skeleton&&function(t){M(t,'<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 a={method:r.method,credentials:"same-origin",headers:{"X-Requested-With":"XMLHttpRequest"}};if(o[r.method.toLowerCase()]){var c=function(){var e=t.querySelector('meta[name="csrf-token"]');if(e)return e.getAttribute("content");var n=t.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);if(n)try{return decodeURIComponent(n[1])}catch(t){return n[1]}return null}();c&&(a.headers["X-CSRF-Token"]=c)}if(r.hasBody)if(r.json){var s=Object.create(null);r.json.split(",").forEach(function(e){if((e=e.trim())&&!u[e]){var n=null;try{n=t.querySelector('[name="'+e.replace(/["\\]/g,"")+'"]')}catch(t){n=null}s[e]=n?n.value:void 0}}),a.headers["Content-Type"]="application/json",a.body=JSON.stringify(s)}else if(r.form){var f=t.getElementById(r.form);a.headers["Content-Type"]="application/x-www-form-urlencoded",a.body=f?new URLSearchParams(new FormData(f)).toString():""}fetch(r.url,a).then(function(n){if(!n.ok)throw new Error("HTTP "+n.status);var r,i=(n.headers.get("content-type")||"").toLowerCase();if(i&&-1===i.indexOf("text/html")&&-1===i.indexOf("text/plain"))throw new Error("Unsupported content type: "+i);return q((r=n.headers).get("x-css-required")).forEach(function(n){if(!m.has(n)){m.add(n);var i=t.createElement("link");i.rel="stylesheet",i.href=n;var o=r.get("x-css-integrity");o&&(i.integrity=o,i.crossOrigin="anonymous"),e.appendChild(i)}}),q(r.get("x-js-required")).forEach(function(n){if(!m.has(n)){m.add(n);var i=t.createElement("script");i.src=n;var o=r.get("x-js-integrity");o&&(i.integrity=o,i.crossOrigin="anonymous"),e.appendChild(i)}}),n.text()}).then(function(e){!function(e,n){var r,i,o,a,c;e.textContent="",M(e,n),r=e,o=(i=t.querySelector("script[nonce]"))&&(i.nonce||i.getAttribute("nonce"))||null,a=Array.prototype.slice.call(r.querySelectorAll("script")),c=0,function e(){if(!(c>=a.length)){var n=a[c],r=n.nonce||n.getAttribute("nonce")||"";if(o&&r!==o)return n.remove(),c++,void e();for(var i=t.createElement("script"),u=0;u<n.attributes.length;u++)i.setAttribute(n.attributes[u].name,n.attributes[u].value);if(r&&(i.nonce=r),n.src){if(w.has(n.src))return n.remove(),c++,void e();w.add(n.src),i.onload=i.onerror=function(){c++,e()},n.replaceWith(i)}else i.textContent="{\n"+n.textContent+"\n}",n.replaceWith(i),c++,e()}}(),R(e)}(i,e)}).catch(function(t){console.error(t),r.retry&&function(t,e){M(t,'<div class="aj-retry-wrap"><button type="button" class="aj-retry" 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" 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></div>');var n=t.querySelector(".aj-retry");n&&n.addEventListener("click",e,{once:!0})}(i,function(){L(n,r)})}).finally(function(){d.delete(n)})}else d.delete(n)}}function O(t){if(!l.has(t)){l.add(t);var e=S(t);e&&_(e).forEach(function(n){if("load"===n||"domcontentloaded"===n||"ready"===n)queueMicrotask(function(){L(t,e)});else if("visible"===n||"intersect"===n){v.set(t,!1);var r=new IntersectionObserver(function(n){n.forEach(function(n){if(!n.target.isConnected)return r.disconnect(),void p.delete(t);var i=n.isIntersecting&&function(t){var e=getComputedStyle(t);if("none"===e.display||"hidden"===e.visibility||"0"===e.opacity)return!1;var n=t.getBoundingClientRect();return n.width>0&&n.height>0}(t);i&&!v.get(t)&&L(t,e),v.set(t,i)})});r.observe(t),p.set(t,r)}else c.has(n)||t.addEventListener(n,function(){L(t,e)})})}}function R(t){1===t.nodeType&&(t.matches&&t.matches(a)&&O(t),t.querySelectorAll&&t.querySelectorAll(a).forEach(O))}function C(t){p.size&&p.forEach(function(e,n){(t===n||t.contains(n))&&(e.disconnect(),p.delete(n),l.delete(n))})}function W(){j=!1;var t=Array.from(b);b.clear();var e=Array.from(x);x.clear(),t.forEach(R),e.forEach(C)}function B(){k||(k=!0,c.forEach(function(e){var n="click"===e||"submit"===e;t.addEventListener(e,function(t){var r=t.target;if(r instanceof Element){var i=r.closest(a);if(i){var o=S(i);o&&-1!==_(o).indexOf(e)&&(n&&t.preventDefault(),L(i,o))}}},!n&&{passive:!0})})),R(n)}}();
|
package/dist/edge-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
2
|
+
* @lengkapp/edge v0.0.18
|
|
3
3
|
* MIT License
|
|
4
4
|
*
|
|
5
5
|
* LengkApp Edge License
|
|
@@ -54,29 +54,14 @@ declare class Context {
|
|
|
54
54
|
|
|
55
55
|
getCookie(name: string): string | null;
|
|
56
56
|
readonly query: URLSearchParams;
|
|
57
|
-
setCookie(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
secure?: boolean;
|
|
66
|
-
httpOnly?: boolean;
|
|
67
|
-
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
68
|
-
}
|
|
69
|
-
): void;
|
|
70
|
-
deleteCookie(
|
|
71
|
-
name: string,
|
|
72
|
-
options?: {
|
|
73
|
-
path?: string;
|
|
74
|
-
domain?: string;
|
|
75
|
-
secure?: boolean;
|
|
76
|
-
httpOnly?: boolean;
|
|
77
|
-
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
78
|
-
}
|
|
79
|
-
): void;
|
|
57
|
+
setCookie(name: string, value: string, options?: {
|
|
58
|
+
path?: string; domain?: string; maxAge?: number; expires?: Date;
|
|
59
|
+
secure?: boolean; httpOnly?: boolean; sameSite?: 'Strict' | 'Lax' | 'None';
|
|
60
|
+
}): void;
|
|
61
|
+
deleteCookie(name: string, options?: {
|
|
62
|
+
path?: string; domain?: string; secure?: boolean; httpOnly?: boolean;
|
|
63
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
64
|
+
}): void;
|
|
80
65
|
|
|
81
66
|
text(data: string, status?: number, headers?: Record<string, string>): Response;
|
|
82
67
|
json(data: any, status?: number, headers?: Record<string, string>): Response;
|
|
@@ -95,29 +80,12 @@ export interface JSXElement {
|
|
|
95
80
|
}
|
|
96
81
|
|
|
97
82
|
export function jsx(type: any, props: Record<string, any> | null, ...children: any[]): JSXElement;
|
|
98
|
-
|
|
99
83
|
export function renderToString(node: any): string;
|
|
100
84
|
|
|
101
|
-
interface AuthOptions {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface RateLimitOptions {
|
|
107
|
-
max?: number;
|
|
108
|
-
window?: number;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface CorsOptions {
|
|
112
|
-
origin?: string;
|
|
113
|
-
methods?: string;
|
|
114
|
-
headers?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
interface CacheOptions {
|
|
118
|
-
ttl?: number;
|
|
119
|
-
staleWhileRevalidate?: number;
|
|
120
|
-
}
|
|
85
|
+
interface AuthOptions { role?: string; scopes?: string[]; }
|
|
86
|
+
interface RateLimitOptions { max?: number; window?: number; }
|
|
87
|
+
interface CorsOptions { origin?: string | string[]; methods?: string; headers?: string; }
|
|
88
|
+
interface CacheOptions { ttl?: number; staleWhileRevalidate?: number; }
|
|
121
89
|
|
|
122
90
|
interface RouteOptions {
|
|
123
91
|
auth?: boolean | AuthOptions;
|
|
@@ -129,46 +97,45 @@ interface RouteOptions {
|
|
|
129
97
|
log?: boolean;
|
|
130
98
|
}
|
|
131
99
|
|
|
100
|
+
interface SecurityOptions {
|
|
101
|
+
hashAuthTokens?: boolean;
|
|
102
|
+
logSecurityEvents?: boolean;
|
|
103
|
+
extraHeaders?: Record<string, string> | null;
|
|
104
|
+
trustedProxies?: string[] | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
132
107
|
type RouteHandler = (ctx: Context) => Response | JSXElement | void | Promise<Response | JSXElement | void>;
|
|
108
|
+
type ScheduledHandler = (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>;
|
|
133
109
|
|
|
134
110
|
export class Edge {
|
|
135
111
|
constructor();
|
|
136
|
-
|
|
137
112
|
authKvBinding: string;
|
|
138
113
|
rateLimitKvBinding: string;
|
|
139
|
-
|
|
114
|
+
defaults: { cors: CorsOptions };
|
|
115
|
+
scheduledHandler: ScheduledHandler | null;
|
|
116
|
+
security: Required<SecurityOptions>;
|
|
140
117
|
|
|
141
118
|
get(path: string, handler: RouteHandler): void;
|
|
142
119
|
get(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
143
|
-
|
|
144
120
|
post(path: string, handler: RouteHandler): void;
|
|
145
121
|
post(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
146
|
-
|
|
147
122
|
put(path: string, handler: RouteHandler): void;
|
|
148
123
|
put(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
149
|
-
|
|
150
124
|
delete(path: string, handler: RouteHandler): void;
|
|
151
125
|
delete(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
152
|
-
|
|
153
126
|
patch(path: string, handler: RouteHandler): void;
|
|
154
127
|
patch(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
155
|
-
|
|
156
128
|
options(path: string, handler: RouteHandler): void;
|
|
157
129
|
options(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
158
|
-
|
|
159
130
|
head(path: string, handler: RouteHandler): void;
|
|
160
131
|
head(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
|
|
161
|
-
|
|
162
|
-
scheduled(handler: (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>): void;
|
|
163
|
-
|
|
132
|
+
scheduled(handler: ScheduledHandler): void;
|
|
164
133
|
fetch(request: Request, env: any, executionCtx: ExecutionContext): Promise<Response>;
|
|
165
134
|
}
|
|
166
135
|
|
|
167
136
|
declare global {
|
|
168
137
|
namespace JSX {
|
|
169
138
|
interface Element extends JSXElement {}
|
|
170
|
-
interface IntrinsicElements {
|
|
171
|
-
[elemName: string]: any;
|
|
172
|
-
}
|
|
139
|
+
interface IntrinsicElements { [elemName: string]: any; }
|
|
173
140
|
}
|
|
174
141
|
}
|
package/dist/edge-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
2
|
+
* @lengkapp/edge v0.0.18
|
|
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={"&":"&","<":"<",">":">",'"':""","'":"'"},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("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=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"]),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={"&":"&","<":"<",">":">",'"':""","'":"'"},w=/[&<>"']/g;function b(t){return w.lastIndex=0,w.test(t)?t.replace(w,t=>g[t]):t}var A=new Map,T=2e3;function _(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>=T&&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);return"number"!=typeof e||m.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}},S=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],$=new Map;function E(t,e,n){const o=`${t}|${e||""}|${n||""}`,r=Date.now(),i=$.get(o);if(i&&r-i<6e4)return!1;if($.set(o,r),$.size>5e3)for(const[t,e]of $)r-e>6e4&&$.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(const t of S)this.staticRoutes.set(t,new Map);this.dynamicTries={};for(const t of S)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 this.dynamicTries[t].add(c(e),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(!E(e,o,r))return;console.log(JSON.stringify({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}))}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{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(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, X-CSRF-Token"),t.headers.set("Access-Control-Max-Age","86400")}_(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){return await caches.default.match(t)||null}async S(t,e,n){if(!n)return;const o=!0===n?{}:n,{ttl:r=3600,staleWhileRevalidate:i=0}=o,s=e.clone();s.headers.set("Cache-Control",`max-age=${r}${i>0?`, stale-while-revalidate=${i}`:""}`),s.headers.delete("Set-Cookie"),await caches.default.put(t,s)}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.$(i.handler,i.options,t,e,n,Object.create(null),o)}const a=this.dynamicTries[i];if(a){const i=a.match(c(r));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,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))return this.p(a,"auth_failure",c&&c.reason||"denied"),a.text("Unauthorized",401);let u,f=!1;try{f=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),f=!1}if(!f)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.T(a,e.cors),this._(a,e.log),t}this.T(a,e.cors);try{const e=await t(a);u=e&&e.__isJSX?a.html(renderToString(e)):e instanceof Response?e:a.text("OK"),a.status=u.status}catch(t){this.p(a,"handler_error",t&&t.message),u=a.text("Internal Server Error",500),a.status=500}return e.cache&&"GET"===n.method&&200===u.status&&a.executionCtx.waitUntil(this.S(n,u.clone(),e.cache)),e.compress&&(u=this.C(n,u)),this._(a,e.log),u}};
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,20 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
|
|
4
4
|
|
|
5
|
-
Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**.
|
|
5
|
+
Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**.
|
|
6
|
+
|
|
7
|
+
> **Security:** Server and client are hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls. The client sanitizes every response, works under `require-trusted-types-for 'script'` CSP, is nonce-aware for script execution, and sends CSRF tokens on state-changing requests. See [Security](#security).
|
|
6
8
|
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
|
|
13
|
+
### Server
|
|
14
|
+
- **Trie-based routing** – static & dynamic routes (`/users/:id`).
|
|
15
|
+
- **JSX support** – render JSX to HTML without a build step.
|
|
16
|
+
- **Middleware** – auth, rate limiting, CORS, logging, caching, compression, validation.
|
|
17
|
+
- **Cookie helpers** with validation.
|
|
18
|
+
- **Scheduled tasks** via Cron triggers.
|
|
19
|
+
- **Zero dependencies.**
|
|
20
|
+
|
|
21
|
+
### Client
|
|
22
|
+
- **Declarative partial updates** via `_get`, `_post`, `_target`, `_trigger`.
|
|
23
|
+
- **Trusted Types aware** – routes all `innerHTML` through an `edge-policy` sanitizer.
|
|
24
|
+
- **Nonce-aware script execution** – response scripts must match the page's CSP nonce when one exists.
|
|
25
|
+
- **Built-in sanitizer** – strips `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and `javascript:`/`vbscript:`/`data:text/html` URLs.
|
|
26
|
+
- **CSRF-aware** – sends `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
|
|
27
|
+
- **Content-Type guarded** – only `text/html` and `text/plain` responses are inserted.
|
|
28
|
+
|
|
29
|
+
### Security (both)
|
|
30
|
+
- **Prototype-pollution safe** – route params, cookies, JSON bodies, JSX attributes.
|
|
31
|
+
- **XSS-hardened JSX** – no `on*` attributes, no `javascript:` URLs, no malformed tag names.
|
|
32
|
+
- **Structured security logging** – throttled JSON events for auth/rate-limit/handler failures.
|
|
33
|
+
- **Fail-closed middleware** – validation, auth, and rate limit errors deny by default.
|
|
34
|
+
- **Opt-in token hashing** – SHA-256 before KV lookup.
|
|
35
|
+
- **Strict CORS allowlist** – per-origin reflection with `Vary: Origin`.
|
|
19
36
|
|
|
20
37
|
---
|
|
21
38
|
|
|
@@ -25,490 +42,271 @@ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of
|
|
|
25
42
|
npm install @lengkapp/edge
|
|
26
43
|
```
|
|
27
44
|
|
|
28
|
-
---
|
|
29
|
-
|
|
30
45
|
## Quick Start
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
`worker.js`:
|
|
33
48
|
|
|
34
49
|
```ts
|
|
35
|
-
import { Edge
|
|
50
|
+
import { Edge } from '@lengkapp/edge';
|
|
36
51
|
|
|
37
52
|
const app = new Edge();
|
|
38
53
|
|
|
39
54
|
app.get('/', (ctx) => ctx.text('Hello World!'));
|
|
40
|
-
|
|
41
|
-
app.get('/users/:id', (ctx) => {
|
|
42
|
-
return ctx.json({ id: ctx.params.id });
|
|
43
|
-
});
|
|
55
|
+
app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
|
|
44
56
|
|
|
45
57
|
export default {
|
|
46
58
|
fetch: (request, env, ctx) => app.fetch(request, env, ctx),
|
|
47
59
|
};
|
|
48
60
|
```
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
|
|
62
|
+
`wrangler.jsonc`:
|
|
53
63
|
|
|
54
64
|
```jsonc
|
|
55
65
|
{
|
|
56
66
|
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
57
67
|
"name": "my-edge-app",
|
|
58
68
|
"main": "worker.js",
|
|
59
|
-
// Set this to today's date
|
|
60
69
|
"compatibility_date": "2026-09-06",
|
|
61
|
-
"observability": {
|
|
62
|
-
"enabled": true
|
|
63
|
-
}
|
|
70
|
+
"observability": { "enabled": true }
|
|
64
71
|
}
|
|
65
72
|
```
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
Deploy:
|
|
68
75
|
|
|
69
76
|
```bash
|
|
70
77
|
wrangler deploy
|
|
71
78
|
```
|
|
72
79
|
|
|
73
|
-
> `wrangler publish` was removed in favor of `wrangler deploy`. If you're on an older Wrangler version, run `npm install -g wrangler@latest` first.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
80
|
## Server API
|
|
78
81
|
|
|
79
|
-
###
|
|
82
|
+
### Context
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
| Member | Description |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `ctx.req` | Incoming Request |
|
|
87
|
+
| `ctx.env` | Environment bindings |
|
|
88
|
+
| `ctx.executionCtx` | ExecutionContext |
|
|
89
|
+
| `ctx.params` | Prototype-safe route params object |
|
|
90
|
+
| `ctx.status` | Default response status (200) |
|
|
91
|
+
| `ctx.headers` | Response Headers |
|
|
92
|
+
| `ctx.query` | URLSearchParams |
|
|
93
|
+
| `ctx.getCookie(name)` | Read a cookie |
|
|
94
|
+
| `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
|
|
95
|
+
| `ctx.deleteCookie(name, options)` | Delete a cookie |
|
|
96
|
+
| `ctx.text/json/html(data, status?, headers?)` | Response helpers |
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
Every response carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, and a restrictive `Permissions-Policy`.
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
### Route Options
|
|
89
101
|
|
|
90
102
|
```ts
|
|
91
|
-
|
|
92
|
-
app.get('/
|
|
93
|
-
app.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
app.get('/
|
|
97
|
-
|
|
98
|
-
});
|
|
103
|
+
app.get('/protected', { auth: true }, handler);
|
|
104
|
+
app.get('/admin', { auth: { role: 'admin' } }, handler);
|
|
105
|
+
app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
|
|
106
|
+
app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
|
|
107
|
+
app.get('/cached', { cache: { ttl: 60 } }, handler);
|
|
108
|
+
app.get('/api', { cors: true }, handler);
|
|
109
|
+
app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
|
|
99
110
|
```
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
### Context Object
|
|
104
|
-
|
|
105
|
-
Each handler receives a `Context` object (`ctx`) with the following members:
|
|
106
|
-
|
|
107
|
-
| Property / Method | Description |
|
|
108
|
-
| --- | --- |
|
|
109
|
-
| `ctx.req` | The original `Request` object |
|
|
110
|
-
| `ctx.env` | The environment bindings (KV, secrets, etc.) |
|
|
111
|
-
| `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
|
|
112
|
-
| `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
|
|
113
|
-
| `ctx.status` | HTTP status code (default `200`) |
|
|
114
|
-
| `ctx.headers` | `Headers` object for the response |
|
|
115
|
-
| `ctx.query` | `URLSearchParams` (lazy) |
|
|
116
|
-
| `ctx.getCookie(name)` | Get a cookie value |
|
|
117
|
-
| `ctx.setCookie(name, value, options)` | Set a cookie |
|
|
118
|
-
| `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
|
|
119
|
-
| `ctx.text(data, status?, headers?)` | Return plain text |
|
|
120
|
-
| `ctx.json(data, status?, headers?)` | Return JSON |
|
|
121
|
-
| `ctx.html(data, status?, headers?)` | Return HTML |
|
|
112
|
+
**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.
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
### Returning Responses
|
|
126
|
-
|
|
127
|
-
You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
|
|
128
|
-
If the handler returns a JSX element, it will be rendered to HTML automatically (see [JSX Support](#jsx-support)).
|
|
114
|
+
**CORS:** The default is `origin: '*'`. For production, use a strict allowlist:
|
|
129
115
|
|
|
130
116
|
```ts
|
|
131
|
-
app.
|
|
132
|
-
return { hello: 'world' }; // → ctx.json()
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
app.get('/html', (ctx) => {
|
|
136
|
-
return '<h1>Hi</h1>'; // → ctx.html()
|
|
137
|
-
});
|
|
117
|
+
app.defaults.cors.origin = ['https://app.example.com'];
|
|
138
118
|
```
|
|
139
119
|
|
|
140
|
-
|
|
120
|
+
**Rate limit:** 429 responses carry `Retry-After: 60`.
|
|
141
121
|
|
|
142
|
-
|
|
122
|
+
## JSX Support
|
|
143
123
|
|
|
144
|
-
|
|
124
|
+
```tsx
|
|
125
|
+
import { renderToString } from '@lengkapp/edge';
|
|
145
126
|
|
|
146
|
-
|
|
127
|
+
function Card({ title }) {
|
|
128
|
+
return <div class="card"><h2>{title}</h2></div>;
|
|
129
|
+
}
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
app.get('/protected', { auth: true }, handler);
|
|
150
|
-
app.get('/admin', { auth: { role: 'admin' } }, handler);
|
|
151
|
-
app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
|
|
131
|
+
app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
|
|
152
132
|
```
|
|
153
133
|
|
|
154
|
-
|
|
134
|
+
`renderToString` hardening:
|
|
155
135
|
|
|
156
|
-
|
|
136
|
+
- Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
|
|
137
|
+
- Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
|
|
138
|
+
- `on*` attributes never serialize.
|
|
139
|
+
- `href`/`src`/`action`/`formaction`/`xlink:href` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
|
|
140
|
+
- Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
|
|
141
|
+
- All string values are HTML-escaped.
|
|
157
142
|
|
|
158
|
-
|
|
143
|
+
## Client (Declarative Partial Updates)
|
|
159
144
|
|
|
160
|
-
```
|
|
161
|
-
|
|
145
|
+
```html
|
|
146
|
+
<script src="https://cdn.example.com/edge-client.min.js"></script>
|
|
162
147
|
```
|
|
163
148
|
|
|
164
|
-
|
|
149
|
+
### Attributes
|
|
165
150
|
|
|
166
|
-
|
|
151
|
+
| Attribute | Description |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `_get` / `_post` / `_put` / `_patch` / `_delete` | URL + method |
|
|
154
|
+
| `_target` | CSS selector, or `"this"` |
|
|
155
|
+
| `_trigger` | Event list: click, load, visible, intersect, etc. |
|
|
156
|
+
| `_form` | Form ID to serialize as URL-encoded body |
|
|
157
|
+
| `_json` | Comma-separated input names to send as JSON |
|
|
158
|
+
| `_skeleton` | `"false"` disables the loading skeleton |
|
|
159
|
+
| `_retry` | `"false"` disables the retry button |
|
|
167
160
|
|
|
168
|
-
|
|
169
|
-
app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
|
|
170
|
-
```
|
|
161
|
+
### Examples
|
|
171
162
|
|
|
172
|
-
|
|
163
|
+
```html
|
|
164
|
+
<button _get="/more-posts" _target="#posts">Load More</button>
|
|
173
165
|
|
|
174
|
-
|
|
166
|
+
<div _get="/user-profile" _target="this"></div>
|
|
175
167
|
|
|
176
|
-
|
|
177
|
-
app.get('/api', { cors: true }, handler);
|
|
168
|
+
<button _post="/login" _json="username,password" _target="#status">Login</button>
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
|
|
170
|
+
<div _get="/lazy" _target="this" _trigger="visible"></div>
|
|
181
171
|
```
|
|
182
172
|
|
|
183
|
-
|
|
173
|
+
### How Content Is Inserted
|
|
184
174
|
|
|
185
|
-
|
|
175
|
+
1. Response is fetched with `credentials: 'same-origin'` and `X-Requested-With: XMLHttpRequest`.
|
|
176
|
+
2. Content-Type is verified to be `text/html` or `text/plain`.
|
|
177
|
+
3. `res.headers` may contain `x-css-required` / `x-js-required` (with optional `x-css-integrity` / `x-js-integrity` for SRI) — matching resources are injected once.
|
|
178
|
+
4. The HTML is run through `sanitizeHtml`, which removes `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and neutralizes `javascript:` / `vbscript:` / `data:text/html` URLs.
|
|
179
|
+
5. The sanitized HTML is assigned via `innerHTML` (through a Trusted Types policy named `edge-policy` when available).
|
|
180
|
+
6. Each `<script>` in the result is rebuilt in place so the browser executes it. If the page has a CSP nonce, only scripts carrying the matching nonce run; otherwise all scripts run.
|
|
186
181
|
|
|
187
|
-
|
|
182
|
+
## CSRF Protection
|
|
188
183
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
#### Custom Validation (`validate`)
|
|
192
|
-
|
|
193
|
-
A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
|
|
184
|
+
The client reads a token from `<meta name="csrf-token" content="...">` or the `XSRF-TOKEN` cookie and sends it as `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
|
|
194
185
|
|
|
195
186
|
```ts
|
|
196
187
|
app.post('/submit', {
|
|
197
|
-
validate: (ctx) =>
|
|
188
|
+
validate: (ctx) => {
|
|
189
|
+
const sent = ctx.req.headers.get('X-CSRF-Token');
|
|
190
|
+
return sent && sent === ctx.getCookie('csrf_token');
|
|
191
|
+
}
|
|
198
192
|
}, handler);
|
|
199
193
|
```
|
|
200
194
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
## JSX Support
|
|
195
|
+
## Trusted Types / CSP
|
|
204
196
|
|
|
205
|
-
|
|
197
|
+
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:
|
|
206
198
|
|
|
207
|
-
```
|
|
208
|
-
|
|
199
|
+
```text
|
|
200
|
+
Content-Security-Policy:
|
|
201
|
+
default-src 'self';
|
|
202
|
+
script-src 'self';
|
|
203
|
+
style-src 'self';
|
|
204
|
+
img-src 'self' data:;
|
|
205
|
+
require-trusted-types-for 'script';
|
|
206
|
+
trusted-types edge-policy;
|
|
207
|
+
frame-ancestors 'none';
|
|
208
|
+
base-uri 'self';
|
|
209
|
+
```
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<div class="card">
|
|
213
|
-
<h2>{title}</h2>
|
|
214
|
-
</div>
|
|
215
|
-
);
|
|
216
|
-
}
|
|
211
|
+
## Security Controls
|
|
217
212
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
```
|
|
213
|
+
```ts
|
|
214
|
+
const app = new Edge();
|
|
222
215
|
|
|
223
|
-
|
|
216
|
+
app.defaults.cors.origin = ['https://app.example.com'];
|
|
224
217
|
|
|
225
|
-
|
|
226
|
-
app.
|
|
218
|
+
app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
|
|
219
|
+
app.security.logSecurityEvents = true; // structured JSON events (default on)
|
|
220
|
+
app.security.extraHeaders = {
|
|
221
|
+
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
|
|
222
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
223
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
224
|
+
};
|
|
227
225
|
```
|
|
228
226
|
|
|
229
|
-
|
|
227
|
+
### OWASP 2025 Coverage
|
|
228
|
+
|
|
229
|
+
| Category | Mitigation |
|
|
230
|
+
|---|---|
|
|
231
|
+
| A01 Broken Access Control | Auth reasons, expiry enforcement, scope/role checks, strict target resolution |
|
|
232
|
+
| A02 Security Misconfiguration | Safe default headers, strict CORS allowlist, `Vary: Origin` |
|
|
233
|
+
| A03 Supply Chain | Zero deps, SRI honored on injected resources |
|
|
234
|
+
| A04 Crypto Failures | Optional SHA-256 token hashing |
|
|
235
|
+
| A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization, Trusted Types policy, response sanitizer |
|
|
236
|
+
| A06 Insecure Design | Numeric-sanitized rate limiter, `Retry-After` |
|
|
237
|
+
| A07 Auth Failures | Structured failure reasons, no swallowed errors |
|
|
238
|
+
| A08 Data Integrity | Prototype-safe JSON, `Set-Cookie` stripped from cache |
|
|
239
|
+
| A09 Logging | Structured JSON security events with 60s dedupe |
|
|
240
|
+
| A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
|
|
230
241
|
|
|
231
242
|
## Scheduled Tasks
|
|
232
243
|
|
|
233
|
-
Register a scheduled handler for Cron triggers.
|
|
234
|
-
|
|
235
244
|
```ts
|
|
236
245
|
app.scheduled(async (event, env, ctx) => {
|
|
237
246
|
console.log('Cron executed:', event.cron);
|
|
238
247
|
});
|
|
239
|
-
```
|
|
240
248
|
|
|
241
|
-
Then export it in your worker:
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
249
|
export default {
|
|
245
250
|
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
246
|
-
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
251
|
+
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx),
|
|
247
252
|
};
|
|
248
253
|
```
|
|
249
254
|
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## Client (Declarative Partial Updates)
|
|
253
|
-
|
|
254
|
-
Add the client script to your page (or include it via a CDN):
|
|
255
|
-
|
|
256
|
-
```html
|
|
257
|
-
<script src="https://cdn.example.com/edge-client.min.js"></script>
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Then use HTML attributes to make elements fetch content asynchronously.
|
|
261
|
-
|
|
262
|
-
### Attributes
|
|
263
|
-
|
|
264
|
-
| Attribute | Description |
|
|
265
|
-
| --- | --- |
|
|
266
|
-
| `_get` | URL to fetch via GET |
|
|
267
|
-
| `_post` | URL to fetch via POST |
|
|
268
|
-
| `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
|
|
269
|
-
| `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
|
|
270
|
-
| `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
|
|
271
|
-
| `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
|
|
272
|
-
|
|
273
|
-
### Examples
|
|
274
|
-
|
|
275
|
-
**Load content on click:**
|
|
276
|
-
|
|
277
|
-
```html
|
|
278
|
-
<button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
|
|
279
|
-
<div id="posts"></div>
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**Auto-load on page load:**
|
|
283
|
-
|
|
284
|
-
```html
|
|
285
|
-
<div _get="/user-profile" _target="this"></div>
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
**Post a form via AJAX:**
|
|
289
|
-
|
|
290
|
-
```html
|
|
291
|
-
<form id="contact-form">
|
|
292
|
-
<input name="email" />
|
|
293
|
-
<button _post="/submit" _form="contact-form" _target="#result">Submit</button>
|
|
294
|
-
</form>
|
|
295
|
-
<div id="result"></div>
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
**Send JSON data:**
|
|
299
|
-
|
|
300
|
-
```html
|
|
301
|
-
<input name="username" />
|
|
302
|
-
<input name="password" />
|
|
303
|
-
<button _post="/login" _json="username,password" _target="#status">Login</button>
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
**Load when element becomes visible (IntersectionObserver):**
|
|
307
|
-
|
|
308
|
-
```html
|
|
309
|
-
<div _get="/lazy-content" _target="this" _trigger="visible"></div>
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Skeleton Loading
|
|
313
|
-
|
|
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.
|
|
315
|
-
|
|
316
|
-
### Resource Injection
|
|
317
|
-
|
|
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.
|
|
319
|
-
|
|
320
|
-
Example server route:
|
|
321
|
-
|
|
322
|
-
```ts
|
|
323
|
-
app.get('/widget', (ctx) => {
|
|
324
|
-
ctx.headers.set('x-css-required', '["/widget.css"]');
|
|
325
|
-
ctx.headers.set('x-js-required', '["/widget.js"]');
|
|
326
|
-
return ctx.html('<div class="widget">...</div>');
|
|
327
|
-
});
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
---
|
|
331
|
-
|
|
332
255
|
## Configuration
|
|
333
256
|
|
|
334
|
-
###
|
|
335
|
-
|
|
336
|
-
If you use authentication or rate limiting, you need KV namespaces. Create them first:
|
|
257
|
+
### KV Namespaces
|
|
337
258
|
|
|
338
259
|
```bash
|
|
339
260
|
wrangler kv namespace create AUTH_KV
|
|
340
261
|
wrangler kv namespace create RATE_LIMIT_KV
|
|
341
262
|
```
|
|
342
263
|
|
|
343
|
-
Then wire up the returned IDs in your config:
|
|
344
|
-
|
|
345
264
|
```jsonc
|
|
346
265
|
{
|
|
347
|
-
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
348
|
-
"name": "my-edge-app",
|
|
349
|
-
"main": "worker.js",
|
|
350
|
-
// Set this to today's date
|
|
351
|
-
"compatibility_date": "2026-09-06",
|
|
352
|
-
"observability": {
|
|
353
|
-
"enabled": true
|
|
354
|
-
},
|
|
355
266
|
"kv_namespaces": [
|
|
356
|
-
{ "binding": "AUTH_KV", "id": "
|
|
357
|
-
{ "binding": "RATE_LIMIT_KV", "id": "
|
|
358
|
-
]
|
|
359
|
-
// Optional Cron triggers
|
|
360
|
-
"triggers": {
|
|
361
|
-
"crons": ["*/5 * * * *"]
|
|
362
|
-
}
|
|
267
|
+
{ "binding": "AUTH_KV", "id": "..." },
|
|
268
|
+
{ "binding": "RATE_LIMIT_KV", "id": "..." }
|
|
269
|
+
]
|
|
363
270
|
}
|
|
364
271
|
```
|
|
365
272
|
|
|
366
|
-
|
|
367
|
-
<summary>Equivalent <code>wrangler.toml</code></summary>
|
|
368
|
-
|
|
369
|
-
```toml
|
|
370
|
-
name = "my-edge-app"
|
|
371
|
-
main = "worker.js"
|
|
372
|
-
compatibility_date = "2026-09-06"
|
|
373
|
-
|
|
374
|
-
[observability]
|
|
375
|
-
enabled = true
|
|
376
|
-
|
|
377
|
-
[[kv_namespaces]]
|
|
378
|
-
binding = "AUTH_KV"
|
|
379
|
-
id = "your-auth-kv-id"
|
|
380
|
-
|
|
381
|
-
[[kv_namespaces]]
|
|
382
|
-
binding = "RATE_LIMIT_KV"
|
|
383
|
-
id = "your-ratelimit-kv-id"
|
|
384
|
-
|
|
385
|
-
# Optional Cron triggers
|
|
386
|
-
[triggers]
|
|
387
|
-
crons = ["*/5 * * * *"]
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
</details>
|
|
391
|
-
|
|
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/).
|
|
393
|
-
|
|
394
|
-
### Environment Variables
|
|
395
|
-
|
|
396
|
-
The KV binding names are configurable on the Edge instance:
|
|
273
|
+
Rename the bindings on the instance:
|
|
397
274
|
|
|
398
275
|
```ts
|
|
399
276
|
app.authKvBinding = 'CUSTOM_AUTH_KV';
|
|
400
277
|
app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
|
|
401
278
|
```
|
|
402
279
|
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
## Full Example
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
import { Edge, renderToString } from '@lengkapp/edge';
|
|
409
|
-
import { HomePage } from './Page.jsx';
|
|
410
|
-
|
|
411
|
-
const app = new Edge();
|
|
412
|
-
|
|
413
|
-
// Basic text
|
|
414
|
-
app.get('/', (ctx) => ctx.text('Hello from Edge!'));
|
|
415
|
-
|
|
416
|
-
// JSX component
|
|
417
|
-
app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
|
|
418
|
-
|
|
419
|
-
// JSON with route params
|
|
420
|
-
app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
|
|
421
|
-
|
|
422
|
-
// POST JSON
|
|
423
|
-
app.post('/users', async (ctx) => {
|
|
424
|
-
const body = await ctx.req.json();
|
|
425
|
-
return ctx.json({ created: true, user: body }, 201);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
// Auth
|
|
429
|
-
app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
|
|
430
|
-
|
|
431
|
-
// Rate limiting
|
|
432
|
-
app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
|
|
433
|
-
|
|
434
|
-
// Caching
|
|
435
|
-
app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
|
|
436
|
-
|
|
437
|
-
// Cookies
|
|
438
|
-
app.get('/set-cookie', (ctx) => {
|
|
439
|
-
ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
|
|
440
|
-
return ctx.text('Cookie set');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
app.get('/get-cookie', (ctx) => {
|
|
444
|
-
const session = ctx.getCookie('session');
|
|
445
|
-
return ctx.text(`Cookie: ${session}`);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Scheduled
|
|
449
|
-
app.scheduled(async (event, env, ctx) => {
|
|
450
|
-
console.log('Cron:', event.cron);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
export default {
|
|
454
|
-
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
455
|
-
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
456
|
-
};
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
280
|
## Performance
|
|
462
281
|
|
|
463
|
-
|
|
282
|
+
Zero runtime dependencies. On a local `wrangler dev` benchmark with `autocannon` (10 connections, 10s per route), throughput sits in the same tier as `hono` / `hono/tiny` / a native worker:
|
|
464
283
|
|
|
465
|
-
|
|
284
|
+
| Framework | /text req/s | /json req/s |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| @lengkapp/edge | 503 | 500 |
|
|
287
|
+
| Hono | 506 | 495 |
|
|
288
|
+
| Hono (tiny) | 505 | 498 |
|
|
289
|
+
| Native Workers | 498 | 500 |
|
|
466
290
|
|
|
467
|
-
-
|
|
468
|
-
- **Target:** local `wrangler dev` server
|
|
469
|
-
- **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
|
|
470
|
-
- **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
|
|
291
|
+
The security hardening adds only cheap operations to the request path: prototype-safe objects, two regex checks per JSX attribute, a sanitizer template parse per response (browser-native parser), and a throttled logger. No new async boundaries, no added dependencies, no hashing on the hot path.
|
|
471
292
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
### Results
|
|
475
|
-
|
|
476
|
-
| 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 |
|
|
481
|
-
| Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
|
|
482
|
-
| Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
|
|
483
|
-
| Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
|
|
484
|
-
| Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
|
|
485
|
-
| Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
|
|
486
|
-
|
|
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
|
-
### Takeaways
|
|
293
|
+
## License
|
|
504
294
|
|
|
505
|
-
|
|
506
|
-
- 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.
|
|
508
|
-
- @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.
|
|
295
|
+
See LICENSE. © LengkApp — Yasir Haris
|
|
509
296
|
|
|
510
297
|
---
|
|
511
298
|
|
|
512
|
-
##
|
|
513
|
-
|
|
514
|
-
|
|
299
|
+
## Summary of the security posture
|
|
300
|
+
|
|
301
|
+
| Layer | Mechanism |
|
|
302
|
+
|---|---|
|
|
303
|
+
| HTML insertion | `sanitizeHtml` → `innerHTML` via Trusted Types policy `edge-policy` |
|
|
304
|
+
| Dangerous elements | `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, `form[action^="javascript:"]` removed |
|
|
305
|
+
| Dangerous URLs | `javascript:`, `vbscript:`, `data:text/html` removed from `href`/`src`/`action`/`formaction`/`xlink:href`/`data` |
|
|
306
|
+
| Script execution | Nonce-gated when the page uses CSP nonces; deduplicated by `src`; inline scripts block-wrapped |
|
|
307
|
+
| CSRF | `X-CSRF-Token` from meta or cookie on state-changing methods |
|
|
308
|
+
| Cookies | `credentials: 'same-origin'`; names validated on the server |
|
|
309
|
+
| Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
|
|
310
|
+
| Server middleware | Fail-closed; structured security logs; strict CORS allowlist; optional token hashing |
|
|
311
|
+
| Prototype pollution | `Object.create(null)` + forbidden-key filtering in params, cookies, JSON, JSX |
|
|
312
|
+
| Build | Reserved exports/properties, `keep_quoted: "strict"`, per-bundle post-minification self-test |
|