Skip to content

SDK overview

Every request that reaches your app has already been through the platform’s sign-in, unless it came in on a public path. The edge adds a signed token saying who the caller is, and your app verifies it to learn their email and role. A webhook delivery on a verified public path gets a token of its own kind, saying which provider signed it: see Webhook deliveries.

The SDKs do the verifying in one call. They are open source (Apache-2.0) at github.com/ss2d22/jiayang.

Terminal window
npm install @jiayang-cloud/sdk

Workers, Node 20 or newer, Deno and Bun.

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}`);
} catch (err) {
if (err instanceof Unauthorized) return err.toResponse();
throw err;
}
},
};

Deploy it and call it as yourself:

Terminal window
jiayang curl acme/hello
hello someone@example.com
200 OK

Each language page covers the framework integrations: Node.js, Python, Go and Rust.

A verified caller has five fields. Each SDK spells them its own way.

Node.js Python Go Rust
Person or machine kind kind Kind kind
Stable id sub sub Sub sub
Email email email Email email
Role on this app role role Role role
Workspace id workspaceId workspace_id WorkspaceID workspace_id
  • kind is user for a person and service for a bypass token. A webhook delivery has a token of its own, which these calls refuse: see Webhook deliveries.
  • sub is the person’s platform id (usr_…), or service:<token id> for a token. Use it as the key when you store something per caller.
  • email is empty for a token: null in Node.js, None in Python and Rust, "" in Go.
  • role is viewer, editor or owner.

A script calling with a bypass token has no email. Code that prints user.email prints nothing useful for it:

Terminal window
JIAYANG_TOKEN=jyb_... jiayang curl acme/hello
hello null
200 OK

Fall back to the id when there is no email: user.email ?? user.sub.

Roles are ordered: viewer, then editor, then owner. An owner can do anything an editor can.

Every SDK has a check that answers yes or no, and one that fails with a 403 error:

Ask Refuse
Node.js hasRole(user, "editor") requireRole(user, "editor") throws Forbidden
Python has_role(user, "editor") require_role(user, "editor") raises Forbidden
Go jiayang.HasRole(user, jiayang.RoleEditor) jiayang.RequireRole(user, jiayang.RoleEditor) returns an error matching jiayang.ErrForbidden
Rust user.has_role(Role::Editor) user.require_role(Role::Editor) returns Err(Forbidden)

Answer with the right status. 401 means “I don’t know who you are”. 403 means “I know, and you may not do this”.

A role the SDK doesn’t know counts for nothing. If the platform adds a role one day, an app built with an older SDK refuses it rather than guessing what it allows. Asking for a role that doesn’t exist refuses everyone too.

The edge sends the token in the X-Jiayang-Identity header. It is a JWT signed with RS256 and lives for 60 seconds. The edge makes a new one for every request.

Decoded, a token looks like this:

{
"alg": "RS256",
"kid": "…",
"typ": "JWT"
}
{
"wid": "2a64ea1d-cebb-4356-89a1-52d01912cf20",
"role": "owner",
"kind": "user",
"email": "someone@example.com",
"iss": "https://edge.jiayang.cloud",
"aud": "adc2f06a-aec5-45e0-bea7-746ac5390091",
"sub": "usr_8xeuqtea67e3kkqpa0od9qhfvl",
"iat": 1790196735,
"exp": 1790196795,
"jti": "97369b6b-123d-4a16-acfe-9c07460bb259"
}
Claim What it is
iss The edge, https://edge.jiayang.cloud
aud Your app’s id. A token made out to another app fails here.
sub The caller’s stable id
kind user or service
email The person’s email. Absent for a token.
role Their role on this app
wid The app’s workspace id
iat, exp When it was made, and 60 seconds later
jti A random id for this token

The SDKs check, and refuse the request if any check fails:

  • the algorithm is RS256, whatever the token’s own header says
  • the token names a key (kid), and that key signed it
  • iss is JIAYANG_IDENTITY_ISSUER
  • aud is JIAYANG_APP_ID
  • exp hasn’t passed, with no clock leeway, and nbf (if present) has
  • iat, sub, kind, role and wid are there, and email too for a person
  • kind is user or service
  • no claim repeats one of those names in another case, like EXP

The keys come from JIAYANG_JWKS_URL. The SDKs cache them for five minutes. A token signed by a key they haven’t seen makes them fetch again, at most once every ten seconds. A fetch that takes more than three seconds fails, redirects aren’t followed, and RSA keys shorter than 2048 bits are ignored.

A delivery on a verified public path carries a token too, of kind webhook. The platform checked the provider’s signature before sending it on, so your app checks the platform’s token instead and never holds the provider’s secret. The token is addressed to webhook:<app id> rather than your app’s id, names the provider and the public path’s pattern, and has no email and no role. requireUser refuses it, and requireWebhook refuses everything else, a person’s token included.

import { requireWebhook, Unauthorized } from "@jiayang-cloud/sdk";
export default {
async fetch(request, env) {
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;
}
},
};

Each call names the providers the route takes, one or a list. A delivery from any other provider is refused, and so is every delivery when none is named. Each language’s page shows where the route goes in its frameworks, so that a check for people never sees the delivery first.

A verified delivery has five fields:

Node.js Python Go Rust
Provider provider provider Provider provider
Public path pattern pattern pattern Pattern pattern
Provider’s signed id delivery delivery Delivery delivery
When it was signed signedAt signed_at SignedAt signed_at
Workspace id workspaceId workspace_id WorkspaceID workspace_id
  • provider is stripe, github, slack, shopify, standard_webhooks or hmac_sha256.
  • pattern is the public path it came in on, such as /hooks/stripe or /hooks/*.
  • delivery is the provider’s id where its signature covers one: Stripe’s event id, Slack’s event_id, the Standard Webhooks message id. GitHub, Shopify and HMAC-SHA256 sign none, and it’s empty: null in Node.js, None in Python and Rust, "" in Go.
  • signedAt is when the provider signed it, for Stripe, Slack and Standard Webhooks: unix seconds, or a time.Time in Go. For the others it’s empty, like delivery, and a zero time.Time in Go.

For a webhook’s token the SDKs check the signature, iss and the expiry as they do for a person’s, and then:

  • aud is webhook: followed by JIAYANG_APP_ID, as one string
  • kind is webhook, and sub and wid are there
  • provider is one the SDK knows and the route names
  • pattern isn’t empty
  • delivery, if present, is a string that isn’t empty, and signed_at, if present, is a whole number of seconds

Public paths and webhooks lists the token’s claims.

The platform gives every app three variables:

Variable Value
JIAYANG_APP_ID Your app’s id, the only audience the SDK accepts
JIAYANG_IDENTITY_ISSUER https://edge.jiayang.cloud
JIAYANG_JWKS_URL https://edge.jiayang.cloud/.well-known/jwks.json

A Worker gets them as bindings, the env your fetch handler receives. A container gets them as environment variables. You can’t set or change them: jiayang env set refuses any name starting with JIAYANG_.

Your app can always fetch the key set. It is the one platform address an app’s outbound requests may reach.

Why a request with only X-Jiayang-Email is refused

Section titled “Why a request with only X-Jiayang-Email is refused”

The edge also sends X-Jiayang-Email, the caller’s email in plain text. It is there for display, like a log line or a greeting. It proves nothing.

The edge deletes every X-Jiayang-* header the client sent, but your app can’t tell from a header alone that this happened. A request reaching it some other way, or on a public path, can carry any header it likes. How access works has the full reasoning.

So the SDKs read only the signed token. A request with X-Jiayang-Email and no valid token gets 401, like a request with nothing.

If your language has no SDK, verify the token yourself with a JWT library:

  1. Read X-Jiayang-Identity. Refuse the request if it’s missing.
  2. Pin the algorithm to RS256. Don’t let the token’s header choose it.
  3. Fetch the key set from JIAYANG_JWKS_URL, and pick the key by the token’s kid.
  4. Check the signature, iss against JIAYANG_IDENTITY_ISSUER, aud against JIAYANG_APP_ID, and exp.
  5. Refuse on any failure, including a key set you couldn’t fetch.

On a route for a verified webhook, check the same way with one change: aud is webhook: followed by JIAYANG_APP_ID. Then check that kind is webhook and provider is the one the route takes. Never accept a webhook’s token where you expect a person, or a person’s where you expect a webhook: the audience keeps them apart, so check it exactly.

Never authorize on a header alone.

The SDKs have no development mode, and there is no setting that skips the signature check. To run your app locally with real tokens, use jiayang dev.