@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.
- package/dist/edge-client.js +1 -1
- package/dist/edge-server.d.ts +111 -73
- package/dist/edge-server.js +2 -2
- package/package.json +1 -1
- package/readme.md +108 -24
package/dist/edge-client.js
CHANGED
package/dist/edge-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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
|
-
|
|
33
|
+
// edge-server.d.ts
|
|
34
34
|
|
|
35
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
53
|
-
private _cookies: Record<string, string> | null;
|
|
104
|
+
readonly query: URLSearchParams;
|
|
54
105
|
|
|
55
106
|
getCookie(name: string): string | null;
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
get(path: string, handler:
|
|
119
|
-
|
|
120
|
-
post(path: string, handler
|
|
121
|
-
post(path: string,
|
|
122
|
-
|
|
123
|
-
put(path: string, options
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/edge-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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={"&":"&","<":"<",">":">",'"':""","'":"'"},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="&";else if(60===e)i="<";else if(62===e)i=">";else if(34===e)i=""";else{if(39!==e)continue;i="'"}n>o&&(r+=t.slice(o,n)),r+=i,o=n+1}return o<e&&(r+=t.slice(o)),r}var Fragment=Symbol("Fragment");function jsx(t,e,...n){return{type:t,props:e||{},children:n,__isJSX:!0}}var d=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),y=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]),m=new Set(["animation-iteration-count","border-image-outset","border-image-slice","border-image-width","box-flex","box-flex-group","box-ordinal-group","column-count","columns","flex","flex-grow","flex-positive","flex-shrink","flex-negative","flex-order","grid-row","grid-row-end","grid-row-span","grid-row-start","grid-column","grid-column-end","grid-column-span","grid-column-start","font-weight","line-clamp","line-height","opacity","order","orphans","tab-size","widows","z-index","zoom","fill-opacity","flood-opacity","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width"]),g=new Map,w=2e3;function b(t){const e=g.get(t);if(void 0!==e)return e;const n=t.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/^-/,"");return g.size>=w&&g.clear(),g.set(t,n),n}function A(t){if(!t||"object"!=typeof t)return"";let e="";for(const n in t){if(!Object.prototype.hasOwnProperty.call(t,n))continue;const r=b(n);let o=t[n];"number"!=typeof o||m.has(r)||(o+="px"),e&&(e+=";"),e+=r+":"+o}return e}var C=new Set(["href","src","xlink:href","action","formaction"]);function renderToString(t){if(null==t||"boolean"==typeof t)return"";const e=typeof t;if("string"===e)return p(t);if("number"===e)return p(String(t));if(Array.isArray(t)){let e="";for(let n=0;n<t.length;n++)e+=renderToString(t[n]);return e}if(!t.__isJSX)return p(String(t));const{type:n,props:r,children:o}=t;if(n===Fragment){let t="";for(let e=0;e<o.length;e++)t+=renderToString(o[e]);return t}if("symbol"==typeof n)return"";if("function"==typeof n)return renderToString(n({...r,children:o}));if("string"!=typeof n||!/^[A-Za-z][A-Za-z0-9-]*$/.test(n))return"";const i=d.has(n);let s="";for(const t in r){if(!Object.prototype.hasOwnProperty.call(r,t))continue;if("children"===t||"key"===t||"ref"===t)continue;const e=r[t];if(null==e||!1===e)continue;if("dangerouslySetInnerHTML"===t)continue;let n=t;if("className"===t||"class"===t?n="class":"htmlFor"!==t&&"for"!==t||(n="for"),f(n)&&!/^on/i.test(n)&&/^[A-Za-z_:][A-Za-z0-9_:.-]*$/.test(n)){if(C.has(n)){const t=String(e).replace(/[\u0000-\u0020]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("vbscript:")||t.startsWith("data:text/html"))continue}if(y.has(n))!0===e&&(s+=" "+n);else if(!0!==e){if("style"===n&&"object"==typeof e){const t=A(e);t&&(s+=' style="'+p(t)+'"');continue}s+=" "+n+'="'+p(String(e))+'"'}else s+=" "+n+'="true"'}}let a="";const c=r.dangerouslySetInnerHTML;if(c&&null!=c.__html)a=c.__html;else for(let t=0;t<o.length;t++)a+=renderToString(o[t]);return i?"<"+n+s+"/>":"<"+n+s+">"+a+"</"+n+">"}var T=class{constructor(t,e,n,r={},o=null){this.req=t,this.env=e,this.executionCtx=n,this.params=r,this.status=200,this.headers=new Headers,this.headers.set("X-Content-Type-Options","nosniff"),this.headers.set("X-Frame-Options","DENY"),this.headers.set("Referrer-Policy","strict-origin-when-cross-origin"),this.headers.set("Permissions-Policy","geolocation=(), microphone=(), camera=()"),this.t=t.headers.get("Cookie")||"",this.o=null,this.url=o||new URL(t.url)}i(){if(null!==this.o)return this.o;const t=Object.create(null),e=this.t;if(e){let n=0;const r=e.length;for(;n<r;){let o=e.indexOf(";",n);-1===o&&(o=r);let i=n,s=o;for(;i<s&&32===e.charCodeAt(i);)i++;for(;s>i&&32===e.charCodeAt(s-1);)s--;if(s>i){const n=e.indexOf("=",i);if(n>i&&n<s)try{const r=decodeURIComponent(e.slice(i,n));f(r)&&(t[r]=decodeURIComponent(e.slice(n+1,s)))}catch{}}n=o+1}}return this.o=t,t}getCookie(t){const e=this.i()[t];return void 0===e?null:e}get query(){return this.url.searchParams}setCookie(t,e,n={}){if("string"!=typeof t||!f(t)||/[\r\n;=]/.test(t))return;let r=encodeURIComponent(t)+"="+encodeURIComponent(e);n.path&&(r+="; Path="+n.path),n.domain&&(r+="; Domain="+n.domain),void 0!==n.maxAge&&(r+="; Max-Age="+n.maxAge),n.expires&&(r+="; Expires="+n.expires.toUTCString()),n.secure&&(r+="; Secure"),n.httpOnly&&(r+="; HttpOnly"),n.sameSite&&(r+="; SameSite="+n.sameSite),this.headers.append("Set-Cookie",r)}deleteCookie(t,e={}){this.setCookie(t,"",{...e,maxAge:0,expires:new Date(0)})}l(t){if(!t)return this.headers;let e=!1;for(const n in t){e=!0;break}if(!e)return this.headers;const n=new Headers(this.headers);for(const e in t)Object.prototype.hasOwnProperty.call(t,e)&&n.set(e,t[e]);return n}text(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","text/plain; charset=utf-8"),new Response(t,{status:e,headers:r})}json(t,e=this.status,n={}){const r=this.l(n);return r.has("Content-Type")||r.set("Content-Type","application/json; charset=utf-8"),new Response(JSON.stringify(t),{status:e,headers:r})}html(t,e=this.status,n={}){const r=this.l(n);r.has("Content-Type")||r.set("Content-Type","text/html; charset=utf-8");const o="string"==typeof t?t:t&&t.__isJSX||Array.isArray(t)?renderToString(t):null==t?"":String(t);return new Response(o,{status:e,headers:r})}},_=class{constructor(){this.children=null,this.paramChild=null,this.handler=null}},k=class{constructor(){this.root=new _}add(t,e,n){let r=this.root;const o=[];for(let e=0;e<t.length;e++){const n=t[e];if(58===n.charCodeAt(0)){const t=n.slice(1);o.push(f(t)?t:"_p"+e),r.paramChild||(r.paramChild=new _),r=r.paramChild}else{r.children||(r.children=new Map);let t=r.children.get(n);t||(t=new _,r.children.set(n,t)),r=t}}r.handler={handler:e,options:n,paramNames:o}}match(t){const e=[],n=this.u(this.root,t,0,e);if(!n)return null;const{handler:r,options:o,paramNames:i}=n,s=Object.create(null);for(let t=0;t<i.length;t++)s[i[t]]=e[t];return{handler:r,options:o,params:s}}u(t,e,n,r){if(n===e.length)return t.handler;const o=e[n],i=t.children;if(i){const t=i.get(o);if(t){const o=this.u(t,e,n+1,r);if(o)return o}}const s=t.paramChild;if(s){r.push(o);const t=this.u(s,e,n+1,r);if(t)return t;r.pop()}return null}},O=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"],S=new Map;function E(t,e,n){const r=t+"|"+(e||"")+"|"+(n||""),o=Date.now(),i=S.get(r);if(i&&o-i<6e4)return!1;if(S.set(r,o),S.size>5e3)for(const[t,e]of S)o-e>6e4&&S.delete(t);return!0}var Edge=class{constructor(){this.staticRoutes=new Map;for(let t=0;t<O.length;t++)this.staticRoutes.set(O[t],new Map);this.dynamicTries={};for(let t=0;t<O.length;t++)this.dynamicTries[O[t]]=new k;this.authKvBinding="AUTH_KV",this.rateLimitKvBinding="RATE_LIMIT_KV",this.defaults={cors:{origin:"*",methods:"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"}},this.scheduledHandler=null,this.security={hashAuthTokens:!1,logSecurityEvents:!0,extraHeaders:null,trustedProxies:null}}h(t,e,n,r){if("boolean"==typeof n?n={auth:n}:"function"==typeof n&&(r=n,n={}),"function"!=typeof r)throw new Error("Handler for "+t+" "+e+" must be a function");const o={...this.defaults,...n||{}};-1===e.indexOf(":")?this.staticRoutes.get(t).set(l(e),{handler:r,options:o}):this.dynamicTries[t].add(c(e),r,o)}get(t,e,n){this.h("GET",t,e,n)}post(t,e,n){this.h("POST",t,e,n)}put(t,e,n){this.h("PUT",t,e,n)}delete(t,e,n){this.h("DELETE",t,e,n)}patch(t,e,n){this.h("PATCH",t,e,n)}options(t,e,n){this.h("OPTIONS",t,e,n)}head(t,e,n){this.h("HEAD",t,e,n)}scheduled(t){this.scheduledHandler=t}p(t,e,n){if(this.security.logSecurityEvents)try{const r=t.req.headers.get("CF-Connecting-IP")||"",o=t.url?t.url.pathname:"";if(!E(e,r,o))return;console.log(JSON.stringify({ts:(new Date).toISOString(),kind:"security",event:e,detail:"string"==typeof n?n:void 0,ip:r||void 0,method:t.req.method,path:o,ua:t.req.headers.get("User-Agent")||void 0}))}catch{}}async m(t,e){if(!e)return{ok:!0};const n=t.getCookie("auth_token")||t.req.headers.get("Authorization")?.replace(/^Bearer\s+/i,"");if(!n)return{ok:!1,reason:"missing_token"};const r=t.env[this.authKvBinding];if(!r)return{ok:!1,reason:"kv_unavailable"};let o,i=n;if(this.security.hashAuthTokens&&globalThis.crypto&&crypto.subtle){const t=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n)),e=new Uint8Array(t);let r="";for(let t=0;t<e.length;t++)r+=e[t].toString(16).padStart(2,"0");i="tok:"+r}try{o=await r.get(i)}catch{return{ok:!1,reason:"kv_error"}}if(!o)return{ok:!1,reason:"invalid_token"};if("object"==typeof e&&null!==e){let t;try{t=h(o)}catch{return{ok:!1,reason:"malformed_payload"}}if(t&&"object"==typeof t){if(null!=t.exp&&"number"==typeof t.exp&&t.exp<Date.now()/1e3)return{ok:!1,reason:"expired_token"};if(e.role&&t.role!==e.role)return{ok:!1,reason:"insufficient_role"};if(e.scopes){const n=Array.isArray(t.scopes)?t.scopes:[];for(let t=0;t<e.scopes.length;t++)if(-1===n.indexOf(e.scopes[t]))return{ok:!1,reason:"insufficient_scopes"}}}}return{ok:!0,payload:o}}async A(t,e){if(!e)return!0;const n=!0===e?{}:e,r=n.max??100,o=n.window??60,i="rl:"+(t.req.headers.get("CF-Connecting-IP")||"unknown"),s=t.env[this.rateLimitKvBinding];if(!s)return!0;let a;try{a=await s.get(i,"json")}catch{return!0}if(("number"!=typeof a||!isFinite(a)||a<0)&&(a=0),a>=r)return!1;a++;try{await s.put(i,JSON.stringify(a),{expirationTtl:o})}catch{}return!0}C(t,e){if(!e)return;const n="object"==typeof e?e:this.defaults.cors,r=t.req.headers.get("Origin"),o=n.origin;"*"===o?t.headers.set("Access-Control-Allow-Origin","*"):Array.isArray(o)?r&&-1!==o.indexOf(r)&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")):"string"==typeof o&&r===o&&(t.headers.set("Access-Control-Allow-Origin",r),t.headers.set("Vary","Origin"),t.headers.set("Access-Control-Allow-Credentials","true")),t.headers.set("Access-Control-Allow-Methods",n.methods||"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD"),t.headers.set("Access-Control-Allow-Headers",n.headers||"Content-Type, Authorization, X-CSRF-Token"),t.headers.set("Access-Control-Max-Age","86400")}T(t,e){e&&console.log(t.req.method+" "+t.req.url+" - "+t.status)}_(t,e){const n=t.headers.get("Accept-Encoding")||"";let r=null;if(-1!==n.indexOf("gzip")?r="gzip":-1!==n.indexOf("deflate")&&(r="deflate"),!r||!e.body)return e;const o=e.body.pipeThrough(new CompressionStream(r)),i=new Headers(e.headers);return i.set("Content-Encoding",r),i.set("Vary","Accept-Encoding"),i.delete("Content-Length"),new Response(o,{status:e.status,statusText:e.statusText,headers:i})}async k(t,e){return!e||"function"!=typeof e||!!await e(t)}async O(t){return await caches.default.match(t)||null}async S(t,e,n){if(!n)return;const r=!0===n?{}:n,o=r.ttl??3600,i=r.staleWhileRevalidate??0,s=e.clone();s.headers.set("Cache-Control","max-age="+o+(i>0?", stale-while-revalidate="+i:"")),s.headers.delete("Set-Cookie"),await caches.default.put(t,s)}async fetch(t,e,n){const r=new URL(t.url),o=l(r.pathname),i=t.method,s=this.staticRoutes.get(i);if(s){const i=s.get(o);if(i)return this.v(i.handler,i.options,t,e,n,Object.create(null),r)}const a=this.dynamicTries[i];if(a){const i=a.match(c(o));if(i)return this.v(i.handler,i.options,t,e,n,i.params,r)}return new Response("Not Found",{status:404})}async v(t,e,n,r,o,i,s){const a=new T(n,r,o,i,s),c=this.security.extraHeaders;if(c)for(const t in c)Object.prototype.hasOwnProperty.call(c,t)&&a.headers.set(t,c[t]);let l,u=!1;try{u=await this.k(a,e.validate)}catch(t){this.p(a,"validation_error",t&&t.message),u=!1}if(!u)return a.text("Validation failed",400);try{l=await this.m(a,e.auth)}catch(t){this.p(a,"auth_error",t&&t.message),l={ok:!1,reason:"auth_error"}}if(!("boolean"==typeof l?l:l&&l.ok))return this.p(a,"auth_failure",l&&l.reason||"denied"),a.text("Unauthorized",401);let f,h=!1;try{h=await this.A(a,e.rateLimit)}catch(t){this.p(a,"rate_limit_error",t&&t.message),h=!1}if(!h)return this.p(a,"rate_limit_exceeded",null),a.text("Too Many Requests",429,{"Retry-After":"60"});if(e.cache&&"GET"===n.method){const t=await this.O(n);if(t)return this.C(a,e.cors),this.T(a,e.log),t}this.C(a,e.cors);try{const e=await t(a);f=e instanceof Response?e:e&&e.__isJSX?a.html(e):a.text("OK"),a.status=f.status}catch(t){this.p(a,"handler_error",t&&t.message),f=a.text("Internal Server Error",500),a.status=500}return e.cache&&"GET"===n.method&&200===f.status&&a.executionCtx.waitUntil(this.S(n,f.clone(),e.cache)),e.compress&&(f=this._(n,f)),this.T(a,e.log),f}};
|
package/package.json
CHANGED
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** –
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
175
|
+
### JSX Support
|
|
123
176
|
|
|
124
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
```
|
|
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;
|
|
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 |
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|