JavaScript and TypeScript SDK
Install and use @aton-network/sdk.
Sign in to see your own test client ID in the samples.
Coming soon. @aton-network/sdk is not published on npm yet, so the install line on this page does not work until it is. Until then, everything works over plain HTTP: pick cURL in any code block, or start from Endpoints.
@aton-network/sdk is Sign in with Atheron for TypeScript and JavaScript: PKCE, the authorize address, the code swap, userinfo and webhook signatures. It uses Web Crypto only, so it runs in Node 20 and later, Deno, Bun, edge runtimes and browsers.
Install
npm install @aton-network/sdkSign somebody in
Build the address on your server, keep verifier, state and nonce for this browser in an HTTP-only cookie, and redirect.
# The authorize request is a page the person opens, not an API call. Build the address on your
# server: a fresh PKCE pair, state and nonce for this browser, then redirect them to it.
VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
STATE=$(openssl rand -hex 16)
echo "https://accounts.aton-network.org/oauth/authorize?response_type=code&client_id=$ATHERON_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"(no body: the browser follows the redirect)Handle the callback
finishSignIn checks state in constant time, then swaps the code. Send clientSecret from a server app; leave it out in a public app.
Check the ID token with a JOSE library (for example jwtVerify with createRemoteJWKSet from jose) against https://accounts.aton-network.org/.well-known/jwks.json: signature, issuer, audience, expiry and nonce. This package does not reimplement signature checks.
# Swap the code from the redirect for tokens, on your server. HTTP Basic carries your client ID and
# secret; the verifier proves this is the browser that started the sign-in.
curl https://accounts.aton-network.org/oauth/token \
-u "$ATHERON_CLIENT_ID:$ATHERON_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://app.example.com/callback \
-d code_verifier="$VERIFIER"{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email offline_access",
"id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYtMTAtMDEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmF0b24tbmV0d29yay5vcmciLCJzdWIiOiI5YTZk...",
"refresh_token": "ath_rt_6Qm9..."
}Read who it is now
# Who the person is as of now, with an access token issued to your client.
curl https://accounts.aton-network.org/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"sub": "9a6d4c1e-1f0b-4a51-8a8e-3b2f6c0d7e11",
"name": "Ada Lovelace",
"email": "ada@example.com",
"email_verified": true,
"age": { "verified": true, "adult": true, "sixteen_or_over": true }
}Check a webhook
// Check that a webhook came from Atheron before you act on it. Pass the raw body, exactly as received.
import { verifyWebhook, type WebhookEvent } from "@aton-network/sdk";
export async function handleWebhook(request: Request): Promise<Response> {
const raw = await request.text();
const ok = await verifyWebhook(request.headers.get("atheron-signature"), raw, process.env.ATHERON_WEBHOOK_SECRET!);
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(raw) as WebhookEvent;
if (event.type === "consent.revoked") {
// Delete the refresh token you hold for event.data.sub, and stop using what you read.
}
return new Response(null, { status: 204 });
}GraphQL
AtheronGraphQL runs typed queries: query(UsageQuery, { days: 30 }) answers usage with its type, and gql<T>("query ...") types your own. The documents the SDK ships (the App, Usage, Overview and Events documents the GraphQL reference publishes) go as their SHA-256 alone, so the server knows them from the first request; your own go in full unless you turn persisted queries on. query() throws an AtheronGraphQLError when any field failed, with the data that could be read on it; request() answers the data and the field errors side by side. events() and subscribe() run over the WebSocket (graphql-transport-ws), reconnect when the connection drops, resume after the last event handed on and stop on a refusal waiting cannot fix. Node 22 and browsers have WebSocket; on Node 20, pass { WebSocket } from the ws package.
// Your app and its usage in one GraphQL request, then its events as they happen over the subscription.
// Node 22 has a global WebSocket; on Node 20, pass { WebSocket } from the ws package.
import { AtheronGraphQL, UsageQuery } from "@aton-network/sdk";
export async function usageThisMonth(apiKey: string): Promise<number> {
const { usage } = await new AtheronGraphQL({ apiKey }).query(UsageQuery, { days: 30 });
return usage ? usage.usage.reduce((sum, day) => sum + day.calls, 0) : 0;
}
export async function watchConsent(apiKey: string, lastId: string | null, forget: (sub: string | undefined) => void, signal?: AbortSignal): Promise<string | null> {
for await (const event of new AtheronGraphQL({ apiKey }).events({ types: ["consent.revoked"], lastEventId: lastId, ...(signal ? { signal } : {}) })) {
forget(event.data.sub);
lastId = event.id; // keep it somewhere durable, to pass back after a restart
}
return lastId;
}Errors
Every failure from our endpoints is thrown as AtheronError, with code (the OAuth error, for example invalid_grant), description and the HTTP status. See Errors for every code.
Reference
| Export | What it does |
|---|---|
ISSUER | https://accounts.aton-network.org |
Scope | The scope names the provider issues |
pkcePair() | A verifier and its S256 challenge |
startSignIn({ clientId, redirectUri, scopes?, issuer?, prompt? }) | The authorize address and the verifier, state and nonce to keep |
finishSignIn({ callbackUrl, kept, clientId, clientSecret?, redirectUri, issuer? }) | Checks state and swaps the code for Tokens |
userinfo(accessToken, issuer?) | The claims as of now |
refreshTokens({ clientId, clientSecret?, refreshToken }), revokeToken({ clientId, clientSecret?, token, hint? }) | New tokens from a refresh token; a token revoked |
idTokenClaims(idToken, { clientId, nonce }) | An ID token from the token endpoint, held to issuer, audience, expiry and nonce |
verifyWebhook(header, rawBody, secret or [secrets], { toleranceSeconds?, now? }) | Whether a delivery is ours and fresh; two secrets while you rotate |
constructEvent(header, rawBody, secret) | The checked event, or invalid_signature |
followEvents({ apiKey, types?, lastEventId?, signal? }) | The events stream as an async iterator: reconnects, resumes after the last id, stops on a refusal waiting cannot fix |
new AtheronGraphQL({ apiKey, baseUrl?, WebSocket?, persistedQueries? }) | GraphQL: query(doc, vars), request(doc, vars), subscribe(doc, vars, options), events({ types?, lastEventId?, signal? }) |
AppQuery, UsageQuery, OverviewQuery, EventsSubscription, gql<T>(document) | The published documents, typed, and your own |
createAtheron({ clientId, clientSecret?, redirectUri, secret }) | A web server's whole sign-in on Request and Response, with a sealed session cookie |
AtheronApi({ apiKey }) | The developer API (app(), usage({ days })), generated from the OpenAPI description |
AtheronError | What every call throws, with code, description, status and, on a 429, retryAfter |
Tokens, Started, WebhookEvent, Session | The shapes of what comes back |
Frameworks
Next.js, React, Vue and Nuxt, SvelteKit, Angular, Express, NestJS, React Router and Remix, Auth.js, Passport, Node and the browser, and the atheron CLI.
Ask Atlas
AI assistantAsk anything about Sign in with Atheron, the API or the portal. You get an answer from our docs, with links to the pages it used.