@lengkapp/edge 0.0.18 → 0.0.20

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.18
2
+ * @lengkapp/edge v0.0.20
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.18
2
+ * @lengkapp/edge v0.0.20
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -30,112 +30,150 @@
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
- /// <reference types="@cloudflare/workers-types" />
33
+ // edge-server.d.ts
34
34
 
35
- declare class Context {
35
+ export function Fragment(props: { children?: any }): JSXNode;
36
+
37
+ export interface JSXNode {
38
+ type: unknown;
39
+ props: Record<string, any>;
40
+ children: any[];
41
+ __isJSX: true;
42
+ }
43
+
44
+ export function jsx(type: unknown, props: any, ...children: any[]): JSXNode;
45
+ export function renderToString(node: any): string;
46
+
47
+ /* ---------------- Context ---------------- */
48
+
49
+ export interface CookieOptions {
50
+ path?: string;
51
+ domain?: string;
52
+ maxAge?: number;
53
+ expires?: Date;
54
+ secure?: boolean;
55
+ httpOnly?: boolean;
56
+ sameSite?: 'Strict' | 'Lax' | 'None';
57
+ }
58
+
59
+ export interface RouteOptions {
60
+ auth?: boolean | {
61
+ role?: string;
62
+ scopes?: string[];
63
+ };
64
+ rateLimit?: boolean | {
65
+ /** max requests per window (default 100) */
66
+ max?: number;
67
+ /** window in seconds (default 60) */
68
+ window?: number;
69
+ };
70
+ cors?: boolean | {
71
+ origin?: string | string[];
72
+ methods?: string;
73
+ headers?: string;
74
+ };
75
+ cache?: boolean | {
76
+ /** seconds (default 3600) */
77
+ ttl?: number;
78
+ staleWhileRevalidate?: number;
79
+ };
80
+ compress?: boolean;
81
+ log?: boolean;
82
+ validate?: (ctx: Context) => boolean | Promise<boolean>;
83
+ }
84
+
85
+ export type Handler<E = any> = (ctx: Context<E>) => any | Promise<any>;
86
+
87
+ export class Context<E = any> {
36
88
  constructor(
37
89
  request: Request,
38
- env: any,
90
+ env: E,
39
91
  executionCtx: ExecutionContext,
40
92
  params?: Record<string, string>,
41
- parsedUrl?: URL | null
93
+ parsedUrl?: URL | null,
42
94
  );
43
95
 
44
96
  req: Request;
45
- env: any;
97
+ env: E;
46
98
  executionCtx: ExecutionContext;
47
99
  params: Record<string, string>;
48
100
  status: number;
49
101
  headers: Headers;
50
102
  url: URL;
51
103
 
52
- private _rawCookie: string;
53
- private _cookies: Record<string, string> | null;
104
+ readonly query: URLSearchParams;
54
105
 
55
106
  getCookie(name: string): string | null;
56
- readonly query: URLSearchParams;
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;
107
+ setCookie(name: string, value: string, options?: CookieOptions): void;
108
+ deleteCookie(name: string, options?: CookieOptions): void;
65
109
 
66
110
  text(data: string, status?: number, headers?: Record<string, string>): Response;
67
111
  json(data: any, status?: number, headers?: Record<string, string>): Response;
68
- html(data: string, status?: number, headers?: Record<string, string>): Response;
69
-
70
- private _buildHeaders(headers: Record<string, string>): Headers;
71
- }
72
112
 
73
- export const Fragment: unique symbol;
74
-
75
- export interface JSXElement {
76
- type: any;
77
- props: Record<string, any>;
78
- children: any[];
79
- __isJSX: true;
113
+ /** Accepts a raw HTML string, JSX, or an array of JSX. */
114
+ html(
115
+ data: string | JSXNode | JSXNode[] | null | undefined,
116
+ status?: number,
117
+ headers?: Record<string, string>,
118
+ ): Response;
80
119
  }
81
120
 
82
- export function jsx(type: any, props: Record<string, any> | null, ...children: any[]): JSXElement;
83
- export function renderToString(node: any): string;
84
-
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
+ /* ---------------- Edge ---------------- */
89
122
 
90
- interface RouteOptions {
91
- auth?: boolean | AuthOptions;
92
- rateLimit?: boolean | RateLimitOptions;
93
- cors?: boolean | CorsOptions;
94
- validate?: (ctx: Context) => boolean | Promise<boolean>;
95
- cache?: boolean | CacheOptions;
96
- compress?: boolean;
97
- log?: boolean;
123
+ export interface SecurityConfig {
124
+ hashAuthTokens: boolean;
125
+ logSecurityEvents: boolean;
126
+ extraHeaders: Record<string, string> | null;
127
+ trustedProxies: string[] | null;
98
128
  }
99
129
 
100
- interface SecurityOptions {
101
- hashAuthTokens?: boolean;
102
- logSecurityEvents?: boolean;
103
- extraHeaders?: Record<string, string> | null;
104
- trustedProxies?: string[] | null;
130
+ export interface Defaults {
131
+ cors: { origin: string; methods: string };
105
132
  }
106
133
 
107
- type RouteHandler = (ctx: Context) => Response | JSXElement | void | Promise<Response | JSXElement | void>;
108
- type ScheduledHandler = (controller: ScheduledController, env: any, ctx: ExecutionContext) => void | Promise<void>;
109
-
110
- export class Edge {
134
+ export class Edge<E = any> {
111
135
  constructor();
136
+
112
137
  authKvBinding: string;
113
138
  rateLimitKvBinding: string;
114
- defaults: { cors: CorsOptions };
115
- scheduledHandler: ScheduledHandler | null;
116
- security: Required<SecurityOptions>;
117
-
118
- get(path: string, handler: RouteHandler): void;
119
- get(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
120
- post(path: string, handler: RouteHandler): void;
121
- post(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
122
- put(path: string, handler: RouteHandler): void;
123
- put(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
124
- delete(path: string, handler: RouteHandler): void;
125
- delete(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
126
- patch(path: string, handler: RouteHandler): void;
127
- patch(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
128
- options(path: string, handler: RouteHandler): void;
129
- options(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
130
- head(path: string, handler: RouteHandler): void;
131
- head(path: string, options: boolean | RouteOptions, handler: RouteHandler): void;
132
- scheduled(handler: ScheduledHandler): void;
133
- fetch(request: Request, env: any, executionCtx: ExecutionContext): Promise<Response>;
139
+ defaults: Defaults;
140
+ security: SecurityConfig;
141
+
142
+ get(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
143
+ get(path: string, handler: Handler<E>): void;
144
+
145
+ post(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
146
+ post(path: string, handler: Handler<E>): void;
147
+
148
+ put(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
149
+ put(path: string, handler: Handler<E>): void;
150
+
151
+ delete(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
152
+ delete(path: string, handler: Handler<E>): void;
153
+
154
+ patch(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
155
+ patch(path: string, handler: Handler<E>): void;
156
+
157
+ options(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
158
+ options(path: string, handler: Handler<E>): void;
159
+
160
+ head(path: string, options?: RouteOptions | boolean, handler?: Handler<E>): void;
161
+ head(path: string, handler: Handler<E>): void;
162
+
163
+ scheduled(
164
+ handler: (event: ScheduledEvent, env: E, ctx: ExecutionContext) => any | Promise<any>,
165
+ ): void;
166
+
167
+ fetch(request: Request, env: E, executionCtx: ExecutionContext): Promise<Response>;
134
168
  }
135
169
 
170
+ /* ---------------- JSX typing for this runtime ---------------- */
171
+
136
172
  declare global {
137
173
  namespace JSX {
138
- interface Element extends JSXElement {}
174
+ type Element = JSXNode;
175
+ type ElementType = string | symbol | ((props: any) => JSXNode);
139
176
  interface IntrinsicElements { [elemName: string]: any; }
177
+ interface ElementChildrenAttribute { children: {}; }
140
178
  }
141
179
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.18
2
+ * @lengkapp/edge v0.0.20
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})},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={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},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}};
33
+ var t=Object.defineProperty,e=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,o=(e,n)=>{for(var r in n)t(e,r,{get:n[r],enumerable:!0})},i=(o,i,s,a)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let c of n(i))r.call(o,c)||c===s||t(o,c,{get:()=>i[c],enumerable:!(a=e(i,c))||a.enumerable});return o},s=e=>i(t({},"__esModule",{value:!0}),e),a={};function c(t){const e=[],n=t.length;let r=1;for(let o=1;o<=n;o++)o!==n&&47!==t.charCodeAt(o)||(o>r&&e.push(t.slice(r,o)),r=o+1);return e}function l(t){const e=t.length;return e>1&&47===t.charCodeAt(e-1)?t.slice(0,-1):t}o(a,{Context:()=>T,Edge:()=>Edge,Fragment:()=>Fragment,jsx:()=>jsx,renderToString:()=>renderToString}),module.exports=s(a);var u=new Set(["__proto__","constructor","prototype"]);function f(t){return"string"==typeof t&&!u.has(t)}function h(t){const e=JSON.parse(t);if(e&&"object"==typeof e)for(const t of Object.keys(e))if(!f(t))throw new SyntaxError("Forbidden key in JSON payload");return e}function p(t){const e=t.length;let n=0;for(;n<e;){const e=t.charCodeAt(n);if(38===e||60===e||62===e||34===e||39===e)break;n++}if(n===e)return t;let r=t.slice(0,n),o=n;for(;n<e;n++){const e=t.charCodeAt(n);let i;if(38===e)i="&amp;";else if(60===e)i="&lt;";else if(62===e)i="&gt;";else if(34===e)i="&quot;";else{if(39!==e)continue;i="&#039;"}n>o&&(r+=t.slice(o,n)),r+=i,o=n+1}return o<e&&(r+=t.slice(o)),r}var Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n,__isJSX:!0}}var d=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),y=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]),m=new Set(["animation-iteration-count","border-image-outset","border-image-slice","border-image-width","box-flex","box-flex-group","box-ordinal-group","column-count","columns","flex","flex-grow","flex-positive","flex-shrink","flex-negative","flex-order","grid-row","grid-row-end","grid-row-span","grid-row-start","grid-column","grid-column-end","grid-column-span","grid-column-start","font-weight","line-clamp","line-height","opacity","order","orphans","tab-size","widows","z-index","zoom","fill-opacity","flood-opacity","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width"]),g=new Map,w=2e3;function b(t){const e=g.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return g.size>=w&&g.clear(),g.set(t,n),n}function A(t){if(!t||"object"!=typeof t)return"";let e="";for(const n in t){if(!Object.prototype.hasOwnProperty.call(t,n))continue;const r=b(n);let o=t[n];"number"!=typeof o||m.has(r)||(o+="px"),e&&(e+=";"),e+=r+":"+o}return e}var C=new Set(["href","src","xlink:href","action","formaction"]);function renderToString(t){if(null==t||"boolean"==typeof t)return"";const e=typeof t;if("string"===e)return p(t);if("number"===e)return p(String(t));if(Array.isArray(t)){let e="";for(let n=0;n<t.length;n++)e+=renderToString(t[n]);return e}if(!t.__isJSX)return p(String(t));const{type:n,props:r,children:o}=t;if(n===Fragment){let t="";for(let e=0;e<o.length;e++)t+=renderToString(o[e]);return t}if("symbol"==typeof n)return"";if("function"==typeof n)return renderToString(n({...r,children:o}));if("string"!=typeof n||!/^[A-Za-z][A-Za-z0-9-]*$/.test(n))return"";const i=d.has(n);let s="";for(const t in r){if(!Object.prototype.hasOwnProperty.call(r,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=r[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let n=t;if("className"===t||"class"===t?n="class":"htmlFor"!==t&&"for"!==t||(n="for"),f(n)&&!/^on/i.test(n)&&/^[A-Za-z_:][A-Za-z0-9_:.-]*$/.test(n)){if(C.has(n)){const t=String(e).replace(/[\u0000-\u0020]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("vbscript:")||t.startsWith("data:text/html"))continue}if(y.has(n))!0===e&&(s+=" "+n);else if(!0!==e){if("style"===n&&"object"==typeof e){const t=A(e);t&&(s+=' style="'+p(t)+'"');continue}s+=" "+n+'="'+p(String(e))+'"'}else s+=" "+n+'="true"'}}let a="";const c=r.dangerouslySetInnerHTML;if(c&&null!=c.__html)a=c.__html;else for(let t=0;t<o.length;t++)a+=renderToString(o[t]);return i?"<"+n+s+"/>":"<"+n+s+">"+a+"</"+n+">"}var T=class{constructor(t,e,n,r={},o=null){this.req=t,this.env=e,this.executionCtx=n,this.params=r,this.status=200,this.headers=new Headers,this.headers.set("X-Content-Type-Options","nosniff"),this.headers.set("X-Frame-Options","DENY"),this.headers.set("Referrer-Policy","strict-origin-when-cross-origin"),this.headers.set("Permissions-Policy","geolocation=(), microphone=(), camera=()"),this.t=t.headers.get("Cookie")||"",this.o=null,this.url=o||new URL(t.url)}i(){if(null!==this.o)return this.o;const t=Object.create(null),e=this.t;if(e){let n=0;const r=e.length;for(;n<r;){let o=e.indexOf(";",n);-1===o&&(o=r);let i=n,s=o;for(;i<s&&32===e.charCodeAt(i);)i++;for(;s>i&&32===e.charCodeAt(s-1);)s--;if(s>i){const n=e.indexOf("=",i);if(n>i&&n<s)try{const r=decodeURIComponent(e.slice(i,n));f(r)&&(t[r]=decodeURIComponent(e.slice(n+1,s)))}catch{}}n=o+1}}return this.o=t,t}getCookie(t){const e=this.i()[t];return void 0===e?null:e}get query(){return this.url.searchParams}setCookie(t,e,n={}){if("string"!=typeof t||!f(t)||/[\r\n;=]/.test(t))return;let r=encodeURIComponent(t)+"="+encodeURIComponent(e);n.path&&(r+="; Path="+n.path),n.domain&&(r+="; Domain="+n.domain),void 0!==n.maxAge&&(r+="; Max-Age="+n.maxAge),n.expires&&(r+="; Expires="+n.expires.toUTCString()),n.secure&&(r+="; Secure"),n.httpOnly&&(r+="; HttpOnly"),n.sameSite&&(r+="; SameSite="+n.sameSite),this.headers.append("Set-Cookie",r)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}l(t){if(!t)return this.headers;let e=!1;for(const n in t){e=!0;break}if(!e)return this.headers;const n=new Headers(this.headers);for(const e in t)Object.prototype.hasOwnProperty.call(t,e)&&n.set(e,t[e]);return n}text(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","text/plain; charset=utf-8"),new Response(t,{status:e,headers:r})}json(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","application/json; charset=utf-8"),new Response(JSON.stringify(t),{status:e,headers:r})}html(t,e=this.status,n={}){const r=this.l(n);r.has("Content-Type")||r.set("Content-Type","text/html; charset=utf-8");const o="string"==typeof t?t:t&&t.__isJSX||Array.isArray(t)?renderToString(t):null==t?"":String(t);return new Response(o,{status:e,headers:r})}},_=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},k=class{constructor(){this.root=new _}add(t,e,n){let r=this.root;const o=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0)){const t=n.slice(1);o.push(f(t)?t:"_p"+e),r.paramChild||(r.paramChild=new _),r=r.paramChild}else{r.children||(r.children=new Map);let t=r.children.get(n);t||(t=new _,r.children.set(n,t)),r=t}}r.handler={handler:e,options:n,paramNames:o}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:r,options:o,paramNames:i}=n,s=Object.create(null);for(let t=0;t<i.length;t++)s[i[t]]=e[t];return{handler:r,options:o,params:s}}u(t,e,n,r){if(n===e.length)return t.handler;const o=e[n],i=t.children;if(i){const t=i.get(o);if(t){const o=this.u(t,e,n+1,r);if(o)return o}}const s=t.paramChild;if(s){r.push(o);const t=this.u(s,e,n+1,r);if(t)return t;r.pop()}return null}},O=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],S=new Map;function E(t,e,n){const r=t+"|"+(e||"")+"|"+(n||""),o=Date.now(),i=S.get(r);if(i&&o-i<6e4)return!1;if(S.set(r,o),S.size>5e3)for(const[t,e]of S)o-e>6e4&&S.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(let t=0;t<O.length;t++)this.staticRoutes.set(O[t],new Map);this.dynamicTries={};for(let t=0;t<O.length;t++)this.dynamicTries[O[t]]=new k;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null,this.security={hashAuthTokens:!1,logSecurityEvents:!0,extraHeaders:null,trustedProxies:null}}h(t,e,n,r){if("boolean"==typeof n?n={auth:n}:"function"==typeof n&&(r=n,n={}),"function"!=typeof r)throw new Error("Handler for "+t+" "+e+" must be a function");const o={...this.defaults,...n||{}};-1===e.indexOf(":")?this.staticRoutes.get(t).set(l(e),{handler:r,options:o}):this.dynamicTries[t].add(c(e),r,o)}get(t,e,n){this.h("GET",t,e,n)}post(t,e,n){this.h("POST",t,e,n)}put(t,e,n){this.h("PUT",t,e,n)}delete(t,e,n){this.h("DELETE",t,e,n)}patch(t,e,n){this.h("PATCH",t,e,n)}options(t,e,n){this.h("OPTIONS",t,e,n)}head(t,e,n){this.h("HEAD",t,e,n)}scheduled(t){this.scheduledHandler=t}p(t,e,n){if(this.security.logSecurityEvents)try{const r=t.req.headers.get("CF-Connecting-IP")||"",o=t.url?t.url.pathname:"";if(!E(e,r,o))return;console.log(JSON.stringify({ts:(new Date).toISOString(),kind:"security",event:e,detail:"string"==typeof n?n:void 0,ip:r||void 0,method:t.req.method,path:o,ua:t.req.headers.get("User-Agent")||void 0}))}catch{}}async m(t,e){if(!e)return{ok:!0};const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return{ok:!1,reason:"missing_token"};const r=t.env[this.authKvBinding];if(!r)return{ok:!1,reason:"kv_unavailable"};let o,i=n;if(this.security.hashAuthTokens&&globalThis.crypto&&crypto.subtle){const t=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n)),e=new Uint8Array(t);let r="";for(let t=0;t<e.length;t++)r+=e[t].toString(16).padStart(2,"0");i="tok:"+r}try{o=await r.get(i)}catch{return{ok:!1,reason:"kv_error"}}if(!o)return{ok:!1,reason:"invalid_token"};if("object"==typeof e&&null!==e){let t;try{t=h(o)}catch{return{ok:!1,reason:"malformed_payload"}}if(t&&"object"==typeof t){if(null!=t.exp&&"number"==typeof t.exp&&t.exp<Date.now()/1e3)return{ok:!1,reason:"expired_token"};if(e.role&&t.role!==e.role)return{ok:!1,reason:"insufficient_role"};if(e.scopes){const n=Array.isArray(t.scopes)?t.scopes:[];for(let t=0;t<e.scopes.length;t++)if(-1===n.indexOf(e.scopes[t]))return{ok:!1,reason:"insufficient_scopes"}}}}return{ok:!0,payload:o}}async A(t,e){if(!e)return!0;const n=!0===e?{}:e,r=n.max??100,o=n.window??60,i="rl:"+(t.req.headers.get("CF-Connecting-IP")||"unknown"),s=t.env[this.rateLimitKvBinding];if(!s)return!0;let a;try{a=await s.get(i,"json")}catch{return!0}if(("number"!=typeof a||!isFinite(a)||a<0)&&(a=0),a>=r)return!1;a++;try{await s.put(i,JSON.stringify(a),{expirationTtl:o})}catch{}return!0}C(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors,r=t.req.headers.get("Origin"),o=n.origin;"*"===o?t.headers.set("Access-Control-Allow-Origin","*"):Array.isArray(o)?r&&-1!==o.indexOf(r)&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")):"string"==typeof o&&r===o&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")),t.headers.set("Access-Control-Allow-Methods",n.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"),t.headers.set("Access-Control-Allow-Headers",n.headers||"Content-Type, Authorization, X-CSRF-Token"),t.headers.set("Access-Control-Max-Age","86400")}T(t,e){e&&console.log(t.req.method+" "+t.req.url+" - "+t.status)}_(t,e){const n=t.headers.get("Accept-Encoding")||"";let r=null;if(-1!==n.indexOf("gzip")?r="gzip":-1!==n.indexOf("deflate")&&(r="deflate"),!r||!e.body)return e;const o=e.body.pipeThrough(new CompressionStream(r)),i=new Headers(e.headers);return i.set("Content-Encoding",r),i.set("Vary","Accept-Encoding"),i.delete("Content-Length"),new Response(o,{status:e.status,statusText:e.statusText,headers:i})}async k(t,e){return!e||"function"!=typeof e||!!await e(t)}async O(t){return await caches.default.match(t)||null}async S(t,e,n){if(!n)return;const r=!0===n?{}:n,o=r.ttl??3600,i=r.staleWhileRevalidate??0,s=e.clone();s.headers.set("Cache-Control","max-age="+o+(i>0?", stale-while-revalidate="+i:"")),s.headers.delete("Set-Cookie"),await caches.default.put(t,s)}async fetch(t,e,n){const r=new URL(t.url),o=l(r.pathname),i=t.method,s=this.staticRoutes.get(i);if(s){const i=s.get(o);if(i)return this.v(i.handler,i.options,t,e,n,Object.create(null),r)}const a=this.dynamicTries[i];if(a){const i=a.match(c(o));if(i)return this.v(i.handler,i.options,t,e,n,i.params,r)}return new Response("Not Found",{status:404})}async v(t,e,n,r,o,i,s){const a=new T(n,r,o,i,s),c=this.security.extraHeaders;if(c)for(const t in c)Object.prototype.hasOwnProperty.call(c,t)&&a.headers.set(t,c[t]);let l,u=!1;try{u=await this.k(a,e.validate)}catch(t){this.p(a,"validation_error",t&&t.message),u=!1}if(!u)return a.text("Validation failed",400);try{l=await this.m(a,e.auth)}catch(t){this.p(a,"auth_error",t&&t.message),l={ok:!1,reason:"auth_error"}}if(!("boolean"==typeof l?l:l&&l.ok))return this.p(a,"auth_failure",l&&l.reason||"denied"),a.text("Unauthorized",401);let f,h=!1;try{h=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),h=!1}if(!h)return this.p(a,"rate_limit_exceeded",null),a.text("Too Many Requests",429,{"Retry-After":"60"});if(e.cache&&"GET"===n.method){const t=await this.O(n);if(t)return this.C(a,e.cors),this.T(a,e.log),t}this.C(a,e.cors);try{const e=await t(a);f=e instanceof Response?e:e&&e.__isJSX?a.html(e):a.text("OK"),a.status=f.status}catch(t){this.p(a,"handler_error",t&&t.message),f=a.text("Internal Server Error",500),a.status=500}return e.cache&&"GET"===n.method&&200===f.status&&a.executionCtx.waitUntil(this.S(n,f.clone(),e.cache)),e.compress&&(f=this._(n,f)),this.T(a,e.log),f}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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,7 +4,7 @@ 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**.
6
6
 
7
- > **Security:** Server and client are hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls. The client sanitizes every response, works under `require-trusted-types-for 'script'` CSP, is nonce-aware for script execution, and sends CSRF tokens on state-changing requests. See [Security](#security).
7
+ > **Security:** Server and client are hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls. The client sanitizes every response, works under `require-trusted-types-for 'script'` CSP, is nonce-aware for script execution, and sends CSRF tokens on state-changing requests. See [Security](#security-controls).
8
8
 
9
9
  ---
10
10
 
@@ -12,7 +12,7 @@ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of
12
12
 
13
13
  ### Server
14
14
  - **Trie-based routing** – static & dynamic routes (`/users/:id`).
15
- - **JSX support** – render JSX to HTML without a build step.
15
+ - **JSX support** – pass JSX straight to `ctx.html(<Card />)`; no build step, no manual `renderToString` call.
16
16
  - **Middleware** – auth, rate limiting, CORS, logging, caching, compression, validation.
17
17
  - **Cookie helpers** with validation.
18
18
  - **Scheduled tasks** via Cron triggers.
@@ -54,9 +54,51 @@ const app = new Edge();
54
54
  app.get('/', (ctx) => ctx.text('Hello World!'));
55
55
  app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
56
56
 
57
- export default {
58
- fetch: (request, env, ctx) => app.fetch(request, env, ctx),
59
- };
57
+ export default app;
58
+ ```
59
+
60
+ or if you will use jsx `worker.tsx`:
61
+
62
+ ```ts
63
+ import { Edge, jsx, Fragment } from '@lengkapp/edge';
64
+
65
+ const app = new Edge();
66
+
67
+ const Card = () => (
68
+ <div>card</div>
69
+ );
70
+
71
+ const LandingPage = () => (
72
+ <>
73
+ <h1>
74
+ hello world
75
+ </h1>
76
+ <Card />
77
+ </>
78
+ );
79
+ app.get('/', () => <LandingPage />);
80
+
81
+ export default app;
82
+ ```
83
+
84
+ if you use typescript `tsconfig.json`:
85
+
86
+ ```typescript
87
+ {
88
+ "compilerOptions": {
89
+ "jsx": "react",
90
+ "jsxFactory": "jsx",
91
+ "jsxFragmentFactory": "Fragment",
92
+ "paths": { "@/*": ["./src/*"] },
93
+ "types": ["@cloudflare/workers-types"],
94
+ "target": "ESNext",
95
+ "module": "ESNext",
96
+ "moduleResolution": "Bundler",
97
+ "strict": true,
98
+ "skipLibCheck": true,
99
+ "lib": ["ESNext", "WebWorker"]
100
+ }
101
+ }
60
102
  ```
61
103
 
62
104
  `wrangler.jsonc`:
@@ -66,17 +108,26 @@ export default {
66
108
  "$schema": "./node_modules/wrangler/config-schema.json",
67
109
  "name": "my-edge-app",
68
110
  "main": "worker.js",
69
- "compatibility_date": "2026-09-06",
70
- "observability": { "enabled": true }
111
+ "compatibility_date": "2026-09-06"
71
112
  }
72
113
  ```
73
114
 
115
+ or `wrangler.toml`:
116
+
117
+ ```toml
118
+ name = "my-edge-app"
119
+ main = "worker.js"
120
+ compatibility_date = "2026-09-14"
121
+ ```
122
+
74
123
  Deploy:
75
124
 
76
125
  ```bash
77
126
  wrangler deploy
78
127
  ```
79
128
 
129
+ ---
130
+
80
131
  ## Server API
81
132
 
82
133
  ### Context
@@ -93,7 +144,9 @@ wrangler deploy
93
144
  | `ctx.getCookie(name)` | Read a cookie |
94
145
  | `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
95
146
  | `ctx.deleteCookie(name, options)` | Delete a cookie |
96
- | `ctx.text/json/html(data, status?, headers?)` | Response helpers |
147
+ | `ctx.text(data, status?, headers?)` | Plain-text response |
148
+ | `ctx.json(data, status?, headers?)` | JSON response |
149
+ | `ctx.html(data, status?, headers?)` | HTML response — accepts a raw string, a JSX element, or an array of JSX elements |
97
150
 
98
151
  Every response carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, and a restrictive `Permissions-Policy`.
99
152
 
@@ -117,22 +170,35 @@ app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
117
170
  app.defaults.cors.origin = ['https://app.example.com'];
118
171
  ```
119
172
 
120
- **Rate limit:** 429 responses carry `Retry-After: 60`.
173
+ **Rate limit:** `429` responses carry `Retry-After: 60`.
121
174
 
122
- ## JSX Support
175
+ ### JSX Support
123
176
 
124
- ```tsx
125
- import { renderToString } from '@lengkapp/edge';
177
+ `ctx.html()` accepts JSX directly — it detects JSX nodes and arrays and renders them automatically. Raw strings are passed through untouched, so you can still serve pre-rendered HTML.
126
178
 
179
+ ```tsx
127
180
  function Card({ title }) {
128
181
  return <div class="card"><h2>{title}</h2></div>;
129
182
  }
130
183
 
131
- app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
184
+ // Pass JSX straight to ctx.html — no manual renderToString needed.
185
+ app.get('/card', (ctx) => ctx.html(<Card title="Hello" />));
186
+ app.get('/heading', (ctx) => ctx.html(<h1>hello</h1>));
187
+ app.get('/list', (ctx) => ctx.html([<Card title="A" />, <Card title="B" />]));
188
+
189
+ // Raw strings still work as before.
190
+ app.get('/raw', (ctx) => ctx.html('<p>pre-rendered</p>'));
132
191
  ```
133
192
 
134
- `renderToString` hardening:
193
+ `renderToString` is still exported for advanced use cases (for example, embedding rendered HTML inside another response body or email template):
135
194
 
195
+ ```tsx
196
+ import { renderToString } from '@lengkapp/edge';
197
+
198
+ const html = renderToString(<Card title="Hello" />);
199
+ ```
200
+
201
+ **`renderToString` hardening:**
136
202
  - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
137
203
  - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
138
204
  - `on*` attributes never serialize.
@@ -140,6 +206,8 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
140
206
  - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
141
207
  - All string values are HTML-escaped.
142
208
 
209
+ ---
210
+
143
211
  ## Client (Declarative Partial Updates)
144
212
 
145
213
  ```html
@@ -152,7 +220,7 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
152
220
  |---|---|
153
221
  | `_get` / `_post` / `_put` / `_patch` / `_delete` | URL + method |
154
222
  | `_target` | CSS selector, or `"this"` |
155
- | `_trigger` | Event list: click, load, visible, intersect, etc. |
223
+ | `_trigger` | Event list: `click`, `load`, `visible`, `intersect`, etc. |
156
224
  | `_form` | Form ID to serialize as URL-encoded body |
157
225
  | `_json` | Comma-separated input names to send as JSON |
158
226
  | `_skeleton` | `"false"` disables the loading skeleton |
@@ -173,12 +241,14 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
173
241
  ### How Content Is Inserted
174
242
 
175
243
  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`.
244
+ 2. `Content-Type` is verified to be `text/html` or `text/plain`.
177
245
  3. `res.headers` may contain `x-css-required` / `x-js-required` (with optional `x-css-integrity` / `x-js-integrity` for SRI) — matching resources are injected once.
178
246
  4. The HTML is run through `sanitizeHtml`, which removes `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and neutralizes `javascript:` / `vbscript:` / `data:text/html` URLs.
179
247
  5. The sanitized HTML is assigned via `innerHTML` (through a Trusted Types policy named `edge-policy` when available).
180
248
  6. Each `<script>` in the result is rebuilt in place so the browser executes it. If the page has a CSP nonce, only scripts carrying the matching nonce run; otherwise all scripts run.
181
249
 
250
+ ---
251
+
182
252
  ## CSRF Protection
183
253
 
184
254
  The client reads a token from `<meta name="csrf-token" content="...">` or the `XSRF-TOKEN` cookie and sends it as `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
@@ -192,11 +262,13 @@ app.post('/submit', {
192
262
  }, handler);
193
263
  ```
194
264
 
265
+ ---
266
+
195
267
  ## Trusted Types / CSP
196
268
 
197
269
  If the page enforces `require-trusted-types-for 'script'`, the client creates a policy named `edge-policy` and routes every `innerHTML` through it. The policy runs the built-in sanitizer. To allowlist the policy in CSP:
198
270
 
199
- ```text
271
+ ```
200
272
  Content-Security-Policy:
201
273
  default-src 'self';
202
274
  script-src 'self';
@@ -208,6 +280,8 @@ Content-Security-Policy:
208
280
  base-uri 'self';
209
281
  ```
210
282
 
283
+ ---
284
+
211
285
  ## Security Controls
212
286
 
213
287
  ```ts
@@ -215,7 +289,7 @@ const app = new Edge();
215
289
 
216
290
  app.defaults.cors.origin = ['https://app.example.com'];
217
291
 
218
- app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
292
+ app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
219
293
  app.security.logSecurityEvents = true; // structured JSON events (default on)
220
294
  app.security.extraHeaders = {
221
295
  'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
@@ -239,6 +313,8 @@ app.security.extraHeaders = {
239
313
  | A09 Logging | Structured JSON security events with 60s dedupe |
240
314
  | A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
241
315
 
316
+ ---
317
+
242
318
  ## Scheduled Tasks
243
319
 
244
320
  ```ts
@@ -252,6 +328,8 @@ export default {
252
328
  };
253
329
  ```
254
330
 
331
+ ---
332
+
255
333
  ## Configuration
256
334
 
257
335
  ### KV Namespaces
@@ -277,11 +355,13 @@ app.authKvBinding = 'CUSTOM_AUTH_KV';
277
355
  app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
278
356
  ```
279
357
 
358
+ ---
359
+
280
360
  ## Performance
281
361
 
282
362
  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:
283
363
 
284
- | Framework | /text req/s | /json req/s |
364
+ | Framework | `/text` req/s | `/json` req/s |
285
365
  |---|---|---|
286
366
  | @lengkapp/edge | 503 | 500 |
287
367
  | Hono | 506 | 495 |
@@ -290,13 +370,11 @@ Zero runtime dependencies. On a local `wrangler dev` benchmark with `autocannon`
290
370
 
291
371
  The security hardening adds only cheap operations to the request path: prototype-safe objects, two regex checks per JSX attribute, a sanitizer template parse per response (browser-native parser), and a throttled logger. No new async boundaries, no added dependencies, no hashing on the hot path.
292
372
 
293
- ## License
294
-
295
- See LICENSE. © LengkApp — Yasir Haris
373
+ The JSX renderer itself is tuned for throughput: single-pass escaping (`charCodeAt` scan with a fast no-op path), direct string concatenation instead of intermediate arrays, cached camelCase → kebab-case conversions for style objects, and a `Set`-based URL-attribute lookup on the hot path. `ctx.html(<Card />)` skips the redundant string-identity check that a manual `renderToString(<Card />)` call would still hit.
296
374
 
297
375
  ---
298
376
 
299
- ## Summary of the security posture
377
+ ## Security Posture Summary
300
378
 
301
379
  | Layer | Mechanism |
302
380
  |---|---|
@@ -309,4 +387,10 @@ See LICENSE. © LengkApp — Yasir Haris
309
387
  | Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
310
388
  | Server middleware | Fail-closed; structured security logs; strict CORS allowlist; optional token hashing |
311
389
  | 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 |
390
+ | Build | Reserved exports/properties, `keep_quoted: "strict"`, per-bundle post-minification self-test |
391
+
392
+ ---
393
+
394
+ ## License
395
+
396
+ See LICENSE. © LengkApp — Yasir Haris