@lengkapp/edge
v0.0.36
Published
Edge framework used by Lengkapp
Maintainers
Readme
@lengkapp/edge
A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, caching, and a declarative client-side partial-update library.
Inspired by Hono, @lengkapp/edge aims for the same class of performance while shipping with zero runtime dependencies.
Security: The server is hardened against the OWASP Top 10:2025 and ASVS 5.0 Layer 1 controls — prototype-safe params, cookies, JSON bodies and JSX attributes; CSP-friendly response headers; fail-closed middleware; strict CORS allowlist; and structured security logging. See Security.
Table of Contents
- Features
- Installation
- Quick Start
- Server API
- JSX Support
- Full Example
- Client (Declarative Partial Updates)
- Device Fingerprint
- CSRF Protection
- Security Controls
- OWASP 2025 Coverage
- Scheduled Tasks
- Configuration
- Performance
- Security Posture Summary
- License
Features
Server
- Trie-based routing – static & dynamic routes (
/users/:id) - JSX support – pass JSX straight to
ctx.html(<Card />); no build step, no manualrenderToStringcall - Middleware – CORS, logging, caching, compression, validation
- Cookie helpers with validation
- Scheduled tasks via Cron triggers
- Zero dependencies
Client
- Declarative partial updates via
_get/_postand the placement modes_in,_out,_before,_after - Client-side navigation via
_go(same tab) and_open(new tab) — no fetch, no loader - Opt-in device fingerprint via
_id— Canvas, WebGL, Audio, font probe, and basic navigator signals, hashed once per page; sent asX-DeviceIdon_get/_postand as?_did=on_go/_open - Event, load, and visibility triggers –
click(default),load,visible, or any DOM event name - JSON and form bodies –
_json="a,b,c"or_form="#signup" - Scalable Translation –
_translate={indentifier}, based onhtml lang={lang-id}it will look for/t/{lang-id}/{identifier}.jon - Built-in loading and error states – deferred spinner, skeleton loader, abortable requests, one-click retry
- View Transitions aware – swaps run inside
document.startViewTransitionwhen available - Zero dependencies
Security
- Prototype-pollution safe – route params, cookies, JSON bodies, JSX attributes
- XSS-hardened JSX – no
on*attributes, nojavascript:URLs, no malformed tag names - Structured security logging – throttled JSON events for validation and handler failures
- Fail-closed middleware – validation and handler errors deny by default
- Strict CORS allowlist – per-origin reflection with
Vary: Origin
Installation
npm install @lengkapp/edgeQuick Start
worker.js:
import { Edge } from '@lengkapp/edge';
const app = new Edge();
app.get('/', (ctx) => ctx.text('Hello World!'));
app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
export default app;Or, if you will use JSX, worker.tsx:
import { Edge, jsx, Fragment } from '@lengkapp/edge';
const app = new Edge();
const Card = () => (
<div>card</div>
);
const LandingPage = () => (
<>
<h1>hello world</h1>
<Card />
</>
);
app.get('/', () => <LandingPage />);
export default app;If you use TypeScript, tsconfig.json:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment",
"paths": { "@/*": ["./src/*"] },
"types": ["@cloudflare/workers-types"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": ["ESNext", "WebWorker"]
}
}wrangler.jsonc:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-edge-app",
"main": "worker.js",
"compatibility_date": "2026-09-06"
}Or wrangler.toml:
name = "my-edge-app"
main = "worker.js"
compatibility_date = "2026-09-14"Deploy:
wrangler deployServer API
Context
| Member | Description |
|---|---|
| ctx.req | Incoming Request |
| ctx.env | Environment bindings |
| ctx.executionCtx | ExecutionContext |
| ctx.params | Prototype-safe route params object |
| ctx.status | Default response status (200) |
| ctx.headers | Response Headers |
| ctx.query | URLSearchParams |
| ctx.url | Parsed URL object |
| ctx.getCookie(name) | Read a cookie |
| ctx.setCookie(name, value, options) | Set a cookie (name validated) |
| ctx.deleteCookie(name, options) | Delete a cookie |
| ctx.text(data, status?, headers?) | Plain-text response |
| ctx.json(data, status?, headers?) | JSON response |
| ctx.html(data, status?, headers?) | HTML response — accepts a raw string, a JSX element, or an array of JSX elements |
| ctx.redirect(location, status?) | Redirect (default 302), preserving headers already set on the context |
Every response carries X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, and a restrictive Permissions-Policy.
Route Options
app.get('/cached', { cache: { ttl: 60 } }, handler);
app.get('/api', { cors: true }, handler);
app.get('/gzip', { compress: true }, handler);
app.get('/logged', { log: true }, handler);
app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);Cache: { ttl, staleWhileRevalidate } — TTL in seconds, default 3600; only applied to GET responses with status 200. Set-Cookie is stripped from cached responses.
CORS: The default is origin: '*'. For production, use a strict allowlist:
app.defaults.cors.origin = ['https://app.example.com'];Compress: Negotiates gzip or deflate from Accept-Encoding using the native CompressionStream.
Log: Logs METHOD URL - STATUS to the console.
Validate: Receives the Context; return true to allow or false / a falsy value to reject with 400 Validation failed. Async validators are awaited. Thrown errors are logged and treated as a rejection.
JSX Support
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.
function Card({ title }) {
return <div class="card"><h2>{title}</h2></div>;
}
// Pass JSX straight to ctx.html — no manual renderToString needed.
app.get('/card', (ctx) => ctx.html(<Card title="Hello" />));
app.get('/heading', (ctx) => ctx.html(<h1>hello</h1>));
app.get('/list', (ctx) => ctx.html([<Card title="A" />, <Card title="B" />]));
// Raw strings still work as before.
app.get('/raw', (ctx) => ctx.html('<p>pre-rendered</p>'));Returning a JSX element directly from a handler is also supported — it is treated as an HTML response:
app.get('/', () => <LandingPage />);renderToString is still exported for advanced use cases (for example, embedding rendered HTML inside another response body or email template):
import { renderToString } from '@lengkapp/edge';
const html = renderToString(<Card title="Hello" />);renderToString hardening:
- Tag names must match
^[A-Za-z][A-Za-z0-9-]*$. - Attribute names must match
^[A-Za-z_:][A-Za-z0-9_:.-]*$. on*attributes never serialize.href/src/action/formaction/xlink:hrefvalues beginning withjavascript:,vbscript:, ordata:text/htmlare stripped.- Prototype keys (
__proto__,constructor,prototype) are rejected. - All string values are HTML-escaped.
- Style objects.
style={{ backgroundColor: 'tomato', padding: 12 }}is emitted asstyle="background-color:tomato;padding:12px". Numeric values are suffixed withpxunless the property is unitless (opacity,lineHeight,zIndex,flex, …). - Aliases.
className→class,htmlFor→for. - Boolean attributes.
checked,disabled,required,readonly,multiple, etc. emit as bare attributes whentrueand are dropped whenfalse. dangerouslySetInnerHTML. Supported viadangerouslySetInnerHTML={{ __html: '…' }}— the value is inserted verbatim and is not escaped. Only use it with trusted content.
Full Example
A single file that exercises every server feature.
// sample.tsx
//
// Demonstrates every feature of @lengkapp/edge:
// - static & dynamic routes, all HTTP methods
// - params, query, cookies (get/set/delete)
// - ctx.text / ctx.json / ctx.html / ctx.redirect
// - JSX rendering (elements, Fragments, function components, arrays)
// - style objects, boolean attributes, void elements,
// className/htmlFor aliases, dangerouslySetInnerHTML
// - route options: cors, cache, compress, log, validate
// - security.extraHeaders, security.logSecurityEvents
// - scheduled handler
// - returning JSX directly from a handler
import {
Edge,
Context,
Fragment,
renderToString,
type JSXNode,
type RouteOptions,
} from '@lengkapp/edge';
/* ------------------------------------------------------------------ *
* Small helper components (JSX function components) *
* ------------------------------------------------------------------ */
function Layout(props: { title: string; children?: any }) {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{props.title}</title>
</head>
<body>
<header>
<nav>
<a href="/">Home</a>{' · '}
<a href="/about">About</a>{' · '}
<a href="/users/42">User 42</a>{' · '}
<a href="/dashboard">Dashboard</a>
</nav>
</header>
<main>{props.children}</main>
<footer>© {new Date().getFullYear()}</footer>
</body>
</html>
);
}
function UserCard(props: { id: string; name: string; admin?: boolean }) {
return (
<div class="card" data-id={props.id}>
<h2>{props.name}</h2>
{props.admin && <span class="badge">admin</span>}
</div>
);
}
function TodoList(props: { items: string[] }) {
return (
<ul>
{props.items.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
/* ------------------------------------------------------------------ *
* App *
* ------------------------------------------------------------------ */
const app = new Edge();
// ---- Security: global extra headers + keep security logging on ------
app.security.logSecurityEvents = true;
app.security.extraHeaders = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Custom-Powered-By': 'edge-server',
};
/* ================================================================== *
* Basic routes *
* ================================================================== */
// Plain text
app.get('/health', (ctx) => ctx.text('ok'));
// JSON with a custom status
app.get('/api/time', (ctx) =>
ctx.json({ now: new Date().toISOString() }, 200)
);
// Returning JSX directly from a handler → automatically becomes
// a text/html Response.
app.get('/', () => (
<Layout title="Home">
<h1>Hello from edge-server</h1>
<p>This page was rendered from JSX.</p>
<TodoList items={['Write routes', 'Render JSX', 'Ship it']} />
</Layout>
));
// Explicit ctx.html with a JSX tree
app.get('/about', (ctx) =>
ctx.html(
<Layout title="About">
<h1>About</h1>
<p>
Fragments, components, arrays — all supported.
</p>
{/* Array of JSX is allowed inside a fragment */}
<Fragment>
<UserCard id="1" name="Ada" admin />
<UserCard id="2" name="Grace" />
</Fragment>
</Layout>
)
);
// ctx.html also accepts a raw HTML string (passes through unchanged)
app.get('/raw', (ctx) =>
ctx.html('<h1>Raw HTML</h1><p>Not escaped.</p>')
);
/* ================================================================== *
* Params, query, cookies *
* ================================================================== */
// Dynamic route: /users/:id
app.get('/users/:id', (ctx) => {
const { id } = ctx.params;
return ctx.html(
<Layout title={`User ${id}`}>
<UserCard id={id} name={`User #${id}`} />
</Layout>
);
});
// Multiple params: /posts/:year/:slug
app.get('/posts/:year/:slug', (ctx) => {
const { year, slug } = ctx.params;
return ctx.json({ year, slug });
});
// Query strings: /search?q=hello&limit=10
app.get('/search', (ctx) => {
const q = ctx.query.get('q') ?? '';
const limit = Number(ctx.query.get('limit') ?? '10');
return ctx.json({ q, limit });
});
// Cookies: read, write, delete
app.get('/login', (ctx) => {
ctx.setCookie('session', 'abc123', {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: 3600,
});
return ctx.redirect('/dashboard');
});
app.get('/logout', (ctx) => {
ctx.deleteCookie('session', { path: '/' });
return ctx.redirect('/');
});
app.get('/dashboard', (ctx) => {
const session = ctx.getCookie('session');
if (!session) return ctx.redirect('/login');
return ctx.html(
<Layout title="Dashboard">
<h1>Dashboard</h1>
<p>Session: {session}</p>
</Layout>
);
});
/* ================================================================== *
* All HTTP methods *
* ================================================================== */
app.post('/api/echo', async (ctx) => {
const body = await ctx.req.json().catch(() => null);
return ctx.json({ received: body }, 201);
});
app.put('/api/items/:id', async (ctx) => {
const body = await ctx.req.json().catch(() => null);
return ctx.json({ updated: ctx.params.id, body });
});
app.patch('/api/items/:id', (ctx) =>
ctx.json({ patched: ctx.params.id })
);
app.delete('/api/items/:id', (ctx) =>
ctx.json({ deleted: ctx.params.id }, 200)
);
app.options('/api/items', (ctx) => ctx.text('', 204));
app.head('/api/items', (ctx) => ctx.text('', 200));
/* ================================================================== *
* Route options: cors, cache, compress, log, validate *
* ================================================================== */
// CORS with a wildcard origin
app.get(
'/cors-open',
{ cors: true, log: true },
(ctx) => ctx.json({ cors: 'wildcard' })
);
// CORS with an allow-list + credentials
app.get(
'/cors-restricted',
{
cors: {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: 'GET, POST',
headers: 'Content-Type, X-CSRF-Token',
},
},
(ctx) => ctx.json({ cors: 'restricted' })
);
// Caching: cache the GET response for 60s, revalidate in background
app.get(
'/cached',
{
cache: { ttl: 60, staleWhileRevalidate: 30 },
log: true,
},
(ctx) => ctx.json({ generatedAt: Date.now() })
);
// Compression (gzip / deflate based on Accept-Encoding)
app.get(
'/big',
{ compress: true },
(ctx) => ctx.html(`<pre>${'x'.repeat(5000)}</pre>`)
);
// Request validation — return false to get a 400 automatically
app.post(
'/admin',
{
validate: (ctx) => {
const token = ctx.req.headers.get('X-Admin-Token');
return token === 'let-me-in';
},
},
(ctx) => ctx.json({ ok: true })
);
// Everything combined
const everythingOptions: RouteOptions = {
cors: { origin: '*' },
cache: { ttl: 120, staleWhileRevalidate: 60 },
compress: true,
log: true,
validate: async (ctx) => ctx.req.method === 'GET',
};
app.get('/everything', everythingOptions, (ctx) =>
ctx.html(
<Layout title="Everything">
<h1>All options at once</h1>
</Layout>
)
);
/* ================================================================== *
* JSX feature gallery *
* ================================================================== */
app.get('/jsx/gallery', (ctx) =>
ctx.html(
<Layout title="JSX Gallery">
{/* Style objects → kebab-cased, numbers get px added */}
<div
style={{
backgroundColor: 'tomato',
padding: 12,
opacity: 0.9,
lineHeight: 1.4, // unitless, stays as-is
}}
>
Styled box
</div>
{/* className and htmlFor are aliased to class / for */}
<label className="lbl" htmlFor="name">
Name
</label>
<input id="name" type="text" required disabled={false} />
{/* Boolean attributes: true emits the bare attribute */}
<input type="checkbox" checked readOnly />
<button disabled>Nope</button>
{/* Void elements self-close */}
<img src="/logo.png" alt="logo" />
<br />
<hr />
{/* dangerouslySetInnerHTML */}
<div dangerouslySetInnerHTML={{ __html: '<b>trusted</b>' }} />
{/* Fragments */}
<>
<p>Fragment child A</p>
<p>Fragment child B</p>
</>
{/* Arrays of JSX */}
{[<span key="a">A</span>, <span key="b">B</span>, <span key="c">C</span>]}
{/* Escaping: user-supplied strings are escaped */}
<p>{'<script>alert(1)</script>'}</p>
{/* Dangerous URLs are dropped */}
<a href="javascript:alert(1)">nope</a>
<a href="https://example.com">ok</a>
{/* Numbers are stringified and escaped */}
<p>Count: {42}</p>
{/* null / undefined / booleans render nothing */}
<p>{null}{undefined}{false}{true}</p>
</Layout>
)
);
/* ================================================================== *
* renderToString() standalone *
* ================================================================== */
app.get('/jsx/string', (ctx) => {
const html = renderToString(
<section>
<h1>Rendered manually</h1>
<p>Via renderToString()</p>
</section>
);
return ctx.html(html);
});
/* ================================================================== *
* Scheduled handler *
* ================================================================== */
app.scheduled(async (event, env, ctx) => {
console.log('cron fired at', new Date(event.scheduledTime).toISOString());
// e.g. warm a cache, prune KV entries, etc.
});
/* ================================================================== *
* Cloudflare Workers entry points *
* ================================================================== */
export default {
fetch: (req: Request, env: any, ctx: ExecutionContext) =>
app.fetch(req, env, ctx),
scheduled: (event: ScheduledEvent, env: any, ctx: ExecutionContext) =>
app.scheduledHandler?.(event, env, ctx),
};Feature → route cheat-sheet
| Feature | Route / location |
|---|---|
| ctx.text | GET /health |
| ctx.json | GET /api/time, POST /api/echo, … |
| ctx.html with JSX | GET / |
| ctx.html with string | GET /raw |
| Returning JSX directly | GET / |
| ctx.redirect | GET /login, GET /logout, GET /dashboard |
| ctx.params | GET /users/:id, GET /posts/:year/:slug |
| ctx.query | GET /search |
| ctx.getCookie / setCookie / deleteCookie | /login, /logout, /dashboard |
| All HTTP verbs | /api/echo (POST), /api/items/:id (PUT/PATCH/DELETE), /api/items (OPTIONS/HEAD) |
| cors | /cors-open, /cors-restricted, /everything |
| cache | /cached, /everything |
| compress | /big, /everything |
| log | /cors-open, /cached, /everything |
| validate | POST /admin, /everything |
| security.extraHeaders | set once near the top |
| security.logSecurityEvents | set once near the top |
| Fragments | GET /about, GET /jsx/gallery |
| Function components | Layout, UserCard, TodoList |
| Style objects | GET /jsx/gallery |
| Boolean attrs / void elements | GET /jsx/gallery |
| dangerouslySetInnerHTML | GET /jsx/gallery |
| renderToString() standalone | GET /jsx/string |
| scheduled() | bottom of file |
Client (Declarative Partial Updates)
<script src="https://cdn.example.com/edge-client.min.js"></script>Attributes
| Attribute | Description |
|---|---|
| _get / _post | Request URL and HTTP method. Only GET and POST are supported. |
| _go | Navigate the current tab to the URL (location.assign). |
| _open | Open the URL in a new tab (noopener,noreferrer). |
| _id | Opt in to the device fingerprint for this element. See Device Fingerprint. |
| _in | Replace the target's children with the response. |
| _out | Replace the target element itself. |
| _before | Insert the response before the target. |
| _after | Insert the response after the target. |
| _trigger | click (default), load, visible, or any DOM event name. |
| _form | CSS selector or element ID of a form to serialize as the body. |
| _json | Comma-separated field names to send as a JSON body. |
| _loader | spinner (default), skeleton, or none / off / false to disable. |
| _timeout | Request timeout in milliseconds (default 20000). |
Targets. Exactly one placement attribute (_in, _out, _before, _after) should be present. Its value is a CSS selector, the literal string this, or empty — the last two both resolve to the element that carries the attribute.
Field scope. _json reads values from the closest enclosing <form>, or from the document if there is none. It also accepts fields by id first, then by name (grouped radio/checkbox inputs are handled).
Examples
<!-- replace the children of #posts with the response -->
<button _get="/more-posts" _in="#posts">Load More</button>
<!-- replace this element with the response -->
<div _get="/user-profile" _out="this"></div>
<!-- POST JSON built from form fields, replace the children of #status -->
<button _post="/login" _json="username,password" _in="#status">Login</button>
<!-- POST a whole form -->
<form id="signup">…</form>
<button _post="/signup" _form="#signup" _in="#result">Sign up</button>
<!-- fetch lazily when the element scrolls into view -->
<div _get="/lazy" _in="this" _trigger="visible"></div>
<!-- skeleton loader with a 5-second timeout -->
<div _get="/feed" _in="this" _loader="skeleton" _timeout="5000"></div>
<!-- same-tab navigation with a device id -->
<a href="/dashboard" _go _id>Dashboard</a>
<!-- new-tab navigation -->
<a href="/docs" _open>Docs</a>
<!-- POST with X-DeviceId header -->
<button _post="/like" _id _in="#card-3">Like</button>Triggers
click(default) — handled by a single delegated document listener.load/visible— the element is observed withIntersectionObserver(300px root margin) and the request fires the first time it enters the viewport.- Any other value — treated as a DOM event name. The listener is attached the first time the element becomes visible, then fires normally.
How Content Is Inserted
- The request is sent with
credentials: 'same-origin'andX-Requested-With: XMLHttpRequest. - When the triggering element carries
_id, the request includes anX-DeviceIdheader (see Device Fingerprint). - For
_post, the body is JSON (_json), aFormDataobject (_form), or empty. - The response text is parsed into a
<template>. - Any
<script>elements are lifted out, then re-created and appended to<head>so the browser executes them. External scripts are de-duplicated by absolute URL. - Newly inserted
[_get]/[_post]elements are scanned and bound. - Placement depends on the target mode:
_in— the target's existing children are removed, then the fragment is appended._out— the target itself is replaced._before/_after— the fragment is inserted adjacent to the target.
Note: No content-type check, HTML sanitization, or CSRF token is applied by the client — it trusts the server's response and inserts it as-is. Treat the partial-HTML endpoints you point
_get/_postat as part of your trusted surface.
Loader & Error States
- The loader is deferred by 100ms: if the response lands before then, no loader is shown at all.
- Once shown, the loader stays for at least 240ms before the content swaps in, so it never flashes.
_loader="spinner"(default),_loader="skeleton"for a shimmering skeleton, or_loader="none"to disable.- A new request on the same element aborts the previous one via
AbortController. - On failure — non-2xx, network error, or
_timeout— the loader is replaced in place by an error box with a retry button that re-issues the request. - Loaders and error states use
role="status"/role="alert"witharia-busyset on the target while in flight.
View Transitions
When document.startViewTransition is available, loader → content and loader → error swaps are wrapped in a view transition. The injected stylesheet disables the animation under prefers-reduced-motion: reduce.
Device Fingerprint
Add _id to any _get / _post / _go / _open element to attach a stable device identifier.
Signals. navigator.userAgent, navigator.language, screen dimensions, color depth, timezone offset, hardwareConcurrency, deviceMemory, plus Canvas, WebGL renderer, an offline Audio context, and a 10-font width probe. Combined and SHA-256 hashed.
Cost. Computed lazily once per page and cached for the document lifetime; warmed at boot if any [_id] element is in the DOM. Adds ~0.9 KB gzipped to the client bundle and a few milliseconds on the first call.
Delivery.
_get/_post→ sent as theX-DeviceIdrequest header (no body, no URL change)._go/_open→ appended to the URL as?_did=<hash>(headers cannot be set onlocation.assign/window.open).
Fallback. If crypto.subtle is unavailable (non-secure context), a djb2 hash of the same signals is used, with lower collision resistance.
Without _id, no fingerprint is computed, no header is set, and no URL parameter is added.
CSRF Protection
The client does not attach a CSRF token automatically. Include the token as a form field or as part of the JSON payload built by _json, then validate it on the server with a validate option:
app.post('/submit', {
validate: async (ctx) => {
const body = await ctx.req.json().catch(() => ({}));
return body.csrf_token && body.csrf_token === ctx.getCookie('csrf_token');
}
}, handler);For _form-based submissions, read the field from the parsed form data using the same pattern.
Security Controls
const app = new Edge();
app.defaults.cors.origin = ['https://app.example.com'];
app.security.logSecurityEvents = true; // structured JSON events (default on)
app.security.extraHeaders = {
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
};OWASP 2025 Coverage
| Category | Mitigation |
|---|---|
| A01 Broken Access Control | Strict CORS allowlist, strict target resolution, no implicit trust |
| A02 Security Misconfiguration | Safe default headers, strict CORS allowlist, Vary: Origin |
| A03 Supply Chain | Zero deps |
| A04 Crypto Failures | Standard Web Crypto only; no home-grown crypto |
| A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization |
| A06 Insecure Design | Fail-closed validation, explicit response modes |
| A07 Auth Failures | Validation errors are surfaced, not swallowed |
| A08 Data Integrity | Prototype-safe JSON, Set-Cookie stripped from cache |
| A09 Logging | Structured JSON security events with 60s dedupe |
| A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
Scheduled Tasks
app.scheduled(async (event, env, ctx) => {
console.log('Cron executed:', event.cron);
});
export default {
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx),
};Configuration
The Edge constructor takes no arguments. Behaviour is configured through public properties and route options:
| Property | Type | Default |
|---|---|---|
| app.defaults.cors | { origin, methods } | origin: '*', methods: 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD' |
| app.security.logSecurityEvents | boolean | true |
| app.security.extraHeaders | Record<string, string> \| null | null |
| app.security.trustedProxies | string[] \| null | null |
app.defaults.cors = { origin: ['https://app.example.com'], methods: 'GET, POST' };
app.security.extraHeaders = {
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
};Performance
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:
| Framework | /text req/s | /json req/s | |---|---|---| | @lengkapp/edge | 503 | 500 | | Hono | 506 | 495 | | Hono (tiny) | 505 | 498 | | Native Workers | 498 | 500 |
The security hardening adds only cheap operations to the request path: prototype-safe objects, two regex checks per JSX attribute, and a throttled logger. No new async boundaries, no added dependencies, no hashing on the hot path.
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.
The client is equally lean: it injects a single stylesheet, binds each element exactly once via a WeakSet, and uses one delegated click listener for all _trigger="click" elements. An in-flight request on an element is aborted when a new one starts, and the loader is deferred by 100ms / held for at least 240ms so fast endpoints never flash UI.
The device fingerprint is opt-in per element (_id) and cached for the document lifetime; it is warmed during boot when any [_id] element exists, so the first click is not delayed by the ~5–15 ms of Canvas / Audio work.
Security Posture Summary
| Layer | Mechanism |
|---|---|
| HTML insertion (client) | Response text parsed into a <template>; placement driven by _in / _out / _before / _after |
| Script execution (client) | Extracted <script> elements re-created and appended to <head>; external scripts de-duplicated by absolute URL |
| Request hygiene (client) | credentials: 'same-origin', X-Requested-With: XMLHttpRequest, optional X-DeviceId (only when _id is present), abortable via AbortController, _timeout (default 20s) |
| Cookies | credentials: 'same-origin'; names validated on the server |
| Server response headers | nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy |
| Server middleware | Fail-closed validation; structured security logs; strict CORS allowlist |
| Prototype pollution | Object.create(null) + forbidden-key filtering in params, cookies, JSON, JSX (server) |
| Build | Reserved exports/properties, keep_quoted: "strict", per-bundle post-minification self-test |
License
MIT — see LICENSE for the full text.
© LengkApp — Yasir Haris
