Node.js
@jiayang-cloud/sdk verifies the identity token the edge sends your app and gives you the caller. It runs anywhere with WebCrypto and fetch: Workers, Node 20 or newer, Deno and Bun.
npm install @jiayang-cloud/sdkIt ships ES modules and CommonJS, with TypeScript types. It has no runtime dependencies to install: the JWT library it uses is bundled in.
A Worker
Section titled “A Worker”requireUser(request, env) takes a Request and the platform’s variables. On Workers those are the bindings your fetch handler receives.
import { requireUser, Unauthorized } from "@jiayang-cloud/sdk";
export default { async fetch(request, env) { try { const user = await requireUser(request, env); return new Response(`hello ${user.email ?? user.sub}`); } catch (err) { if (err instanceof Unauthorized) return err.toResponse(); throw err; } },};It returns a User:
interface User { kind: "user" | "service"; sub: string; // "usr_…", or "service:<token id>" for a bypass token email: string | null; // null for a bypass token role: "viewer" | "editor" | "owner"; workspaceId: string;}See the SDK overview for what each field means and what the SDK checks.
import { Forbidden, hasRole, requireRole } from "@jiayang-cloud/sdk";
requireRole(user, "editor"); // throws Forbidden unless the caller is an editor or an owner
if (hasRole(user, "owner")) { // show the settings link}Forbidden means the caller is known but may not do this. Answer it with err.toResponse(), which is a 403.
Your framework
Section titled “Your framework”Each framework has its own entry point, so a Next.js app never loads the Express code.
| Import | For |
|---|---|
@jiayang-cloud/sdk |
Workers, and anything that hands you a Request |
@jiayang-cloud/sdk/next |
Next.js server components and route handlers |
@jiayang-cloud/sdk/express |
Express, Connect, and Fastify’s Express plugin |
@jiayang-cloud/sdk/hono |
Hono, on Workers, Node, Bun or Deno |
@jiayang-cloud/sdk/node |
node:http, Koa, anything with Node-style headers |
The /next and /express entries read the platform’s variables from process.env, unless you pass env yourself. /node reads nothing on its own: pass it envFromProcess(), as in the example below.
Next.js
Section titled “Next.js”import { getUser, requireUser } from "@jiayang-cloud/sdk/next";
export default async function Page() { const user = await getUser(); return <p>hello {user?.email ?? "stranger"}</p>;}Both read the request’s headers through next/headers. In a page or other server component, use getUser(): it returns null when there is no valid caller. requireUser() throws Unauthorized, and thrown from a server component that’s a 500, not a 401. Use it in a route handler, and answer with the error’s response:
import { Unauthorized } from "@jiayang-cloud/sdk";import { requireUser } from "@jiayang-cloud/sdk/next";
export async function GET() { try { const user = await requireUser(); return Response.json({ email: user.email }); } catch (err) { if (err instanceof Unauthorized) return err.toResponse(); throw err; }}Anywhere next/headers can’t be used, pass the request’s headers in: requireUser({ headers: request.headers }).
Express
Section titled “Express”import { requireUser, withUser } from "@jiayang-cloud/sdk/express";
app.use(requireUser());app.get("/", (req, res) => res.send(`hello ${req.user.email}`));
app.post("/orders", requireUser({ role: "editor" }), placeOrder);requireUser() answers 401 unauthorized without a valid caller, and 403 forbidden: this needs editor when role is set and the caller has less. Neither reaches your handler. Otherwise it sets req.user.
withUser() lets everyone through and sets req.user when there is a caller, for a page that renders either way.
import { getUser, jiayang } from "@jiayang-cloud/sdk/hono";
app.use("*", jiayang());app.get("/", (c) => c.text(`hello ${getUser(c).email}`));
app.post("/orders", jiayang({ role: "editor" }), placeOrder);jiayang() answers 401 or 403 itself and puts the caller on the context. It reads the platform’s variables from Hono’s bindings on Workers, and from process.env on Node, Bun and Deno, so the same line works in a container.
getUser(c) throws if jiayang() didn’t run on that route.
node:http, Koa and others
Section titled “node:http, Koa and others”import { createServer } from "node:http";import { envFromProcess, requireUserFrom } from "@jiayang-cloud/sdk/node";
createServer(async (req, res) => { try { const user = await requireUserFrom(req, envFromProcess()); res.end(`hello ${user.email}\n`); } catch { res.statusCode = 401; res.end("unauthorized\n"); }}).listen(process.env.PORT);requireUserFrom takes anything with Node-style headers, or the headers themselves. envFromProcess() reads the three variables from process.env.
A token sent twice is refused rather than one of them being picked. The edge sends exactly one, so two means something else added one. Node’s own server joins a repeated header into one value, so under node:http, Express and Koa two tokens fail as invalid identity token.
Webhooks
Section titled “Webhooks”A delivery on a verified public path arrives with a token of kind webhook, not a person’s. requireWebhook checks it and says which provider signed the delivery. requireUser refuses a webhook’s token, and requireWebhook refuses a person’s.
import { requireWebhook, Unauthorized } from "@jiayang-cloud/sdk";
export default { async fetch(request, env) { if (new URL(request.url).pathname === "/hooks/stripe") { try { const hook = await requireWebhook(request, env, { provider: "stripe" }); const event = await request.json(); // hook.delivery is Stripe's event id. Skip one you've handled. return new Response(null, { status: 204 }); } catch (err) { if (err instanceof Unauthorized) return err.toResponse(); throw err; } } // requireUser for everything else },};provider is required: stripe, github, slack, shopify, standard_webhooks or hmac_sha256, or a list of them, like { provider: ["github", "stripe"] }. A delivery from any other provider is refused, and so is every delivery when the list is empty. It returns a Webhook:
interface Webhook { provider: "stripe" | "github" | "slack" | "shopify" | "standard_webhooks" | "hmac_sha256"; pattern: string; // the public path it came in on, like "/hooks/stripe" delivery: string | null; // the provider's signed id, null where it signs none signedAt: number | null; // unix seconds, null where the provider signs no time workspaceId: string;}The body arrives byte for byte as the provider sent it, so request.json() or your framework’s body parser reads it as usual.
Mount the route before anything that checks every request for a person, which would refuse the delivery first.
With Express
Section titled “With Express”import express from "express";import { requireUser, requireWebhook } from "@jiayang-cloud/sdk/express";
app.post("/hooks/stripe", express.json(), requireWebhook({ provider: "stripe" }), (req, res) => { // req.webhook, req.body res.sendStatus(204);});app.use(requireUser());requireWebhook() answers 401 unauthorized to anything but a verified delivery from the providers it names, and otherwise sets req.webhook.
With Hono
Section titled “With Hono”import { getWebhook, jiayang, webhook } from "@jiayang-cloud/sdk/hono";
app.post("/hooks/stripe", webhook({ provider: "stripe" }), async (c) => { const hook = getWebhook(c); const event = await c.req.json(); return c.body(null, 204);});app.use("*", jiayang());webhook() answers 401 itself, and reads the platform’s variables the way jiayang() does. getWebhook(c) throws if webhook() didn’t run on that route.
With Next.js
Section titled “With Next.js”In a route handler, such as app/hooks/stripe/route.ts:
import { Unauthorized } from "@jiayang-cloud/sdk";import { requireWebhook } from "@jiayang-cloud/sdk/next";
export async function POST(request: Request) { try { const hook = await requireWebhook({ provider: "stripe" }); } catch (err) { if (err instanceof Unauthorized) return err.toResponse(); throw err; } const event = await request.json(); return new Response(null, { status: 204 });}It reads the headers through next/headers, or from headers if you pass it: requireWebhook({ provider: "stripe", headers: request.headers }).
With node:http, Koa and others
Section titled “With node:http, Koa and others”import { envFromProcess, requireWebhookFrom } from "@jiayang-cloud/sdk/node";
const hook = await requireWebhookFrom(req, envFromProcess(), { provider: "github" });Like requireUserFrom, it refuses a token sent twice.
A token you already have
Section titled “A token you already have”verifyIdentity(token, env) runs the same checks on a token you took from somewhere else, such as a WebSocket message:
import { verifyIdentity } from "@jiayang-cloud/sdk";
const user = await verifyIdentity(token, env);verifyWebhook(token, env, { provider: "stripe" }) does the same for a webhook’s token.
Errors
Section titled “Errors”Every refusal is an Unauthorized (status 401) or a Forbidden (status 403). Both have .toResponse(), which answers unauthorized or forbidden: this needs editor as plain text with Cache-Control: no-store.
The message says why, for your logs:
err.message |
Why |
|---|---|
no identity token |
The request has no X-Jiayang-Identity header |
more than one identity token |
The headers given to /node or /express hold X-Jiayang-Identity as an array. Node’s own server never does this, so there two tokens give invalid identity token |
invalid identity token |
The token failed a check |
couldn't fetch the identity keys |
The key set couldn’t be fetched, and there’s no fresh copy |
JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL must be set |
The variables didn’t reach the SDK |
pass headers() in, or call this where next/headers can be imported |
/next was called where next/headers isn’t available |
this needs editor |
A Forbidden from requireRole(user, "editor") |
not a webhook this route takes |
From requireWebhook: a provider the route doesn’t name signed the delivery |
invalid webhook token |
From requireWebhook: the token’s webhook claims failed a check |
no provider named |
From requireWebhook: the call named no provider, so it refuses everything |
A person’s token given to requireWebhook, or a webhook’s given to requireUser, fails as invalid identity token: its audience is the other kind’s.
Don’t send the message to the caller. The 401 or 403 is all they need.
Run it locally
Section titled “Run it locally”jiayang dev puts the platform’s front door in front of your app on your machine, with real tokens. See Local development.
jiayang dev -- npm run dev