npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lengkapp/edge

v0.0.36

Published

Edge framework used by Lengkapp

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

Server

  • Trie-based routing – static & dynamic routes (/users/:id)
  • JSX support – pass JSX straight to ctx.html(<Card />); no build step, no manual renderToString call
  • Middleware – CORS, logging, caching, compression, validation
  • Cookie helpers with validation
  • Scheduled tasks via Cron triggers
  • Zero dependencies

Client

  • Declarative partial updates via _get / _post and 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 as X-DeviceId on _get / _post and 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 on html 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.startViewTransition when available
  • Zero dependencies

Security

  • Prototype-pollution safe – route params, cookies, JSON bodies, JSX attributes
  • XSS-hardened JSX – no on* attributes, no javascript: 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/edge

Quick 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 deploy

Server 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:href values beginning with javascript:, vbscript:, or data:text/html are stripped.
  • Prototype keys (__proto__, constructor, prototype) are rejected.
  • All string values are HTML-escaped.
  • 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, …).
  • Aliases. className → class, htmlFor → for.
  • Boolean attributes. checked, disabled, required, readonly, multiple, etc. emit as bare attributes when true and are dropped when false.
  • dangerouslySetInnerHTML. Supported via dangerouslySetInnerHTML={{ __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 with IntersectionObserver (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' and X-Requested-With: XMLHttpRequest.
  • When the triggering element carries _id, the request includes an X-DeviceId header (see Device Fingerprint).
  • For _post, the body is JSON (_json), a FormData object (_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 / _post at 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" with aria-busy set 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 the X-DeviceId request header (no body, no URL change).
  • _go / _open → appended to the URL as ?_did=<hash> (headers cannot be set on location.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