# Edge runtimes

The Node SDK's **native client** and **app kit** have a node-free `/edge` entry designed for Cloudflare Workers, Vercel
Edge, Bun, and Deno. They import nothing from `node:*` and are built on web globals (`fetch`, `ReadableStream`,
`TextEncoder`, `btoa`, `crypto.subtle`).

Static import-graph and Node-hosted WebCrypto tests cover that portable boundary. A committed smoke also runs under Bun
and Deno, exercising WebCrypto plus native token mint and request handling. The Hono example also runs under Wrangler's
local workerd engine and Vercel's Edge Runtime VM. B-009 remains open only for hosted deployment provenance and a
protected live flow in authorized provider projects.

The one exception is the **S3 `Client`**, whose SigV4 signer uses `node:crypto`. It is Node-only by design and is not
exported from the edge entry. On the edge you use the native data plane instead, which is the recommended surface
anyway.

## Use the `/edge` entry

Import from the dedicated edge entry, `@kelphect/sdk/edge`:

```ts
import {
  LockwellKit,
  NativeClient,
  AdminClient,
  verifyWebhook,
  WEBHOOK_SIGNATURE_HEADER_NAME,
  sha256ChecksumBase64,
} from "@kelphect/sdk/edge";
```

The `/edge` subpath re-exports only the `node:*`-free modules (`native.js`, `admin.js`, `kit.js`, and helpers).

Import from `/edge`, not the default barrel. The default barrel (`@kelphect/sdk`) also re-exports the Node-only S3
`Client`, so a bundler that follows the barrel drags `node:crypto` into the edge bundle and forces a `nodejs_compat`
flag or polyfill. Importing from `/edge` bundles to zero node-only code with no compatibility flag.

::: tip What is enforced The SDK's `test/edge.test.js` statically walks the `/edge` import graph and **fails the build**
if any `node:` specifier becomes reachable. It also runs a request with `node:crypto` poisoned at the module loader.
This enforces the node-free import boundary. Bun, Deno, Wrangler/workerd, and Vercel Edge Runtime add real execution;
hosted provider deployments remain a separate evidence gate. :::

## A Cloudflare Workers / Hono example

This is the intended shape of the
[`examples/hono-edge`](https://github.com/RusticStack/lockwell-sdk-node/tree/main/examples/hono-edge) app. It has not
yet been validated as a deployed Cloudflare Worker. Secrets should live only in edge env bindings (`c.env`) and never
reach the browser; the browser should receive only a short-lived signed URL.

```ts
import { Hono } from "hono";
import { cors } from "hono/cors";
// Edge-safe imports only. The S3 Client is not even reachable through /edge.
import { LockwellKit, verifyWebhook, WEBHOOK_SIGNATURE_HEADER_NAME } from "@kelphect/sdk/edge";

const app = new Hono();
app.use("/api/*", cors());

// env bindings are per-request on Workers, so build the kit from c.env per call.
function kitFor(env) {
  return new LockwellKit({
    admin: { endpoint: env.LOCKWELL_ADMIN_ENDPOINT, token: env.LOCKWELL_ADMIN_TOKEN },
    native: { endpoint: env.LOCKWELL_PUBLIC_ENDPOINT },
  });
}

// Mint a browser-direct signed upload URL (no credential leaves the server).
app.post("/api/signed-upload", async (c) => {
  const { tenantId, creds } = resolveTenant(c); // server-side; never from a forgeable path
  const { key, contentType } = await c.req.json();

  const kit = kitFor(c.env);
  const native = kit.clientForTenant(tenantId, creds);
  const up = await kit.signedUploadUrl(native, creds.bucket, key, {
    ttlSeconds: 300,
    contentType,
  });
  // Return ONLY the URL + how to use it.
  return c.json({ url: up.url, method: up.method, headers: up.headers });
});

// Stream an upload straight through the Worker to the native PUT (no buffering).
app.put("/api/objects/:key{.+}", async (c) => {
  const { tenantId, creds } = resolveTenant(c);
  const native = kitFor(c.env).clientForTenant(tenantId, creds);
  const res = await native.putObject(creds.bucket, c.req.param("key"), c.req.raw.body, {
    contentType: c.req.header("content-type") || "application/octet-stream",
  });
  return c.json({ etag: res.etag, versionId: res.versionId });
});

// Receive + verify a webhook with the constant-time WebCrypto HMAC.
app.post("/api/webhooks/lockwell", async (c) => {
  const secret = c.env.WEBHOOK_SHARED_SECRET;
  if (!secret) return c.json({ error: "no verification secret configured" }, 501);

  const sig = c.req.header(WEBHOOK_SIGNATURE_HEADER_NAME) || "";
  const raw = new Uint8Array(await c.req.raw.clone().arrayBuffer()); // exact bytes
  if (!(await verifyWebhook(raw, sig, secret))) return c.json({ error: "invalid signature" }, 401);

  const event = await c.req.json();
  return c.json({ verified: true, recordCount: (event.Records || []).length });
});

export default app; // Hono's `app` IS a { fetch(request, env, ctx) } Workers handler
```

`c.req.raw.body` is a web `ReadableStream`. The native client streams it straight to Lockwell with no whole-object
buffering. The native bearer token is minted and refreshed by the SDK and never leaves the server.

## Per-runtime integration sketches

The Cloudflare and Vercel deployment sections are configuration sketches, not records of hosted deployments. Their
official local runtime engines pass alongside Bun and Deno; B-009 must clear before hosted deployments are claimed.

**Cloudflare Workers.** `export default app`. Set the two endpoint URLs as `wrangler.toml` `[vars]` and the admin token
and shared secrets via `wrangler secret put`. The node-free entry is intended to avoid `nodejs_compat`; validate that in
the target Worker project before relying on it.

**Vercel Edge.** Re-export `app.fetch` from an Edge Function or route handler with `export const runtime = 'edge'`,
passing `process.env` as the env arg: `app.fetch(request, process.env)`.

**Bun.** `Bun.serve({ port, fetch: (req) => app.fetch(req, process.env) })`.

**Deno.** `Deno.serve({ port }, (req) => app.fetch(req, Deno.env.toObject()))`. Import-map the `@kelphect/sdk` and
`hono` specifiers in `deno.json`.

## Edge-safe checksums

A native idempotent PUT needs a body-integrity checksum (see
[idempotency keys](/guide/conditional-writes#idempotency-keys)). On the edge, `sha256ChecksumBase64` computes the
SHA-256 with `crypto.subtle`, no `node:crypto`:

```ts
import { sha256ChecksumBase64 } from "@kelphect/sdk/edge";

const body = "invoice-payload";
await native.putObject("billing", "invoices/2026-001.json", body, {
  idempotencyKey: "invoice-2026-001",
  checksums: { sha256: await sha256ChecksumBase64(body) },
});
```

CRC32C and CRC64NVME checksums can be precomputed and passed directly.

## What runs where

| Surface                       | Edge-safe?      | Notes                                                                |
| ----------------------------- | --------------- | -------------------------------------------------------------------- |
| `NativeClient` (data plane)   | Node-free entry | Bun/Deno/workerd/Vercel-VM smokes pass; hosted deploys pending.      |
| `LockwellKit` (app kit)       | Node-free entry | `clientForTenant`, signed URLs, `verifyWebhook` are node-free.       |
| `AdminClient` (control plane) | Node-free entry | Uses global `fetch`; typically a server-side/admin context.          |
| `verifyWebhook`               | Node-free entry | WebCrypto HMAC + constant-time compare; hosted deploys pending.      |
| S3 `Client` (SigV4)           | No              | Node-only. Its signer uses `node:crypto`. Not exported from `/edge`. |

## Next steps

- [Data operations](/guide/data-operations). The native API the edge client exposes.
- [Signed URLs](/guide/signed-urls). The browser-direct upload pattern shown above.
- [Webhooks](/guide/webhooks). Receiving and verifying deliveries.
