@lengkapp/edge 0.0.36 → 0.0.37

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/README.md CHANGED
@@ -1,878 +1,438 @@
1
- # @lengkapp/edge
1
+ # Edge Libraries
2
2
 
3
- A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, caching, and a declarative client-side partial-update library.
3
+ Dependency-free edge runtime — HonoJS + htmx inspired. Used by **lengkapp**.
4
4
 
5
- Inspired by Hono, `@lengkapp/edge` aims for the same class of performance while shipping with zero runtime dependencies.
5
+ A tiny, zero-dependency toolkit that gives you:
6
6
 
7
- **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](#security-controls).
7
+ - **`client.js`** — browser runtime driven by HTML attributes (`_get`, `_post`, `_go`, …)
8
+ - **`server.js`** — Cloudflare Worker router with JSX, no bundler required
8
9
 
9
- ---
10
-
11
- ## Table of Contents
12
-
13
- - [Features](#features)
14
- - [Installation](#installation)
15
- - [Quick Start](#quick-start)
16
- - [Server API](#server-api)
17
- - [JSX Support](#jsx-support)
18
- - [Full Example](#full-example)
19
- - [Client (Declarative Partial Updates)](#client-declarative-partial-updates)
20
- - [Device Fingerprint](#device-fingerprint)
21
- - [CSRF Protection](#csrf-protection)
22
- - [Security Controls](#security-controls)
23
- - [OWASP 2025 Coverage](#owasp-2025-coverage)
24
- - [Scheduled Tasks](#scheduled-tasks)
25
- - [Configuration](#configuration)
26
- - [Performance](#performance)
27
- - [Security Posture Summary](#security-posture-summary)
28
- - [License](#license)
29
-
30
- ---
31
-
32
- ## Features
33
-
34
- ### Server
35
-
36
- - **Trie-based routing** – static & dynamic routes (`/users/:id`)
37
- - **JSX support** – pass JSX straight to `ctx.html(<Card />)`; no build step, no manual `renderToString` call
38
- - **Middleware** – CORS, logging, caching, compression, validation
39
- - **Cookie helpers** with validation
40
- - **Scheduled tasks** via Cron triggers
41
- - **Zero dependencies**
10
+ Small code, maximum security, no external runtime dependencies.
42
11
 
43
- ### Client
12
+ ## Contents
44
13
 
45
- - **Declarative partial updates** via `_get` / `_post` and the placement modes `_in`, `_out`, `_before`, `_after`
46
- - **Client-side navigation** via `_go` (same tab) and `_open` (new tab) — no fetch, no loader
47
- - **Opt-in device fingerprint** via `_id` — Canvas, WebGL, Audio, font probe, and basic navigator signals, hashed once per page; sent as `X-DeviceId` on `_get` / `_post` and as `?_did=` on `_go` / `_open`
48
- - **Event, load, and visibility triggers** – `click` (default), `load`, `visible`, or any DOM event name
49
- - **JSON and form bodies** – `_json="a,b,c"` or `_form="#signup"`
50
- - **Scalable Translation** – `_translate={indentifier}`, based on `html lang={lang-id}` it will look for `/t/{lang-id}/{identifier}.jon`
51
- - **Built-in loading and error states** – deferred spinner, skeleton loader, abortable requests, one-click retry
52
- - **View Transitions aware** – swaps run inside `document.startViewTransition` when available
53
- - **Zero dependencies**
14
+ 1. [Features](#1-features)
15
+ 2. [Install](#2-install)
16
+ 3. [Project Layout](#3-project-layout)
17
+ 4. [Client Usage](#4-client-usage)
18
+ 5. [Server Usage](#5-server-usage)
19
+ 6. [Full Example (sample.tsx)](#6-full-example-sampletsx)
20
+ 7. [Theming (Dark / Light)](#7-theming-dark--light)
21
+ 8. [Icons (Inline Lucide)](#8-icons-inline-lucide-svg)
22
+ 9. [Build](#9-build)
23
+ 10. [Deploy (Wrangler)](#10-deploy-wrangler)
24
+ 11. [Security](#11-security)
25
+ 12. [License](#12-license)
54
26
 
55
- ### Security
27
+ ---
56
28
 
57
- - **Prototype-pollution safe** – route params, cookies, JSON bodies, JSX attributes
58
- - **XSS-hardened JSX** – no `on*` attributes, no `javascript:` URLs, no malformed tag names
59
- - **Structured security logging** – throttled JSON events for validation and handler failures
60
- - **Fail-closed middleware** – validation and handler errors deny by default
61
- - **Strict CORS allowlist** – per-origin reflection with `Vary: Origin`
29
+ ## 1. Features
30
+
31
+ ### `client.js` (browser)
32
+
33
+ | Attribute | Description |
34
+ |------------|--------------|
35
+ | `_get` | `fetch(url)` GET |
36
+ | `_post` | `fetch(url)` POST, needs `_form` or `_json` |
37
+ | `_go` | Navigate |
38
+ | `_open` | Open in new tab |
39
+ | `_in` | Place response using `innerHTML` |
40
+ | `_out` | Place response using `outerHTML` |
41
+ | `_before` | Place response before target |
42
+ | `_after` | Place response after target |
43
+ | `_form` | FormData source selector (for `_post`) |
44
+ | `_json` | JSON source selector list (for `_post`) |
45
+ | `_trigger` | `"click"` \| `"load"` \| `"visible"` (default `"click"`) |
46
+ | `_id` | Send `X-DeviceId` header (GET/POST only) |
47
+
48
+ **Automatic behavior:**
49
+
50
+ - Always sends `X-CSRF-Token` from `<meta name="_csrf" content="...">`.
51
+ - Sends `X-DeviceId` from `deviceId()` when `_id` is present.
52
+ - `MutationObserver` re-attaches handlers to injected elements.
53
+ - Loader + skeleton + retry/error UI injected around the target (cancelable via `AbortController` on repeat requests).
54
+ - Uses `IntersectionObserver` for `_trigger="visible"`.
55
+
56
+ ### `server.js` (Cloudflare Worker)
57
+
58
+ - Hono-style router: `app.get` / `post` / `put` / `delete` / `options`
59
+ - Path params: `"/:lang/:page"`
60
+ - JSX without a bundler — works with `wrangler dev` / `deploy --minify`
61
+ - Per-route config: `{ valid, auth }` for CSRF + cookie auth
62
+ - Response helpers: `c.text`, `c.json`, `c.html`, `c.page`, `c.redirect`
63
+ - CORS allowlist (exact hosts or `*.example.com`)
64
+ - Extra response headers via `app.headers`
62
65
 
63
66
  ---
64
67
 
65
- ## Installation
68
+ ## 2. Install
66
69
 
67
70
  ```bash
68
- npm install @lengkapp/edge
69
- ```
70
-
71
- ## Quick Start
72
-
73
- `worker.js`:
74
-
75
- ```ts
76
- import { Edge } from '@lengkapp/edge';
77
-
78
- const app = new Edge();
79
-
80
- app.get('/', (ctx) => ctx.text('Hello World!'));
81
- app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
82
-
83
- export default app;
84
- ```
85
-
86
- Or, if you will use JSX, `worker.tsx`:
87
-
88
- ```tsx
89
- import { Edge, jsx, Fragment } from '@lengkapp/edge';
90
-
91
- const app = new Edge();
92
-
93
- const Card = () => (
94
- <div>card</div>
95
- );
96
-
97
- const LandingPage = () => (
98
- <>
99
- <h1>hello world</h1>
100
- <Card />
101
- </>
102
- );
103
-
104
- app.get('/', () => <LandingPage />);
105
-
106
- export default app;
107
- ```
108
-
109
- If you use TypeScript, `tsconfig.json`:
110
-
111
- ```json
112
- {
113
- "compilerOptions": {
114
- "jsx": "react",
115
- "jsxFactory": "jsx",
116
- "jsxFragmentFactory": "Fragment",
117
- "paths": { "@/*": ["./src/*"] },
118
- "types": ["@cloudflare/workers-types"],
119
- "target": "ESNext",
120
- "module": "ESNext",
121
- "moduleResolution": "Bundler",
122
- "strict": true,
123
- "skipLibCheck": true,
124
- "lib": ["ESNext", "WebWorker"]
125
- }
126
- }
71
+ git clone <this repo>
72
+ cd edge-libraries
73
+ npm install
74
+ npm run build # → dist/
75
+ npm run dev #trying sample.tsx
127
76
  ```
128
77
 
129
- `wrangler.jsonc`:
78
+ Requires Node 18+ (built and tested on Node 20+).
130
79
 
131
- ```jsonc
132
- {
133
- "$schema": "./node_modules/wrangler/config-schema.json",
134
- "name": "my-edge-app",
135
- "main": "worker.js",
136
- "compatibility_date": "2026-09-06"
137
- }
138
- ```
139
-
140
- Or `wrangler.toml`:
141
-
142
- ```toml
143
- name = "my-edge-app"
144
- main = "worker.js"
145
- compatibility_date = "2026-09-14"
146
- ```
147
-
148
- Deploy:
80
+ ---
149
81
 
150
- ```bash
151
- wrangler deploy
152
- ```
82
+ ## 3. Project Layout
153
83
 
154
- ## Server API
155
-
156
- ### Context
157
-
158
- | Member | Description |
159
- |---|---|
160
- | `ctx.req` | Incoming Request |
161
- | `ctx.env` | Environment bindings |
162
- | `ctx.executionCtx` | ExecutionContext |
163
- | `ctx.params` | Prototype-safe route params object |
164
- | `ctx.status` | Default response status (200) |
165
- | `ctx.headers` | Response Headers |
166
- | `ctx.query` | URLSearchParams |
167
- | `ctx.url` | Parsed URL object |
168
- | `ctx.getCookie(name)` | Read a cookie |
169
- | `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
170
- | `ctx.deleteCookie(name, options)` | Delete a cookie |
171
- | `ctx.text(data, status?, headers?)` | Plain-text response |
172
- | `ctx.json(data, status?, headers?)` | JSON response |
173
- | `ctx.html(data, status?, headers?)` | HTML response — accepts a raw string, a JSX element, or an array of JSX elements |
174
- | `ctx.redirect(location, status?)` | Redirect (default 302), preserving headers already set on the context |
175
-
176
- Every response carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, and a restrictive `Permissions-Policy`.
177
-
178
- ### Route Options
179
-
180
- ```ts
181
- app.get('/cached', { cache: { ttl: 60 } }, handler);
182
- app.get('/api', { cors: true }, handler);
183
- app.get('/gzip', { compress: true }, handler);
184
- app.get('/logged', { log: true }, handler);
185
- app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
186
84
  ```
187
-
188
- **Cache:** `{ ttl, staleWhileRevalidate }` — TTL in seconds, default 3600; only applied to GET responses with status 200. `Set-Cookie` is stripped from cached responses.
189
-
190
- **CORS:** The default is `origin: '*'`. For production, use a strict allowlist:
191
-
192
- ```ts
193
- app.defaults.cors.origin = ['https://app.example.com'];
85
+ client.js browser runtime (source)
86
+ client.d.ts typings for the client directives
87
+ server.js worker runtime + JSX factory (source)
88
+ server.d.ts typings for Edge, Context, JSX
89
+ sample.tsx full demo (routing, languages, theme toggle)
90
+ build.mjs esbuild + terser bundler → dist/
91
+ package.json dev deps: esbuild, terser
92
+ tsconfig.json JSX: react / jsxFactory: jsx / jsxFragmentFactory: Fragment
93
+ LICENSE MIT
94
+
95
+ dist/ build output
96
+ client.js minified + banner
97
+ server.js minified + banner
98
+ client.d.ts
99
+ server.d.ts
100
+ LICENSE
194
101
  ```
195
102
 
196
- **Compress:** Negotiates gzip or deflate from `Accept-Encoding` using the native `CompressionStream`.
103
+ ---
197
104
 
198
- **Log:** Logs `METHOD URL - STATUS` to the console.
105
+ ## 4. Client Usage
199
106
 
200
- **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.
107
+ ```html
108
+ <meta name="_csrf" content="...">
201
109
 
202
- ## JSX Support
110
+ <div id="page">loading</div>
203
111
 
204
- `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.
112
+ <!-- GET, replace inner HTML of #page, fire on click -->
113
+ <div _get="/data" _in="#page" _trigger="click">Load</div>
205
114
 
206
- ```tsx
207
- function Card({ title }) {
208
- return <div class="card"><h2>{title}</h2></div>;
209
- }
115
+ <!-- GET on page load, send X-DeviceId -->
116
+ <div _get="/stats" _in="#stats" _trigger="load" _id="1"></div>
210
117
 
211
- // Pass JSX straight to ctx.html — no manual renderToString needed.
212
- app.get('/card', (ctx) => ctx.html(<Card title="Hello" />));
213
- app.get('/heading', (ctx) => ctx.html(<h1>hello</h1>));
214
- app.get('/list', (ctx) => ctx.html([<Card title="A" />, <Card title="B" />]));
118
+ <!-- GET only when scrolled into view -->
119
+ <div _get="/more" _in="#list" _trigger="visible"></div>
215
120
 
216
- // Raw strings still work as before.
217
- app.get('/raw', (ctx) => ctx.html('<p>pre-rendered</p>'));
218
- ```
121
+ <form id="myForm" onSubmit="return false;">
122
+ <input type="text" id="emailInput" name="email" />
123
+ <input type="password" id="passwordInput" name="password" />
124
+ </form>
219
125
 
220
- Returning a JSX element directly from a handler is also supported — it is treated as an HTML response:
126
+ <!-- POST multipart/form-data from #myForm -->
127
+ <div _post="/by-form" _in="#page" _trigger="click" _form="#myForm">
128
+ Submit (FORM)
129
+ </div>
221
130
 
222
- ```tsx
223
- app.get('/', () => <LandingPage />);
224
- ```
131
+ <!-- POST application/json from selected inputs -->
132
+ <div _post="/by-json" _in="#page" _trigger="click"
133
+ _json="#emailInput,#passwordInput">
134
+ Submit (JSON)
135
+ </div>
225
136
 
226
- `renderToString` is still exported for advanced use cases (for example, embedding rendered HTML inside another response body or email template):
137
+ <!-- Navigate / new tab -->
138
+ <div _go="/home" _trigger="click">Go home</div>
139
+ <div _open="https://example.com" _trigger="click">Open example</div>
227
140
 
228
- ```tsx
229
- import { renderToString } from '@lengkapp/edge';
141
+ <!-- Placement variants -->
142
+ <div _get="/x" _in="#target"> innerHTML </div>
143
+ <div _get="/x" _out="#target"> outerHTML </div>
144
+ <div _get="/x" _before="#target"> before </div>
145
+ <div _get="/x" _after="#target"> after </div>
230
146
 
231
- const html = renderToString(<Card title="Hello" />);
147
+ <script src="/client.js"></script>
232
148
  ```
233
149
 
234
- **`renderToString` hardening:**
235
-
236
- - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
237
- - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
238
- - `on*` attributes never serialize.
239
- - `href` / `src` / `action` / `formaction` / `xlink:href` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
240
- - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
241
- - All string values are HTML-escaped.
242
- - **Style objects.** `style={{ backgroundColor: 'tomato', padding: 12 }}` is emitted as `style="background-color:tomato;padding:12px"`. Numeric values are suffixed with `px` unless the property is unitless (`opacity`, `lineHeight`, `zIndex`, `flex`, …).
243
- - **Aliases.** `className` → `class`, `htmlFor` → `for`.
244
- - **Boolean attributes.** `checked`, `disabled`, `required`, `readonly`, `multiple`, etc. emit as bare attributes when `true` and are dropped when `false`.
245
- - **`dangerouslySetInnerHTML`.** Supported via `dangerouslySetInnerHTML={{ __html: '…' }}` — the value is inserted verbatim and is not escaped. Only use it with trusted content.
150
+ Every element with a directive is auto-initialized, including elements added later to the DOM.
246
151
 
247
- ## Full Example
152
+ ---
248
153
 
249
- A single file that exercises every server feature.
154
+ ## 5. Server Usage
250
155
 
251
156
  ```tsx
252
- // sample.tsx
253
- //
254
- // Demonstrates every feature of @lengkapp/edge:
255
- // - static & dynamic routes, all HTTP methods
256
- // - params, query, cookies (get/set/delete)
257
- // - ctx.text / ctx.json / ctx.html / ctx.redirect
258
- // - JSX rendering (elements, Fragments, function components, arrays)
259
- // - style objects, boolean attributes, void elements,
260
- // className/htmlFor aliases, dangerouslySetInnerHTML
261
- // - route options: cors, cache, compress, log, validate
262
- // - security.extraHeaders, security.logSecurityEvents
263
- // - scheduled handler
264
- // - returning JSX directly from a handler
265
-
266
- import {
267
- Edge,
268
- Context,
269
- Fragment,
270
- renderToString,
271
- type JSXNode,
272
- type RouteOptions,
273
- } from '@lengkapp/edge';
274
-
275
- /* ------------------------------------------------------------------ *
276
- * Small helper components (JSX function components) *
277
- * ------------------------------------------------------------------ */
278
-
279
- function Layout(props: { title: string; children?: any }) {
280
- return (
281
- <html lang="en">
282
- <head>
283
- <meta charset="utf-8" />
284
- <meta name="viewport" content="width=device-width, initial-scale=1" />
285
- <title>{props.title}</title>
286
- </head>
287
- <body>
288
- <header>
289
- <nav>
290
- <a href="/">Home</a>{' · '}
291
- <a href="/about">About</a>{' · '}
292
- <a href="/users/42">User 42</a>{' · '}
293
- <a href="/dashboard">Dashboard</a>
294
- </nav>
295
- </header>
296
- <main>{props.children}</main>
297
- <footer>© {new Date().getFullYear()}</footer>
298
- </body>
299
- </html>
300
- );
301
- }
302
-
303
- function UserCard(props: { id: string; name: string; admin?: boolean }) {
304
- return (
305
- <div class="card" data-id={props.id}>
306
- <h2>{props.name}</h2>
307
- {props.admin && <span class="badge">admin</span>}
308
- </div>
309
- );
310
- }
311
-
312
- function TodoList(props: { items: string[] }) {
313
- return (
314
- <ul>
315
- {props.items.map((item, i) => (
316
- <li key={i}>{item}</li>
317
- ))}
318
- </ul>
319
- );
320
- }
321
-
322
- /* ------------------------------------------------------------------ *
323
- * App *
324
- * ------------------------------------------------------------------ */
157
+ import { Edge, jsx, Fragment } from "./dist/server.js";
325
158
 
326
159
  const app = new Edge();
327
160
 
328
- // ---- Security: global extra headers + keep security logging on ------
329
- app.security.logSecurityEvents = true;
330
- app.security.extraHeaders = {
331
- 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
332
- 'X-Custom-Powered-By': 'edge-server',
161
+ app.cors = ["abc.com", "*.cde.com"]; // ["*"] by default
162
+ app.headers = { "x-powered-by": "edge" }; // extra response headers
163
+
164
+ const config = {
165
+ valid: false, // require cookie _csrf == X-CSRF-Token
166
+ auth: false, // require cookie _auth
333
167
  };
334
168
 
335
- /* ================================================================== *
336
- * Basic routes *
337
- * ================================================================== */
338
-
339
- // Plain text
340
- app.get('/health', (ctx) => ctx.text('ok'));
341
-
342
- // JSON with a custom status
343
- app.get('/api/time', (ctx) =>
344
- ctx.json({ now: new Date().toISOString() }, 200)
345
- );
346
-
347
- // Returning JSX directly from a handler → automatically becomes
348
- // a text/html Response.
349
- app.get('/', () => (
350
- <Layout title="Home">
351
- <h1>Hello from edge-server</h1>
352
- <p>This page was rendered from JSX.</p>
353
- <TodoList items={['Write routes', 'Render JSX', 'Ship it']} />
354
- </Layout>
355
- ));
356
-
357
- // Explicit ctx.html with a JSX tree
358
- app.get('/about', (ctx) =>
359
- ctx.html(
360
- <Layout title="About">
361
- <h1>About</h1>
362
- <p>
363
- Fragments, components, arrays — all supported.
364
- </p>
365
- {/* Array of JSX is allowed inside a fragment */}
366
- <Fragment>
367
- <UserCard id="1" name="Ada" admin />
368
- <UserCard id="2" name="Grace" />
369
- </Fragment>
370
- </Layout>
371
- )
372
- );
373
-
374
- // ctx.html also accepts a raw HTML string (passes through unchanged)
375
- app.get('/raw', (ctx) =>
376
- ctx.html('<h1>Raw HTML</h1><p>Not escaped.</p>')
377
- );
378
-
379
- /* ================================================================== *
380
- * Params, query, cookies *
381
- * ================================================================== */
382
-
383
- // Dynamic route: /users/:id
384
- app.get('/users/:id', (ctx) => {
385
- const { id } = ctx.params;
386
- return ctx.html(
387
- <Layout title={`User ${id}`}>
388
- <UserCard id={id} name={`User #${id}`} />
389
- </Layout>
169
+ app.get("/", config, (c) => {
170
+ return c.page(
171
+ <html>
172
+ <body><h1>Hello Edge</h1></body>
173
+ </html>
390
174
  );
391
175
  });
392
176
 
393
- // Multiple params: /posts/:year/:slug
394
- app.get('/posts/:year/:slug', (ctx) => {
395
- const { year, slug } = ctx.params;
396
- return ctx.json({ year, slug });
397
- });
398
-
399
- // Query strings: /search?q=hello&limit=10
400
- app.get('/search', (ctx) => {
401
- const q = ctx.query.get('q') ?? '';
402
- const limit = Number(ctx.query.get('limit') ?? '10');
403
- return ctx.json({ q, limit });
404
- });
405
-
406
- // Cookies: read, write, delete
407
- app.get('/login', (ctx) => {
408
- ctx.setCookie('session', 'abc123', {
409
- path: '/',
410
- httpOnly: true,
411
- secure: true,
412
- sameSite: 'Lax',
413
- maxAge: 3600,
414
- });
415
- return ctx.redirect('/dashboard');
416
- });
417
-
418
- app.get('/logout', (ctx) => {
419
- ctx.deleteCookie('session', { path: '/' });
420
- return ctx.redirect('/');
421
- });
422
-
423
- app.get('/dashboard', (ctx) => {
424
- const session = ctx.getCookie('session');
425
- if (!session) return ctx.redirect('/login');
426
- return ctx.html(
427
- <Layout title="Dashboard">
428
- <h1>Dashboard</h1>
429
- <p>Session: {session}</p>
430
- </Layout>
177
+ app.get("/:lang/:page", config, (c) => {
178
+ return c.html(
179
+ <h1>{c.req.param("lang")} / {c.req.param("page")}</h1>
431
180
  );
432
181
  });
433
182
 
434
- /* ================================================================== *
435
- * All HTTP methods *
436
- * ================================================================== */
437
-
438
- app.post('/api/echo', async (ctx) => {
439
- const body = await ctx.req.json().catch(() => null);
440
- return ctx.json({ received: body }, 201);
183
+ app.post("/api/save", async (c) => {
184
+ const body = await c.req.json();
185
+ return c.json({ ok: true, body });
441
186
  });
442
187
 
443
- app.put('/api/items/:id', async (ctx) => {
444
- const body = await ctx.req.json().catch(() => null);
445
- return ctx.json({ updated: ctx.params.id, body });
446
- });
188
+ app.get("/old", (c) => c.redirect("/new", 301));
447
189
 
448
- app.patch('/api/items/:id', (ctx) =>
449
- ctx.json({ patched: ctx.params.id })
450
- );
451
-
452
- app.delete('/api/items/:id', (ctx) =>
453
- ctx.json({ deleted: ctx.params.id }, 200)
454
- );
455
-
456
- app.options('/api/items', (ctx) => ctx.text('', 204));
457
-
458
- app.head('/api/items', (ctx) => ctx.text('', 200));
459
-
460
- /* ================================================================== *
461
- * Route options: cors, cache, compress, log, validate *
462
- * ================================================================== */
463
-
464
- // CORS with a wildcard origin
465
- app.get(
466
- '/cors-open',
467
- { cors: true, log: true },
468
- (ctx) => ctx.json({ cors: 'wildcard' })
469
- );
470
-
471
- // CORS with an allow-list + credentials
472
- app.get(
473
- '/cors-restricted',
474
- {
475
- cors: {
476
- origin: ['https://app.example.com', 'https://admin.example.com'],
477
- methods: 'GET, POST',
478
- headers: 'Content-Type, X-CSRF-Token',
479
- },
480
- },
481
- (ctx) => ctx.json({ cors: 'restricted' })
482
- );
483
-
484
- // Caching: cache the GET response for 60s, revalidate in background
485
- app.get(
486
- '/cached',
487
- {
488
- cache: { ttl: 60, staleWhileRevalidate: 30 },
489
- log: true,
490
- },
491
- (ctx) => ctx.json({ generatedAt: Date.now() })
492
- );
493
-
494
- // Compression (gzip / deflate based on Accept-Encoding)
495
- app.get(
496
- '/big',
497
- { compress: true },
498
- (ctx) => ctx.html(`<pre>${'x'.repeat(5000)}</pre>`)
499
- );
500
-
501
- // Request validation — return false to get a 400 automatically
502
- app.post(
503
- '/admin',
504
- {
505
- validate: (ctx) => {
506
- const token = ctx.req.headers.get('X-Admin-Token');
507
- return token === 'let-me-in';
508
- },
509
- },
510
- (ctx) => ctx.json({ ok: true })
511
- );
512
-
513
- // Everything combined
514
- const everythingOptions: RouteOptions = {
515
- cors: { origin: '*' },
516
- cache: { ttl: 120, staleWhileRevalidate: 60 },
517
- compress: true,
518
- log: true,
519
- validate: async (ctx) => ctx.req.method === 'GET',
190
+ export default {
191
+ fetch: (request, env, ctx) => app.fetch(request, env, ctx),
520
192
  };
193
+ ```
521
194
 
522
- app.get('/everything', everythingOptions, (ctx) =>
523
- ctx.html(
524
- <Layout title="Everything">
525
- <h1>All options at once</h1>
526
- </Layout>
527
- )
528
- );
529
-
530
- /* ================================================================== *
531
- * JSX feature gallery *
532
- * ================================================================== */
533
-
534
- app.get('/jsx/gallery', (ctx) =>
535
- ctx.html(
536
- <Layout title="JSX Gallery">
537
- {/* Style objects → kebab-cased, numbers get px added */}
538
- <div
539
- style={{
540
- backgroundColor: 'tomato',
541
- padding: 12,
542
- opacity: 0.9,
543
- lineHeight: 1.4, // unitless, stays as-is
544
- }}
545
- >
546
- Styled box
547
- </div>
548
-
549
- {/* className and htmlFor are aliased to class / for */}
550
- <label className="lbl" htmlFor="name">
551
- Name
552
- </label>
553
- <input id="name" type="text" required disabled={false} />
554
-
555
- {/* Boolean attributes: true emits the bare attribute */}
556
- <input type="checkbox" checked readOnly />
557
- <button disabled>Nope</button>
558
-
559
- {/* Void elements self-close */}
560
- <img src="/logo.png" alt="logo" />
561
- <br />
562
- <hr />
563
-
564
- {/* dangerouslySetInnerHTML */}
565
- <div dangerouslySetInnerHTML={{ __html: '<b>trusted</b>' }} />
566
-
567
- {/* Fragments */}
568
- <>
569
- <p>Fragment child A</p>
570
- <p>Fragment child B</p>
571
- </>
572
-
573
- {/* Arrays of JSX */}
574
- {[<span key="a">A</span>, <span key="b">B</span>, <span key="c">C</span>]}
575
-
576
- {/* Escaping: user-supplied strings are escaped */}
577
- <p>{'<script>alert(1)</script>'}</p>
578
-
579
- {/* Dangerous URLs are dropped */}
580
- <a href="javascript:alert(1)">nope</a>
581
- <a href="https://example.com">ok</a>
582
-
583
- {/* Numbers are stringified and escaped */}
584
- <p>Count: {42}</p>
585
-
586
- {/* null / undefined / booleans render nothing */}
587
- <p>{null}{undefined}{false}{true}</p>
588
- </Layout>
589
- )
590
- );
591
-
592
- /* ================================================================== *
593
- * renderToString() standalone *
594
- * ================================================================== */
595
-
596
- app.get('/jsx/string', (ctx) => {
597
- const html = renderToString(
598
- <section>
599
- <h1>Rendered manually</h1>
600
- <p>Via renderToString()</p>
601
- </section>
602
- );
603
- return ctx.html(html);
604
- });
195
+ **Response helpers:**
196
+
197
+ | Helper | Behavior |
198
+ |--------|----------|
199
+ | `c.text(str, status?)` | `text/plain; charset=utf-8` |
200
+ | `c.json(obj \| str, status?)` | `application/json; charset=utf-8` |
201
+ | `c.html(str, status?)` | `text/html; charset=utf-8` |
202
+ | `c.page(str, status?)` | `text/html`, prepends `<!DOCTYPE html>` if needed |
203
+ | `c.redirect(url, 301 \| 302)` | default `302` |
204
+
205
+ **Request helpers on `c.req`:**
206
+
207
+ | Helper | Description |
208
+ |--------|-------------|
209
+ | `.param(name)` | path params |
210
+ | `.query(name)` | URL query |
211
+ | `.header(name)` | request header (lowercase) |
212
+ | `.cookie(name)` | parsed cookie value |
213
+ | `.json()` | parse JSON body |
214
+ | `.text()` | parse text body |
215
+ | `.formData()` | parse form body |
216
+ | `.raw` | original `Request` |
217
+ | `.url` | full URL string |
218
+ | `.method` | HTTP verb |
219
+
220
+ `c.env` is your Worker bindings. `c.ctx` is the Worker `ExecutionContext`.
605
221
 
606
- /* ================================================================== *
607
- * Scheduled handler *
608
- * ================================================================== */
222
+ ---
609
223
 
610
- app.scheduled(async (event, env, ctx) => {
611
- console.log('cron fired at', new Date(event.scheduledTime).toISOString());
612
- // e.g. warm a cache, prune KV entries, etc.
613
- });
224
+ ## 6. Full Example (sample.tsx)
614
225
 
615
- /* ================================================================== *
616
- * Cloudflare Workers entry points *
617
- * ================================================================== */
226
+ `sample.tsx` demonstrates:
618
227
 
619
- export default {
620
- fetch: (req: Request, env: any, ctx: ExecutionContext) =>
621
- app.fetch(req, env, ctx),
622
- scheduled: (event: ScheduledEvent, env: any, ctx: ExecutionContext) =>
623
- app.scheduledHandler?.(event, env, ctx),
624
- };
625
- ```
228
+ - Cookie-aware language routing (path → cookie → default)
229
+ - Root `/` redirect to `/:lang/welcome`
230
+ - Full localized page in the selected script
231
+ - Client-driven partials via `_get` + `_in` + `_trigger`
232
+ - `_trigger="load"` (fire on page load)
233
+ - `_trigger="visible"` (`IntersectionObserver`)
234
+ - `_id` → `X-DeviceId` header from `deviceId()` (returns `new Date().toString()`)
235
+ - `_form` and `_json` submissions to two separate endpoints
236
+ - `_go` navigation between language routes
237
+ - `_open` → new tab
238
+ - Placement variants (`_in`, `_out`, `_before`, `_after`)
239
+ - Dark / light theme toggle (persisted, respects `prefers-color-scheme`)
240
+ - Chakra Petch typography via Google Fonts
241
+ - Inline lucide SVG icons (no CDN, no images)
626
242
 
627
- ### Feature → route cheat-sheet
628
-
629
- | Feature | Route / location |
630
- |---|---|
631
- | `ctx.text` | `GET /health` |
632
- | `ctx.json` | `GET /api/time`, `POST /api/echo`, … |
633
- | `ctx.html` with JSX | `GET /` |
634
- | `ctx.html` with string | `GET /raw` |
635
- | Returning JSX directly | `GET /` |
636
- | `ctx.redirect` | `GET /login`, `GET /logout`, `GET /dashboard` |
637
- | `ctx.params` | `GET /users/:id`, `GET /posts/:year/:slug` |
638
- | `ctx.query` | `GET /search` |
639
- | `ctx.getCookie` / `setCookie` / `deleteCookie` | `/login`, `/logout`, `/dashboard` |
640
- | All HTTP verbs | `/api/echo` (POST), `/api/items/:id` (PUT/PATCH/DELETE), `/api/items` (OPTIONS/HEAD) |
641
- | `cors` | `/cors-open`, `/cors-restricted`, `/everything` |
642
- | `cache` | `/cached`, `/everything` |
643
- | `compress` | `/big`, `/everything` |
644
- | `log` | `/cors-open`, `/cached`, `/everything` |
645
- | `validate` | `POST /admin`, `/everything` |
646
- | `security.extraHeaders` | set once near the top |
647
- | `security.logSecurityEvents` | set once near the top |
648
- | Fragments | `GET /about`, `GET /jsx/gallery` |
649
- | Function components | `Layout`, `UserCard`, `TodoList` |
650
- | Style objects | `GET /jsx/gallery` |
651
- | Boolean attrs / void elements | `GET /jsx/gallery` |
652
- | `dangerouslySetInnerHTML` | `GET /jsx/gallery` |
653
- | `renderToString()` standalone | `GET /jsx/string` |
654
- | `scheduled()` | bottom of file |
655
-
656
- ## Client (Declarative Partial Updates)
243
+ Run it locally:
657
244
 
658
- ```html
659
- <script src="https://cdn.example.com/edge-client.min.js"></script>
245
+ ```bash
246
+ npm run build
247
+ npx wrangler dev sample.tsx
660
248
  ```
661
249
 
662
- ### Attributes
250
+ ---
663
251
 
664
- | Attribute | Description |
665
- |---|---|
666
- | `_get` / `_post` | Request URL and HTTP method. Only GET and POST are supported. |
667
- | `_go` | Navigate the current tab to the URL (`location.assign`). |
668
- | `_open` | Open the URL in a new tab (`noopener,noreferrer`). |
669
- | `_id` | Opt in to the device fingerprint for this element. See [Device Fingerprint](#device-fingerprint). |
670
- | `_in` | Replace the target's children with the response. |
671
- | `_out` | Replace the target element itself. |
672
- | `_before` | Insert the response before the target. |
673
- | `_after` | Insert the response after the target. |
674
- | `_trigger` | `click` (default), `load`, `visible`, or any DOM event name. |
675
- | `_form` | CSS selector or element ID of a form to serialize as the body. |
676
- | `_json` | Comma-separated field names to send as a JSON body. |
677
- | `_loader` | `spinner` (default), `skeleton`, or `none` / `off` / `false` to disable. |
678
- | `_timeout` | Request timeout in milliseconds (default 20000). |
252
+ ## 7. Theming (Dark / Light)
679
253
 
680
- **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.
254
+ The page reads `data-theme` on `<html>` and drives every color through CSS custom properties. There are two token sets:
681
255
 
682
- **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).
256
+ - `:root` → light
257
+ - `:root[data-theme="dark"]` → dark
683
258
 
684
- ### Examples
259
+ Initialization runs synchronously in `<head>` before the first paint, so there is no flash of the wrong theme:
685
260
 
686
261
  ```html
687
- <!-- replace the children of #posts with the response -->
688
- <button _get="/more-posts" _in="#posts">Load More</button>
689
-
690
- <!-- replace this element with the response -->
691
- <div _get="/user-profile" _out="this"></div>
262
+ <script>
263
+ (function () {
264
+ try {
265
+ var t = localStorage.getItem("theme");
266
+ if (t !== "dark" && t !== "light") {
267
+ t = window.matchMedia &&
268
+ window.matchMedia("(prefers-color-scheme: dark)").matches
269
+ ? "dark" : "light";
270
+ }
271
+ document.documentElement.setAttribute("data-theme", t);
272
+ } catch (e) {
273
+ document.documentElement.setAttribute("data-theme", "light");
274
+ }
275
+ })();
276
+ </script>
277
+ ```
692
278
 
693
- <!-- POST JSON built from form fields, replace the children of #status -->
694
- <button _post="/login" _json="username,password" _in="#status">Login</button>
279
+ The toggle button contains **both** the moon and the sun SVG. CSS shows only one:
695
280
 
696
- <!-- POST a whole form -->
697
- <form id="signup">…</form>
698
- <button _post="/signup" _form="#signup" _in="#result">Sign up</button>
281
+ ```css
282
+ :root[data-theme="light"] .icon-btn .icon-moon { display: inline-flex }
283
+ :root[data-theme="dark"] .icon-btn .icon-sun { display: inline-flex }
284
+ ```
699
285
 
700
- <!-- fetch lazily when the element scrolls into view -->
701
- <div _get="/lazy" _in="this" _trigger="visible"></div>
286
+ The click handler flips `data-theme`, writes to `localStorage`, and updates `aria-label`. When no preference is stored, the page follows the OS scheme.
702
287
 
703
- <!-- skeleton loader with a 5-second timeout -->
704
- <div _get="/feed" _in="this" _loader="skeleton" _timeout="5000"></div>
288
+ All edge-client loader/error UI uses `currentColor`, so it inherits the active theme automatically.
705
289
 
706
- <!-- same-tab navigation with a device id -->
707
- <a href="/dashboard" _go _id>Dashboard</a>
290
+ ---
708
291
 
709
- <!-- new-tab navigation -->
710
- <a href="/docs" _open>Docs</a>
292
+ ## 8. Icons (Inline Lucide SVG)
711
293
 
712
- <!-- POST with X-DeviceId header -->
713
- <button _post="/like" _id _in="#card-3">Like</button>
714
- ```
294
+ Every icon is an inline `<svg>` — no CDN, no `<img>`, no external requests.
715
295
 
716
- ### Triggers
296
+ | Icon | Purpose |
297
+ |------|---------|
298
+ | `IconMoon` | theme (dark side) |
299
+ | `IconSun` | theme (light side) |
300
+ | `IconLanguages` | brand mark |
301
+ | `IconRefresh` | refresh stats |
302
+ | `IconZap` | device ping |
303
+ | `IconExternal` | external link |
304
+ | `IconHome` | go home |
305
+ | `IconGlobe` | card header |
717
306
 
718
- - `click` (default) — handled by a single delegated document listener.
719
- - `load` / `visible` — the element is observed with `IntersectionObserver` (300px root margin) and the request fires the first time it enters the viewport.
720
- - Any other value — treated as a DOM event name. The listener is attached the first time the element becomes visible, then fires normally.
307
+ All icons use `stroke="currentColor"` and inherit their color from the surrounding element, so they follow the theme automatically.
721
308
 
722
- ### How Content Is Inserted
309
+ ---
723
310
 
724
- - The request is sent with `credentials: 'same-origin'` and `X-Requested-With: XMLHttpRequest`.
725
- - When the triggering element carries `_id`, the request includes an `X-DeviceId` header (see [Device Fingerprint](#device-fingerprint)).
726
- - For `_post`, the body is JSON (`_json`), a `FormData` object (`_form`), or empty.
727
- - The response text is parsed into a `<template>`.
728
- - 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.
729
- - Newly inserted `[_get]` / `[_post]` elements are scanned and bound.
730
- - Placement depends on the target mode:
731
- - `_in` — the target's existing children are removed, then the fragment is appended.
732
- - `_out` — the target itself is replaced.
733
- - `_before` / `_after` — the fragment is inserted adjacent to the target.
311
+ ## 9. Build
734
312
 
735
- > **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` / `_post` at as part of your trusted surface.
313
+ ```bash
314
+ npm run build
315
+ ```
736
316
 
737
- ### Loader & Error States
317
+ `build.mjs`:
738
318
 
739
- - The loader is deferred by 100ms: if the response lands before then, no loader is shown at all.
740
- - Once shown, the loader stays for at least 240ms before the content swaps in, so it never flashes.
741
- - `_loader="spinner"` (default), `_loader="skeleton"` for a shimmering skeleton, or `_loader="none"` to disable.
742
- - A new request on the same element aborts the previous one via `AbortController`.
743
- - 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.
744
- - Loaders and error states use `role="status"` / `role="alert"` with `aria-busy` set on the target while in flight.
319
+ - **esbuild** — bundles and minifies `client.js` and `server.js` (target `es2022`, format `esm`). License banner injected via esbuild's `banner.js` option.
320
+ - **terser** — second pass, toplevel mangle + 2 compress passes.
321
+ - Copies `client.d.ts`, `server.d.ts`, `LICENSE` into `dist/`.
322
+ - Gracefully falls back to an inline MIT stub if `LICENSE` is missing.
745
323
 
746
- ### View Transitions
324
+ **Output:**
747
325
 
748
- 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`.
326
+ ```
327
+ dist/client.js minified + /*! MIT */ banner
328
+ dist/server.js minified + /*! MIT */ banner
329
+ dist/client.d.ts
330
+ dist/server.d.ts
331
+ dist/LICENSE
332
+ ```
749
333
 
750
- ## Device Fingerprint
334
+ ---
751
335
 
752
- Add `_id` to any `_get` / `_post` / `_go` / `_open` element to attach a stable device identifier.
336
+ ## 10. Deploy (Wrangler)
753
337
 
754
- **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.
338
+ `wrangler.toml`:
755
339
 
756
- **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.
340
+ ```toml
341
+ name = "edge-translations"
342
+ main = "sample.tsx"
343
+ compatibility_date = "2026-01-01"
757
344
 
758
- **Delivery.**
345
+ [[rules]]
346
+ type = "Text"
347
+ globs = ["dist/client.js"]
348
+ ```
759
349
 
760
- - `_get` / `_post` → sent as the `X-DeviceId` request header (no body, no URL change).
761
- - `_go` / `_open` → appended to the URL as `?_did=<hash>` (headers cannot be set on `location.assign` / `window.open`).
350
+ In `sample.tsx`, import the built `client.js` as text and expose it via `env`:
762
351
 
763
- **Fallback.** If `crypto.subtle` is unavailable (non-secure context), a djb2 hash of the same signals is used, with lower collision resistance.
352
+ ```tsx
353
+ // @ts-ignore
354
+ import clientJs from "./dist/client.js";
764
355
 
765
- Without `_id`, no fingerprint is computed, no header is set, and no URL parameter is added.
356
+ export default {
357
+ fetch: (request, env, ctx) => {
358
+ env.CLIENT_JS ??= clientJs;
359
+ return app.fetch(request, env, ctx);
360
+ },
361
+ };
362
+ ```
766
363
 
767
- ## CSRF Protection
364
+ Alternatively, serve `dist/client.js` as a static asset and drop the `/client.js` route from the app.
768
365
 
769
- 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:
366
+ **Commands:**
770
367
 
771
- ```ts
772
- app.post('/submit', {
773
- validate: async (ctx) => {
774
- const body = await ctx.req.json().catch(() => ({}));
775
- return body.csrf_token && body.csrf_token === ctx.getCookie('csrf_token');
776
- }
777
- }, handler);
368
+ ```bash
369
+ npx wrangler dev sample.tsx # local dev
370
+ npx wrangler deploy sample.tsx --minify # production
778
371
  ```
779
372
 
780
- For `_form`-based submissions, read the field from the parsed form data using the same pattern.
373
+ ---
781
374
 
782
- ## Security Controls
375
+ ## 11. Security
783
376
 
784
- ```ts
785
- const app = new Edge();
377
+ **CSRF**
378
+ The client always sends `X-CSRF-Token` read from `<meta name="_csrf" content="...">`. Enable per-route verification with `{ valid: true }`. The server then requires cookie `_csrf == X-CSRF-Token`.
786
379
 
787
- app.defaults.cors.origin = ['https://app.example.com'];
788
-
789
- app.security.logSecurityEvents = true; // structured JSON events (default on)
790
- app.security.extraHeaders = {
791
- 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
792
- 'Cross-Origin-Opener-Policy': 'same-origin',
793
- 'Cross-Origin-Resource-Policy': 'same-origin',
794
- };
795
- ```
380
+ **Auth**
381
+ Enable per-route verification with `{ auth: true }`. The server requires a cookie named `_auth`.
796
382
 
797
- ## OWASP 2025 Coverage
798
-
799
- | Category | Mitigation |
800
- |---|---|
801
- | A01 Broken Access Control | Strict CORS allowlist, strict target resolution, no implicit trust |
802
- | A02 Security Misconfiguration | Safe default headers, strict CORS allowlist, `Vary: Origin` |
803
- | A03 Supply Chain | Zero deps |
804
- | A04 Crypto Failures | Standard Web Crypto only; no home-grown crypto |
805
- | A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization |
806
- | A06 Insecure Design | Fail-closed validation, explicit response modes |
807
- | A07 Auth Failures | Validation errors are surfaced, not swallowed |
808
- | A08 Data Integrity | Prototype-safe JSON, `Set-Cookie` stripped from cache |
809
- | A09 Logging | Structured JSON security events with 60s dedupe |
810
- | A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
811
-
812
- ## Scheduled Tasks
813
-
814
- ```ts
815
- app.scheduled(async (event, env, ctx) => {
816
- console.log('Cron executed:', event.cron);
817
- });
383
+ **Device identity**
384
+ Elements with `_id="1"` add an `X-DeviceId` header. The default `deviceId()` implementation returns `new Date().toString()`; override it globally:
818
385
 
819
- export default {
820
- fetch: (req, env, ctx) => app.fetch(req, env, ctx),
821
- scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx),
822
- };
386
+ ```js
387
+ window.deviceId = () => myStableFingerprint();
823
388
  ```
824
389
 
825
- ## Configuration
390
+ **CORS**
826
391
 
827
- The `Edge` constructor takes no arguments. Behaviour is configured through public properties and route options:
392
+ ```js
393
+ app.cors = ["abc.com", "*.cde.com"]; // exact + wildcard subdomains
394
+ app.cors = ["*"]; // allow all (default)
395
+ ```
828
396
 
829
- | Property | Type | Default |
830
- |---|---|---|
831
- | `app.defaults.cors` | `{ origin, methods }` | `origin: '*'`, `methods: 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'` |
832
- | `app.security.logSecurityEvents` | `boolean` | `true` |
833
- | `app.security.extraHeaders` | `Record<string, string> \| null` | `null` |
834
- | `app.security.trustedProxies` | `string[] \| null` | `null` |
397
+ CORS is emitted automatically, including `OPTIONS` preflight.
835
398
 
836
- ```ts
837
- app.defaults.cors = { origin: ['https://app.example.com'], methods: 'GET, POST' };
838
- app.security.extraHeaders = {
839
- 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
840
- };
841
- ```
399
+ **Cookies**
400
+ Language cookie: `lang=xx; Max-Age=31536000; Path=/; SameSite=Lax`. `HttpOnly` is deliberately **not** set so client JS can read it if needed. Add `Secure` in production by prepending `__Host-` or via response header transformation if required.
842
401
 
843
- ## Performance
402
+ **Headers**
403
+ Every response from `Edge` goes through `withHeaders()`, which merges `app.headers` and per-route CORS headers safely.
844
404
 
845
- 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:
405
+ **No dependencies**
406
+ Nothing is pulled from npm at runtime. Only build-time tooling (esbuild, terser) is dev-only.
846
407
 
847
- | Framework | /text req/s | /json req/s |
848
- |---|---|---|
849
- | @lengkapp/edge | 503 | 500 |
850
- | Hono | 506 | 495 |
851
- | Hono (tiny) | 505 | 498 |
852
- | Native Workers | 498 | 500 |
408
+ ---
853
409
 
854
- 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.
410
+ ## 12. License
855
411
 
856
- 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.
412
+ LengkApp Edge License
857
413
 
858
- 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.
414
+ Copyright (c) LengkApp — Yasir Haris
415
+ Contact: yh@lengk.app / yasir.haris@gmail.com
859
416
 
860
- 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.
417
+ Permission is granted to any person obtaining a copy of this software
418
+ and associated documentation files (the "Software") to use, copy, and
419
+ distribute the Software free of charge, for any purpose, including
420
+ commercial use, subject to the following conditions:
861
421
 
862
- ## Security Posture Summary
422
+ 1. The Software may not be modified, adapted, or altered in any way
423
+ without prior written permission from the copyright holder.
863
424
 
864
- | Layer | Mechanism |
865
- |---|---|
866
- | HTML insertion (client) | Response text parsed into a `<template>`; placement driven by `_in` / `_out` / `_before` / `_after` |
867
- | Script execution (client) | Extracted `<script>` elements re-created and appended to `<head>`; external scripts de-duplicated by absolute URL |
868
- | Request hygiene (client) | `credentials: 'same-origin'`, `X-Requested-With: XMLHttpRequest`, optional `X-DeviceId` (only when `_id` is present), abortable via `AbortController`, `_timeout` (default 20s) |
869
- | Cookies | `credentials: 'same-origin'`; names validated on the server |
870
- | Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
871
- | Server middleware | Fail-closed validation; structured security logs; strict CORS allowlist |
872
- | Prototype pollution | `Object.create(null)` + forbidden-key filtering in params, cookies, JSON, JSX (server) |
873
- | Build | Reserved exports/properties, `keep_quoted: "strict"`, per-bundle post-minification self-test |
425
+ 2. Redistribution of the Software, in whole or in part, must retain
426
+ this LICENSE file unmodified and include the copyright notice above.
874
427
 
875
- ## License
428
+ 3. This permission notice does not grant any right to use the
429
+ LengkApp name, brand, or trademarks without separate written
430
+ permission.
876
431
 
877
- MIT — see [LICENSE](./LICENSE) for the full text.
878
- © LengkApp — Yasir Haris
432
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
433
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
434
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
435
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE
436
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
437
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
438
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.