@lengkapp/edge 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.17
2
+ * @lengkapp/edge v0.0.18
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.17
2
+ * @lengkapp/edge v0.0.18
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @lengkapp/edge v0.0.17
2
+ * @lengkapp/edge v0.0.18
3
3
  * MIT License
4
4
  *
5
5
  * LengkApp Edge License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Edge framework used by Lengkapp",
5
5
  "main": "dist/edge-server.js",
6
6
  "types": "dist/edge-server.d.ts",
package/readme.md CHANGED
@@ -2,20 +2,37 @@
2
2
 
3
3
  A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
4
4
 
5
- Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
5
+ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**.
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).
6
8
 
7
9
  ---
8
10
 
9
11
  ## Features
10
12
 
11
- - **Trie-based routing** – static & dynamic routes (`/users/:id`) with fast lookups.
12
- - **JSX support** – render JSX components to HTML without a build step (using `jsx` and `renderToString`).
13
- - **Middleware options** – authentication, rate limiting, CORS, logging, caching, compression, and custom validation.
14
- - **Cookie helpers** – set, get, and delete cookies with flexible options.
15
- - **Declarative client** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically, with skeleton loaders and retry buttons.
16
- - **Resource injection** – automatically load CSS/JS files from response headers.
17
- - **Scheduled tasks** – built-in support for Cron triggers.
18
- - **Zero dependencies** – lightweight and fast.
13
+ ### Server
14
+ - **Trie-based routing** – static & dynamic routes (`/users/:id`).
15
+ - **JSX support** – render JSX to HTML without a build step.
16
+ - **Middleware** – auth, rate limiting, CORS, logging, caching, compression, validation.
17
+ - **Cookie helpers** with validation.
18
+ - **Scheduled tasks** via Cron triggers.
19
+ - **Zero dependencies.**
20
+
21
+ ### Client
22
+ - **Declarative partial updates** via `_get`, `_post`, `_target`, `_trigger`.
23
+ - **Trusted Types aware** – routes all `innerHTML` through an `edge-policy` sanitizer.
24
+ - **Nonce-aware script execution** – response scripts must match the page's CSP nonce when one exists.
25
+ - **Built-in sanitizer** – strips `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, and `javascript:`/`vbscript:`/`data:text/html` URLs.
26
+ - **CSRF-aware** – sends `X-CSRF-Token` on `POST`/`PUT`/`PATCH`/`DELETE`.
27
+ - **Content-Type guarded** – only `text/html` and `text/plain` responses are inserted.
28
+
29
+ ### Security (both)
30
+ - **Prototype-pollution safe** – route params, cookies, JSON bodies, JSX attributes.
31
+ - **XSS-hardened JSX** – no `on*` attributes, no `javascript:` URLs, no malformed tag names.
32
+ - **Structured security logging** – throttled JSON events for auth/rate-limit/handler failures.
33
+ - **Fail-closed middleware** – validation, auth, and rate limit errors deny by default.
34
+ - **Opt-in token hashing** – SHA-256 before KV lookup.
35
+ - **Strict CORS allowlist** – per-origin reflection with `Vary: Origin`.
19
36
 
20
37
  ---
21
38
 
@@ -25,490 +42,271 @@ Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of
25
42
  npm install @lengkapp/edge
26
43
  ```
27
44
 
28
- ---
29
-
30
45
  ## Quick Start
31
46
 
32
- **1. Create a worker script (`worker.js` or `src/index.ts`):**
47
+ `worker.js`:
33
48
 
34
49
  ```ts
35
- import { Edge, renderToString } from '@lengkapp/edge';
50
+ import { Edge } from '@lengkapp/edge';
36
51
 
37
52
  const app = new Edge();
38
53
 
39
54
  app.get('/', (ctx) => ctx.text('Hello World!'));
40
-
41
- app.get('/users/:id', (ctx) => {
42
- return ctx.json({ id: ctx.params.id });
43
- });
55
+ app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id }));
44
56
 
45
57
  export default {
46
58
  fetch: (request, env, ctx) => app.fetch(request, env, ctx),
47
59
  };
48
60
  ```
49
61
 
50
- **2. Create `wrangler.jsonc`:**
51
-
52
- Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
62
+ `wrangler.jsonc`:
53
63
 
54
64
  ```jsonc
55
65
  {
56
66
  "$schema": "./node_modules/wrangler/config-schema.json",
57
67
  "name": "my-edge-app",
58
68
  "main": "worker.js",
59
- // Set this to today's date
60
69
  "compatibility_date": "2026-09-06",
61
- "observability": {
62
- "enabled": true
63
- }
70
+ "observability": { "enabled": true }
64
71
  }
65
72
  ```
66
73
 
67
- **3. Deploy:**
74
+ Deploy:
68
75
 
69
76
  ```bash
70
77
  wrangler deploy
71
78
  ```
72
79
 
73
- > `wrangler publish` was removed in favor of `wrangler deploy`. If you're on an older Wrangler version, run `npm install -g wrangler@latest` first.
74
-
75
- ---
76
-
77
80
  ## Server API
78
81
 
79
- ### Creating an App
82
+ ### Context
80
83
 
81
- ```ts
82
- import { Edge } from '@lengkapp/edge';
83
- const app = new Edge();
84
- ```
84
+ | Member | Description |
85
+ |---|---|
86
+ | `ctx.req` | Incoming Request |
87
+ | `ctx.env` | Environment bindings |
88
+ | `ctx.executionCtx` | ExecutionContext |
89
+ | `ctx.params` | Prototype-safe route params object |
90
+ | `ctx.status` | Default response status (200) |
91
+ | `ctx.headers` | Response Headers |
92
+ | `ctx.query` | URLSearchParams |
93
+ | `ctx.getCookie(name)` | Read a cookie |
94
+ | `ctx.setCookie(name, value, options)` | Set a cookie (name validated) |
95
+ | `ctx.deleteCookie(name, options)` | Delete a cookie |
96
+ | `ctx.text/json/html(data, status?, headers?)` | Response helpers |
85
97
 
86
- ### Routing
98
+ Every response carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`, and a restrictive `Permissions-Policy`.
87
99
 
88
- Register routes using HTTP method helpers. Both static and dynamic paths are supported.
100
+ ### Route Options
89
101
 
90
102
  ```ts
91
- // Static
92
- app.get('/home', handler);
93
- app.post('/submit', handler);
94
-
95
- // Dynamic (colon-prefixed segments)
96
- app.get('/users/:id', (ctx) => {
97
- const userId = ctx.params.id;
98
- });
103
+ app.get('/protected', { auth: true }, handler);
104
+ app.get('/admin', { auth: { role: 'admin' } }, handler);
105
+ app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
106
+ app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
107
+ app.get('/cached', { cache: { ttl: 60 } }, handler);
108
+ app.get('/api', { cors: true }, handler);
109
+ app.post('/submit', { validate: (ctx) => /* ... */ true }, handler);
99
110
  ```
100
111
 
101
- Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
102
-
103
- ### Context Object
104
-
105
- Each handler receives a `Context` object (`ctx`) with the following members:
106
-
107
- | Property / Method | Description |
108
- | --- | --- |
109
- | `ctx.req` | The original `Request` object |
110
- | `ctx.env` | The environment bindings (KV, secrets, etc.) |
111
- | `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
112
- | `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
113
- | `ctx.status` | HTTP status code (default `200`) |
114
- | `ctx.headers` | `Headers` object for the response |
115
- | `ctx.query` | `URLSearchParams` (lazy) |
116
- | `ctx.getCookie(name)` | Get a cookie value |
117
- | `ctx.setCookie(name, value, options)` | Set a cookie |
118
- | `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
119
- | `ctx.text(data, status?, headers?)` | Return plain text |
120
- | `ctx.json(data, status?, headers?)` | Return JSON |
121
- | `ctx.html(data, status?, headers?)` | Return HTML |
112
+ **Auth:** Tokens are looked up in `AUTH_KV`. Send via `Authorization: Bearer <token>` or the `auth_token` cookie. Payloads with an `exp` field (Unix seconds) are rejected when expired.
122
113
 
123
- Cookie options: `path`, `domain`, `maxAge`, `expires`, `secure`, `httpOnly`, `sameSite`.
124
-
125
- ### Returning Responses
126
-
127
- You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
128
- If the handler returns a JSX element, it will be rendered to HTML automatically (see [JSX Support](#jsx-support)).
114
+ **CORS:** The default is `origin: '*'`. For production, use a strict allowlist:
129
115
 
130
116
  ```ts
131
- app.get('/json', (ctx) => {
132
- return { hello: 'world' }; // → ctx.json()
133
- });
134
-
135
- app.get('/html', (ctx) => {
136
- return '<h1>Hi</h1>'; // → ctx.html()
137
- });
117
+ app.defaults.cors.origin = ['https://app.example.com'];
138
118
  ```
139
119
 
140
- ### Route Options
120
+ **Rate limit:** 429 responses carry `Retry-After: 60`.
141
121
 
142
- Pass an options object as the second argument (before the handler) to enable middleware.
122
+ ## JSX Support
143
123
 
144
- #### Authentication (`auth`)
124
+ ```tsx
125
+ import { renderToString } from '@lengkapp/edge';
145
126
 
146
- Requires an `AUTH_KV` KV binding. Valid tokens are stored as keys with a JSON payload.
127
+ function Card({ title }) {
128
+ return <div class="card"><h2>{title}</h2></div>;
129
+ }
147
130
 
148
- ```ts
149
- app.get('/protected', { auth: true }, handler);
150
- app.get('/admin', { auth: { role: 'admin' } }, handler);
151
- app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
131
+ app.get('/card', (ctx) => ctx.html(renderToString(<Card title="Hello" />)));
152
132
  ```
153
133
 
154
- The client must send the token via `Authorization: Bearer <token>` or a cookie named `auth_token`.
134
+ `renderToString` hardening:
155
135
 
156
- #### Rate Limiting (`rateLimit`)
136
+ - Tag names must match `^[A-Za-z][A-Za-z0-9-]*$`.
137
+ - Attribute names must match `^[A-Za-z_:][A-Za-z0-9_:.-]*$`.
138
+ - `on*` attributes never serialize.
139
+ - `href`/`src`/`action`/`formaction`/`xlink:href` values beginning with `javascript:`, `vbscript:`, or `data:text/html` are stripped.
140
+ - Prototype keys (`__proto__`, `constructor`, `prototype`) are rejected.
141
+ - All string values are HTML-escaped.
157
142
 
158
- Requires a `RATE_LIMIT_KV` binding.
143
+ ## Client (Declarative Partial Updates)
159
144
 
160
- ```ts
161
- app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
145
+ ```html
146
+ <script src="https://cdn.example.com/edge-client.min.js"></script>
162
147
  ```
163
148
 
164
- #### Caching (`cache`)
149
+ ### Attributes
165
150
 
166
- Caches successful GET responses in the Cloudflare cache.
151
+ | Attribute | Description |
152
+ |---|---|
153
+ | `_get` / `_post` / `_put` / `_patch` / `_delete` | URL + method |
154
+ | `_target` | CSS selector, or `"this"` |
155
+ | `_trigger` | Event list: click, load, visible, intersect, etc. |
156
+ | `_form` | Form ID to serialize as URL-encoded body |
157
+ | `_json` | Comma-separated input names to send as JSON |
158
+ | `_skeleton` | `"false"` disables the loading skeleton |
159
+ | `_retry` | `"false"` disables the retry button |
167
160
 
168
- ```ts
169
- app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
170
- ```
161
+ ### Examples
171
162
 
172
- #### CORS (`cors`)
163
+ ```html
164
+ <button _get="/more-posts" _target="#posts">Load More</button>
173
165
 
174
- Enables CORS headers.
166
+ <div _get="/user-profile" _target="this"></div>
175
167
 
176
- ```ts
177
- app.get('/api', { cors: true }, handler);
168
+ <button _post="/login" _json="username,password" _target="#status">Login</button>
178
169
 
179
- // Custom origin / methods
180
- app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
170
+ <div _get="/lazy" _target="this" _trigger="visible"></div>
181
171
  ```
182
172
 
183
- #### Logging (`log`)
173
+ ### How Content Is Inserted
184
174
 
185
- Logs method, URL, and status to the console.
175
+ 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`.
177
+ 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
+ 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
+ 5. The sanitized HTML is assigned via `innerHTML` (through a Trusted Types policy named `edge-policy` when available).
180
+ 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.
186
181
 
187
- #### Compression (`compress`)
182
+ ## CSRF Protection
188
183
 
189
- Compresses the response using gzip/deflate/br if the client supports it.
190
-
191
- #### Custom Validation (`validate`)
192
-
193
- A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
184
+ 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`.
194
185
 
195
186
  ```ts
196
187
  app.post('/submit', {
197
- validate: (ctx) => ctx.req.headers.get('Authorization') === 'Bearer secret'
188
+ validate: (ctx) => {
189
+ const sent = ctx.req.headers.get('X-CSRF-Token');
190
+ return sent && sent === ctx.getCookie('csrf_token');
191
+ }
198
192
  }, handler);
199
193
  ```
200
194
 
201
- ---
202
-
203
- ## JSX Support
195
+ ## Trusted Types / CSP
204
196
 
205
- The package includes a minimal JSX runtime. Write components as functions returning JSX.
197
+ 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:
206
198
 
207
- ```tsx
208
- import { jsx, renderToString, Fragment } from '@lengkapp/edge';
199
+ ```text
200
+ Content-Security-Policy:
201
+ default-src 'self';
202
+ script-src 'self';
203
+ style-src 'self';
204
+ img-src 'self' data:;
205
+ require-trusted-types-for 'script';
206
+ trusted-types edge-policy;
207
+ frame-ancestors 'none';
208
+ base-uri 'self';
209
+ ```
209
210
 
210
- function Card({ title }) {
211
- return (
212
- <div class="card">
213
- <h2>{title}</h2>
214
- </div>
215
- );
216
- }
211
+ ## Security Controls
217
212
 
218
- app.get('/card', (ctx) => {
219
- return ctx.html(renderToString(<Card title="Hello" />));
220
- });
221
- ```
213
+ ```ts
214
+ const app = new Edge();
222
215
 
223
- If your handler returns a JSX element directly, Edge will automatically render it:
216
+ app.defaults.cors.origin = ['https://app.example.com'];
224
217
 
225
- ```tsx
226
- app.get('/auto', (ctx) => <h1>Auto rendered</h1>);
218
+ app.security.hashAuthTokens = true; // SHA-256 token keys, migrate first
219
+ app.security.logSecurityEvents = true; // structured JSON events (default on)
220
+ app.security.extraHeaders = {
221
+ 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
222
+ 'Cross-Origin-Opener-Policy': 'same-origin',
223
+ 'Cross-Origin-Resource-Policy': 'same-origin',
224
+ };
227
225
  ```
228
226
 
229
- ---
227
+ ### OWASP 2025 Coverage
228
+
229
+ | Category | Mitigation |
230
+ |---|---|
231
+ | A01 Broken Access Control | Auth reasons, expiry enforcement, scope/role checks, strict target resolution |
232
+ | A02 Security Misconfiguration | Safe default headers, strict CORS allowlist, `Vary: Origin` |
233
+ | A03 Supply Chain | Zero deps, SRI honored on injected resources |
234
+ | A04 Crypto Failures | Optional SHA-256 token hashing |
235
+ | A05 Injection / XSS | Prototype-safe objects, JSX attribute sanitization, Trusted Types policy, response sanitizer |
236
+ | A06 Insecure Design | Numeric-sanitized rate limiter, `Retry-After` |
237
+ | A07 Auth Failures | Structured failure reasons, no swallowed errors |
238
+ | A08 Data Integrity | Prototype-safe JSON, `Set-Cookie` stripped from cache |
239
+ | A09 Logging | Structured JSON security events with 60s dedupe |
240
+ | A10 Exceptional Conditions | Fail-closed middleware, no internal leakage |
230
241
 
231
242
  ## Scheduled Tasks
232
243
 
233
- Register a scheduled handler for Cron triggers.
234
-
235
244
  ```ts
236
245
  app.scheduled(async (event, env, ctx) => {
237
246
  console.log('Cron executed:', event.cron);
238
247
  });
239
- ```
240
248
 
241
- Then export it in your worker:
242
-
243
- ```ts
244
249
  export default {
245
250
  fetch: (req, env, ctx) => app.fetch(req, env, ctx),
246
- scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
251
+ scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx),
247
252
  };
248
253
  ```
249
254
 
250
- ---
251
-
252
- ## Client (Declarative Partial Updates)
253
-
254
- Add the client script to your page (or include it via a CDN):
255
-
256
- ```html
257
- <script src="https://cdn.example.com/edge-client.min.js"></script>
258
- ```
259
-
260
- Then use HTML attributes to make elements fetch content asynchronously.
261
-
262
- ### Attributes
263
-
264
- | Attribute | Description |
265
- | --- | --- |
266
- | `_get` | URL to fetch via GET |
267
- | `_post` | URL to fetch via POST |
268
- | `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
269
- | `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
270
- | `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
271
- | `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
272
-
273
- ### Examples
274
-
275
- **Load content on click:**
276
-
277
- ```html
278
- <button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
279
- <div id="posts"></div>
280
- ```
281
-
282
- **Auto-load on page load:**
283
-
284
- ```html
285
- <div _get="/user-profile" _target="this"></div>
286
- ```
287
-
288
- **Post a form via AJAX:**
289
-
290
- ```html
291
- <form id="contact-form">
292
- <input name="email" />
293
- <button _post="/submit" _form="contact-form" _target="#result">Submit</button>
294
- </form>
295
- <div id="result"></div>
296
- ```
297
-
298
- **Send JSON data:**
299
-
300
- ```html
301
- <input name="username" />
302
- <input name="password" />
303
- <button _post="/login" _json="username,password" _target="#status">Login</button>
304
- ```
305
-
306
- **Load when element becomes visible (IntersectionObserver):**
307
-
308
- ```html
309
- <div _get="/lazy-content" _target="this" _trigger="visible"></div>
310
- ```
311
-
312
- ### Skeleton Loading
313
-
314
- While the request is in progress, the target container is filled with a skeleton loader (three shimmer bars). On error, a "Retry" button is shown.
315
-
316
- ### Resource Injection
317
-
318
- If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page.
319
-
320
- Example server route:
321
-
322
- ```ts
323
- app.get('/widget', (ctx) => {
324
- ctx.headers.set('x-css-required', '["/widget.css"]');
325
- ctx.headers.set('x-js-required', '["/widget.js"]');
326
- return ctx.html('<div class="widget">...</div>');
327
- });
328
- ```
329
-
330
- ---
331
-
332
255
  ## Configuration
333
256
 
334
- ### `wrangler.jsonc`
335
-
336
- If you use authentication or rate limiting, you need KV namespaces. Create them first:
257
+ ### KV Namespaces
337
258
 
338
259
  ```bash
339
260
  wrangler kv namespace create AUTH_KV
340
261
  wrangler kv namespace create RATE_LIMIT_KV
341
262
  ```
342
263
 
343
- Then wire up the returned IDs in your config:
344
-
345
264
  ```jsonc
346
265
  {
347
- "$schema": "./node_modules/wrangler/config-schema.json",
348
- "name": "my-edge-app",
349
- "main": "worker.js",
350
- // Set this to today's date
351
- "compatibility_date": "2026-09-06",
352
- "observability": {
353
- "enabled": true
354
- },
355
266
  "kv_namespaces": [
356
- { "binding": "AUTH_KV", "id": "your-auth-kv-id" },
357
- { "binding": "RATE_LIMIT_KV", "id": "your-ratelimit-kv-id" }
358
- ],
359
- // Optional Cron triggers
360
- "triggers": {
361
- "crons": ["*/5 * * * *"]
362
- }
267
+ { "binding": "AUTH_KV", "id": "..." },
268
+ { "binding": "RATE_LIMIT_KV", "id": "..." }
269
+ ]
363
270
  }
364
271
  ```
365
272
 
366
- <details>
367
- <summary>Equivalent <code>wrangler.toml</code></summary>
368
-
369
- ```toml
370
- name = "my-edge-app"
371
- main = "worker.js"
372
- compatibility_date = "2026-09-06"
373
-
374
- [observability]
375
- enabled = true
376
-
377
- [[kv_namespaces]]
378
- binding = "AUTH_KV"
379
- id = "your-auth-kv-id"
380
-
381
- [[kv_namespaces]]
382
- binding = "RATE_LIMIT_KV"
383
- id = "your-ratelimit-kv-id"
384
-
385
- # Optional Cron triggers
386
- [triggers]
387
- crons = ["*/5 * * * *"]
388
- ```
389
-
390
- </details>
391
-
392
- Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
393
-
394
- ### Environment Variables
395
-
396
- The KV binding names are configurable on the Edge instance:
273
+ Rename the bindings on the instance:
397
274
 
398
275
  ```ts
399
276
  app.authKvBinding = 'CUSTOM_AUTH_KV';
400
277
  app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
401
278
  ```
402
279
 
403
- ---
404
-
405
- ## Full Example
406
-
407
- ```ts
408
- import { Edge, renderToString } from '@lengkapp/edge';
409
- import { HomePage } from './Page.jsx';
410
-
411
- const app = new Edge();
412
-
413
- // Basic text
414
- app.get('/', (ctx) => ctx.text('Hello from Edge!'));
415
-
416
- // JSX component
417
- app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
418
-
419
- // JSON with route params
420
- app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
421
-
422
- // POST JSON
423
- app.post('/users', async (ctx) => {
424
- const body = await ctx.req.json();
425
- return ctx.json({ created: true, user: body }, 201);
426
- });
427
-
428
- // Auth
429
- app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
430
-
431
- // Rate limiting
432
- app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
433
-
434
- // Caching
435
- app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
436
-
437
- // Cookies
438
- app.get('/set-cookie', (ctx) => {
439
- ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
440
- return ctx.text('Cookie set');
441
- });
442
-
443
- app.get('/get-cookie', (ctx) => {
444
- const session = ctx.getCookie('session');
445
- return ctx.text(`Cookie: ${session}`);
446
- });
447
-
448
- // Scheduled
449
- app.scheduled(async (event, env, ctx) => {
450
- console.log('Cron:', event.cron);
451
- });
452
-
453
- export default {
454
- fetch: (req, env, ctx) => app.fetch(req, env, ctx),
455
- scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
456
- };
457
- ```
458
-
459
- ---
460
-
461
280
  ## Performance
462
281
 
463
- @lengkapp/edge is inspired by [Hono](https://hono.dev) — one of the fastest Workers frameworks around — but ships with **zero dependencies**. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
282
+ 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:
464
283
 
465
- ### Test setup
284
+ | Framework | /text req/s | /json req/s |
285
+ |---|---|---|
286
+ | @lengkapp/edge | 503 | 500 |
287
+ | Hono | 506 | 495 |
288
+ | Hono (tiny) | 505 | 498 |
289
+ | Native Workers | 498 | 500 |
466
290
 
467
- - **Tool:** `autocannon` — 10 connections, 10s run per route
468
- - **Target:** local `wrangler dev` server
469
- - **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
470
- - **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
291
+ 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.
471
292
 
472
- > This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
473
-
474
- ### Results
475
-
476
- | Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
477
- | --- | --- | --- | --- | --- | --- |
478
- | **@lengkapp/edge** | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
479
- | **@lengkapp/edge** | `/json` | **500.4** | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
480
- | Hono | `/text` | **506.0** | 19.27 ms | 5,000 / 10.03s | 455 kB |
481
- | Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
482
- | Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
483
- | Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
484
- | Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
485
- | Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
486
-
487
- ```mermaid
488
- xychart-beta
489
- title "Avg requests/sec — GET /text"
490
- x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
491
- y-axis "Req/sec" 490 --> 510
492
- bar [503.1, 506.0, 505.5, 498.4]
493
- ```
494
-
495
- ```mermaid
496
- xychart-beta
497
- title "Avg requests/sec — GET /json"
498
- x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
499
- y-axis "Req/sec" 490 --> 505
500
- bar [500.4, 495.2, 497.9, 499.7]
501
- ```
502
-
503
- ### Takeaways
293
+ ## License
504
294
 
505
- - On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
506
- - On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
507
- - All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with **zero runtime dependencies**, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
508
- - @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
295
+ See LICENSE. © LengkApp — Yasir Haris
509
296
 
510
297
  ---
511
298
 
512
- ## License
513
-
514
- See [LICENSE](./LICENSE). © LengkApp — Yasir Haris
299
+ ## Summary of the security posture
300
+
301
+ | Layer | Mechanism |
302
+ |---|---|
303
+ | HTML insertion | `sanitizeHtml` → `innerHTML` via Trusted Types policy `edge-policy` |
304
+ | Dangerous elements | `<base>`, `<meta refresh>`, `<iframe srcdoc>`, `<object>`, `<embed>`, `form[action^="javascript:"]` removed |
305
+ | Dangerous URLs | `javascript:`, `vbscript:`, `data:text/html` removed from `href`/`src`/`action`/`formaction`/`xlink:href`/`data` |
306
+ | Script execution | Nonce-gated when the page uses CSP nonces; deduplicated by `src`; inline scripts block-wrapped |
307
+ | CSRF | `X-CSRF-Token` from meta or cookie on state-changing methods |
308
+ | Cookies | `credentials: 'same-origin'`; names validated on the server |
309
+ | Server response headers | `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy` |
310
+ | Server middleware | Fail-closed; structured security logs; strict CORS allowlist; optional token hashing |
311
+ | 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 |