@danimo-hq/core
v0.2.0
Published
Danimo protocol core: OIDC discovery, PKCE, token exchange, refresh and JWT validation. Runtime-neutral (Web Crypto + fetch).
Readme
@danimo-hq/core
Protocol logic for signing in with Danimo: OIDC discovery, PKCE (S256), code exchange, single-flight refresh, revoke, and access/ID token validation. It uses only Web Crypto and fetch, so it runs in Node, Edge runtimes and React Native.
Most apps use a platform package such as @danimo-hq/next instead of this one.
pnpm add @danimo-hq/core
# npm install @danimo-hq/coreIssuer format: https://danimo.zentovar.space/e/<env_id>. Docs: https://danimo.zentovar.space/docs
import { createDanimoClient } from "@danimo-hq/core";
const danimo = createDanimoClient({
issuer: "https://danimo.zentovar.space/e/<env_id>",
clientId: "dnm_cl_...",
redirectUri: "https://app.example.com/callback",
});
// 1. Start: keep `transaction` safe (sealed cookie, secure storage) and send the user to `url`.
const { url, transaction } = await danimo.createAuthorization({ scopes: ["openid", "profile", "email", "offline_access"] });
// 2. Return: checks state, iss, exchanges the code with PKCE, validates the ID token (iss, aud, nonce, exp).
const { tokens, claims } = await danimo.handleCallback(callbackUrlOrParams, transaction);
// 3. Later: refresh (callers sharing a refresh token share one request), revoke, end the session.
const next = await danimo.refresh(tokens.refreshToken!);
await danimo.revoke(next.refreshToken!, "refresh_token");The SDK reads every endpoint from the issuer's discovery document and requires its issuer to equal the configured one exactly. It never builds Danimo URLs by hand.
Protecting an API
import { verifyAccessToken } from "@danimo-hq/core";
const claims = await verifyAccessToken(bearerToken, { issuer, audience: "https://api.example.com" });Checks the signature against the issuer's JWKS (ES256 only), iss, aud, exp and nbf (5 s clock tolerance), and typ: at+jwt. Failures throw a DanimoError whose code is one of token_expired, invalid_audience, invalid_issuer, invalid_signature, unknown_key, token_not_yet_valid or invalid_token. Access tokens are verified offline, so a revoked token is accepted until it expires (at most 10 minutes).
Refresh tokens rotate
Danimo treats a second use of a rotated refresh token as theft and ends the session. refresh() therefore shares one request between concurrent callers and hands a late caller that still holds the old token the same result for 30 seconds. This protects one process. If you run several instances, route refreshes for one user to one place or store tokens centrally.
Token storage
TokenStore is the interface platforms implement (cookies, Keychain, memory); getFreshTokens(store, refresh, ...) refreshes before expiry and clears the store when Danimo rejects the refresh token.
Webhooks
import { verifyWebhook } from "@danimo-hq/core";
// rawBody: the request body exactly as received (a string or bytes), before any JSON parsing.
const { timestamp } = await verifyWebhook(rawBody, request.headers.get("danimo-signature"), process.env.DANIMO_WEBHOOK_SECRET!, { toleranceSeconds: 300 });Checks Danimo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")> in constant time, accepts any one of several v1= values (a secret being rolled), and rejects a timestamp more than toleranceSeconds (default 300) from now. It throws invalid_webhook_signature (wrong secret, tampered body or header) or webhook_timestamp_expired (a genuine message that arrived too late, or a clock that is off). It uses Web Crypto HMAC, so it needs a server runtime. Answer the sender with a plain 400 either way, and use the event id to ignore repeats.
For SDK authors
createSessionController({ client, store }): the session logic@danimo-hq/reactand@danimo-hq/react-nativeshare. It holds the access token in memory, saves the refresh token to aRefreshTokenStore, refreshes single-flight before expiry, restores a session (refresh, then userinfo) and signs out (revoke, clear). State is{ status, user, session, error }and never contains a token.client.userInfo(accessToken)calls the discovered userinfo endpoint.verifyIdTokenSignature: falseoncreateDanimoClientskips the ID token signature (never its claims) for runtimes that cannot verify ES256, such as stock Hermes. Use it only for tokens received straight from the token endpoint over TLS.verifyAccessTokenaccepts a list of audiences for an API that serves several clients.
