Skip to content

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.

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 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). 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

SurfaceEdge-safe?Notes
NativeClient (data plane)Node-free entryBun/Deno/workerd/Vercel-VM smokes pass; hosted deploys pending.
LockwellKit (app kit)Node-free entryclientForTenant, signed URLs, verifyWebhook are node-free.
AdminClient (control plane)Node-free entryUses global fetch; typically a server-side/admin context.
verifyWebhookNode-free entryWebCrypto HMAC + constant-time compare; hosted deploys pending.
S3 Client (SigV4)NoNode-only. Its signer uses node:crypto. Not exported from /edge.

Next steps

Source-available under PolyForm Noncommercial 1.0.0; commercial use requires a written grant. License