@empyre/relay-sdk
v1.0.0
Published
Relay OAuth and identity for AI agents: Continue with Relay, PKCE, scoped tokens, and Agent ID + Agent Secret authentication.
Maintainers
Readme
@empyre/relay-sdk
OAuth & identity for AI agents — the official SDK for Relay by Empyre.
- Relay Identity — mint an agent (any name, e.g.
claude@relay) and get its Agent ID + Agent Secret in one step. That pair is the agent's entire credential: exchange it for short-lived tokens at any app that supports Relay. - Relay Platform — add Continue with Relay (OAuth 2.1 + PKCE) so AI agents authenticate into your app through a secure consent flow.
Zero runtime dependencies. Works in Node 18+, Deno, Bun, browsers, and edge runtimes.
0.4 security contract:
authorizeUrl()requires high-entropystateand an S256 PKCE challenge. Callbacks must consume the stored state once and pass the matching verifier toexchangeCode().
Install
npm i @empyre/relay-sdkThe package name is
@empyre/relay-sdk(scoped).npm i empyre@relay-sdkwill fail with a 404 — that syntax asks npm for a package calledempyreat a version tag calledrelay-sdk.
Quick start — give an agent an identity
Create an agent at relay.empyre.dev/identity/dashboard (any name, renameable
any time). You get its Agent ID and Agent Secret right there — the
secret is shown once (rotate any time). Pick a permission mode at creation:
ask_critical— default; normal actions run, critical ones email you for one-tap approval.ask_everything— every token request waits for your email approval.full_access— the agent acts without asking; everything is still audited and revocable.
These snippets are JavaScript, not shell. Save them to a file (e.g.
agent.mjs) and runnode agent.mjs— pasting them straight into a terminal giveszsh: parse error.
# terminal
export RELAY_AGENT_ID="paste-agent-id"
export RELAY_AGENT_SECRET="ras_paste-agent-secret"
npm i @empyre/relay-sdk// agent.mjs — run with: node agent.mjs
import { RelayClient } from "@empyre/relay-sdk";
// Sign in by the app's DOMAIN — Relay resolves it to that app's client_id for
// you, so you never need the opaque relay_client_… id.
const relay = new RelayClient({ audience: "deelflow.dev" });
const result = await relay.authenticateAgent(
process.env.RELAY_AGENT_ID,
process.env.RELAY_AGENT_SECRET,
);
if ("status" in result && result.status === "pending_approval") {
// The owner gets a one-tap approval email. Retry after approval.
console.log(result.message);
} else {
console.log(result.access_token); // Bearer token the app verifies via Relay
}Which app am I signing in to? Pass its domain as
audience(e.g."deelflow.dev") and Relay looks up the client_id. If you already have the app'srelay_client_…id, you can pass it asclientIdinstead — both work. To fetch the id yourself:await relay.resolveClient("deelflow.dev"), orGET https://api.empyre.dev/relay/oauth/clients/resolve?domain=deelflow.dev.
Quick start — Continue with Relay (PKCE)
# terminal — both credentials, server-side only
export RELAY_CLIENT_ID="relay_client_..."
export RELAY_CLIENT_SECRET="relay_secret_..."// server-side JavaScript (e.g. relay.mjs) — not shell
import { RelayClient, createOAuthState, createPkcePair } from "@empyre/relay-sdk";
const relay = new RelayClient({
clientId: process.env.RELAY_CLIENT_ID,
clientSecret: process.env.RELAY_CLIENT_SECRET,
});
// 1. Send the user/agent to Relay to approve Relay sign-in/profile scopes.
const { verifier, challenge } = await createPkcePair();
const state = createOAuthState();
const url = relay.authorizeUrl({
redirectUri: "https://yourapp.com/callback",
scopes: ["openid", "profile"],
state,
codeChallenge: challenge,
});
session.relayOAuth = { state, verifier }; // server-side, bound to this browser
// → Redirect the browser to `url`.
// 2. On your callback, exchange the code for tokens.
const transaction = session.relayOAuth;
delete session.relayOAuth; // consume before validating so the callback cannot replay
if (!transaction || callbackState !== transaction.state) throw new Error("OAuth state mismatch");
const tokens = await relay.exchangeCode(code, transaction.verifier, "https://yourapp.com/callback");
// 3. Verify a token on each request.
const info = await relay.verifyToken(tokens.access_token);
if (!info.active) throw new Error("token revoked");Account linking — "Connect Relay" for existing users
The same OAuth flow powers two buttons:
- Continue with Relay — signup/login: no session yet; create or fetch the
user keyed by the introspected
sub. - Connect Relay — account linking: your user is already logged in (e.g. via
Google). From your settings page, send them through
authorizeUrl()with astatebound to their session; on the callback exchange the code, introspect the token, and attachinfo.subto the existing account. Never create a second user during a link, and reject the link if thatsubis already attached to a different account.
const tokens = await relay.exchangeCode(code, verifier, redirectUri);
const info = await relay.verifyToken(tokens.access_token);
await db.users.update(currentUser.id, { relay_identity_id: info.sub });CLI
export RELAY_CLIENT_ID="relay_client_..."
export RELAY_TOKEN="paste-token-here"
npx @empyre/relay-sdk authorize-url --redirect https://yourapp.com/callback --scopes openid,profile
npx @empyre/relay-sdk verify "$RELAY_TOKEN"
npx @empyre/relay-sdk revoke "$RELAY_TOKEN"
npx @empyre/relay-sdk agent-login "$RELAY_AGENT_ID" "$RELAY_AGENT_SECRET"Configure via env: RELAY_CLIENT_ID, RELAY_CLIENT_SECRET, RELAY_BASE_URL
(default https://api.empyre.dev), and RELAY_AUTHORIZE_URL (default
https://relay.empyre.dev/consent). For a local simulation, set the API and
consent endpoints independently:
export RELAY_BASE_URL=http://127.0.0.1:8000
export RELAY_AUTHORIZE_URL=http://relay.localhost:4173/consentAPI
RelayClient
| Method | Description |
| --- | --- |
| authorizeUrl(params) | Build the consent URL. Requires high-entropy state and an S256 codeChallenge. |
| exchangeCode(code, verifier, redirectUri) | Authorization code → tokens. |
| refresh(refreshToken) | Rotate an access token. |
| authenticateAgent(id, secret, scopes) | Relay Identity client-credentials grant. Returns tokens or a typed pending_approval result. Target the app via clientId or audience (its domain). |
| resolveClient(domain) | Resolve an app's domain → its public client_id. |
| verifyToken(token) | Introspect (RFC 7662). |
| revokeToken(token) | Revoke (RFC 7009). |
| createOAuthState() | Generate a high-entropy, URL-safe OAuth state value. |
| createPkcePair() | Generate a PKCE verifier + S256 challenge. |
The token endpoints target POST /relay/oauth/{token,introspect,revoke}. See the
Relay docs for the current OAuth surface.
MIT © Empyre
