@lengkapp/edge 0.0.18 → 0.0.19

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.19
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.19
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -30,112 +30,151 @@
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 const Fragment: unique symbol;
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
112
 
70
- private _buildHeaders(headers: Record<string, string>): Headers;
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;
71
119
  }
72
120
 
73
- export const Fragment: unique symbol;
121
+ /* ---------------- Edge ---------------- */
74
122
 
75
- export interface JSXElement {
76
- type: any;
77
- props: Record<string, any>;
78
- children: any[];
79
- __isJSX: true;
123
+ export interface SecurityConfig {
124
+ hashAuthTokens: boolean;
125
+ logSecurityEvents: boolean;
126
+ extraHeaders: Record<string, string> | null;
127
+ trustedProxies: string[] | null;
80
128
  }
81
129
 
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; }
89
-
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;
98
- }
99
-
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
 
136
- declare global {
137
- namespace JSX {
138
- interface Element extends JSXElement {}
139
- interface IntrinsicElements { [elemName: string]: any; }
170
+ /* ---------------- JSX typing for this runtime ---------------- */
171
+
172
+ export namespace JSX {
173
+ type Element = JSXNode;
174
+ interface IntrinsicElements {
175
+ [elemName: string]: any;
176
+ }
177
+ interface ElementChildrenAttribute {
178
+ children: {};
140
179
  }
141
180
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.18
2
+ * @lengkapp/edge v0.0.19
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.19",
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.
@@ -77,6 +77,8 @@ Deploy:
77
77
  wrangler deploy
78
78
  ```
79
79
 
80
+ ---
81
+
80
82
  ## Server API
81
83
 
82
84
  ### Context
@@ -93,7 +95,9 @@ wrangler deploy
93
95
  | `ctx.getCookie(name)` | Read a cookie |
94
96
  | `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
95
97
  | `ctx.deleteCookie(name, options)` | Delete a cookie |
96
- | `ctx.text/json/html(data, status?, headers?)` | Response helpers |
98
+ | `ctx.text(data, status?, headers?)` | Plain-text response |
99
+ | `ctx.json(data, status?, headers?)` | JSON response |
100
+ | `ctx.html(data, status?, headers?)` | HTML response — accepts a raw string, a JSX element, or an array of JSX elements |
97
101
 
98
102
  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
103
 
@@ -117,22 +121,35 @@ app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
117
121
  app.defaults.cors.origin = ['https://app.example.com'];
118
122
  ```
119
123
 
120
- **Rate limit:** 429 responses carry `Retry-After: 60`.
124
+ **Rate limit:** `429` responses carry `Retry-After: 60`.
121
125
 
122
- ## JSX Support
126
+ ### JSX Support
123
127
 
124
- ```tsx
125
- import { renderToString } from '@lengkapp/edge';
128
+ `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
129
 
130
+ ```tsx
127
131
  function Card({ title }) {
128
132
  return <div class="card"><h2>{title}</h2></div>;
129
133
  }
130
134
 
131
- app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
135
+ // Pass JSX straight to ctx.html — no manual renderToString needed.
136
+ app.get('/card', (ctx) => ctx.html(<Card title="Hello" />));
137
+ app.get('/heading', (ctx) => ctx.html(<h1>hello</h1>));
138
+ app.get('/list', (ctx) => ctx.html([<Card title="A" />, <Card title="B" />]));
139
+
140
+ // Raw strings still work as before.
141
+ app.get('/raw', (ctx) => ctx.html('<p>pre-rendered</p>'));
132
142
  ```
133
143
 
134
- `renderToString` hardening:
144
+ `renderToString` is still exported for advanced use cases (for example, embedding rendered HTML inside another response body or email template):
145
+
146
+ ```tsx
147
+ import { renderToString } from '@lengkapp/edge';
135
148
 
149
+ const html = renderToString(<Card title="Hello" />);
150
+ ```
151
+
152
+ **`renderToString` hardening:**
136
153
  - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
137
154
  - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
138
155
  - `on*` attributes never serialize.
@@ -140,6 +157,8 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
140
157
  - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
141
158
  - All string values are HTML-escaped.
142
159
 
160
+ ---
161
+
143
162
  ## Client (Declarative Partial Updates)
144
163
 
145
164
  ```html
@@ -152,7 +171,7 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
152
171
  |---|---|
153
172
  | `_get` / `_post` / `_put` / `_patch` / `_delete` | URL + method |
154
173
  | `_target` | CSS selector, or `"this"` |
155
- | `_trigger` | Event list: click, load, visible, intersect, etc. |
174
+ | `_trigger` | Event list: `click`, `load`, `visible`, `intersect`, etc. |
156
175
  | `_form` | Form ID to serialize as URL-encoded body |
157
176
  | `_json` | Comma-separated input names to send as JSON |
158
177
  | `_skeleton` | `"false"` disables the loading skeleton |
@@ -173,12 +192,14 @@ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
173
192
  ### How Content Is Inserted
174
193
 
175
194
  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`.
195
+ 2. `Content-Type` is verified to be `text/html` or `text/plain`.
177
196
  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
197
  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
198
  5. The sanitized HTML is assigned via `innerHTML` (through a Trusted Types policy named `edge-policy` when available).
180
199
  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
200
 
201
+ ---
202
+
182
203
  ## CSRF Protection
183
204
 
184
205
  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 +213,13 @@ app.post('/submit', {
192
213
  }, handler);
193
214
  ```
194
215
 
216
+ ---
217
+
195
218
  ## Trusted Types / CSP
196
219
 
197
220
  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
221
 
199
- ```text
222
+ ```
200
223
  Content-Security-Policy:
201
224
  default-src 'self';
202
225
  script-src 'self';
@@ -208,6 +231,8 @@ Content-Security-Policy:
208
231
  base-uri 'self';
209
232
  ```
210
233
 
234
+ ---
235
+
211
236
  ## Security Controls
212
237
 
213
238
  ```ts
@@ -215,7 +240,7 @@ const app = new Edge();
215
240
 
216
241
  app.defaults.cors.origin = ['https://app.example.com'];
217
242
 
218
- app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
243
+ app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
219
244
  app.security.logSecurityEvents = true; // structured JSON events (default on)
220
245
  app.security.extraHeaders = {
221
246
  'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
@@ -239,6 +264,8 @@ app.security.extraHeaders = {
239
264
  | A09 Logging | Structured JSON security events with 60s dedupe |
240
265
  | A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
241
266
 
267
+ ---
268
+
242
269
  ## Scheduled Tasks
243
270
 
244
271
  ```ts
@@ -252,6 +279,8 @@ export default {
252
279
  };
253
280
  ```
254
281
 
282
+ ---
283
+
255
284
  ## Configuration
256
285
 
257
286
  ### KV Namespaces
@@ -277,11 +306,13 @@ app.authKvBinding = 'CUSTOM_AUTH_KV';
277
306
  app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
278
307
  ```
279
308
 
309
+ ---
310
+
280
311
  ## Performance
281
312
 
282
313
  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
314
 
284
- | Framework | /text req/s | /json req/s |
315
+ | Framework | `/text` req/s | `/json` req/s |
285
316
  |---|---|---|
286
317
  | @lengkapp/edge | 503 | 500 |
287
318
  | Hono | 506 | 495 |
@@ -290,13 +321,11 @@ Zero runtime dependencies. On a local `wrangler dev` benchmark with `autocannon`
290
321
 
291
322
  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
323
 
293
- ## License
294
-
295
- See LICENSE. © LengkApp — Yasir Haris
324
+ 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
325
 
297
326
  ---
298
327
 
299
- ## Summary of the security posture
328
+ ## Security Posture Summary
300
329
 
301
330
  | Layer | Mechanism |
302
331
  |---|---|
@@ -309,4 +338,10 @@ See LICENSE. © LengkApp — Yasir Haris
309
338
  | Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
310
339
  | Server middleware | Fail-closed; structured security logs; strict CORS allowlist; optional token hashing |
311
340
  | 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 |
341
+ | Build | Reserved exports/properties, `keep_quoted: "strict"`, per-bundle post-minification self-test |
342
+
343
+ ---
344
+
345
+ ## License
346
+
347
+ See LICENSE. © LengkApp — Yasir Haris