@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.
- package/dist/edge-client.js +1 -1
- package/dist/edge-server.d.ts +115 -76
- package/dist/edge-server.js +2 -2
- package/package.json +1 -1
- package/readme.md +54 -19
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.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
|
-
|
|
33
|
+
// edge-server.d.ts
|
|
34
34
|
|
|
35
|
-
|
|
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:
|
|
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
112
|
|
|
70
|
-
|
|
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
|
-
|
|
121
|
+
/* ---------------- Edge ---------------- */
|
|
74
122
|
|
|
75
|
-
export interface
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|
package/dist/edge-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @lengkapp/edge v0.0.
|
|
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={"&":"&","<":"<",">":">",'"':""","'":"'"},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.
|
|
@@ -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
|
|
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
|
-
|
|
126
|
+
### JSX Support
|
|
123
127
|
|
|
124
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
```
|
|
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;
|
|
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 |
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|