TypeScript/JavaScript OAuth client for Divine authentication and Nostr signing. It runs the OAuth 2.0 + PKCE authorization flow against a Divine login server, manages session storage and token refresh, and exposes a low-latency REST RPC client that mirrors NIP-46 signing methods without a relay round trip.
npm install @divinevideo/loginnostr-tools is an optional peer dependency. Install it only if you use the BYOK (bring your own key) flow, which derives a public key from an nsec:
npm install nostr-toolsThe package ships ESM, CommonJS, and IIFE (browser global) builds with TypeScript declarations.
import { createDivineClient } from '@divinevideo/login';
const client = createDivineClient({
serverUrl: 'https://login.divine.video',
clientId: 'my-app',
redirectUri: window.location.origin + '/callback',
storage: localStorage, // persist the session and PKCE verifier across reloads
});
// 1. Start the OAuth flow and redirect the browser to the login server.
const { url } = await client.oauth.getAuthorizationUrl();
window.location.href = url;
// 2. Back on your redirect URI, parse the callback and exchange the code.
const result = client.oauth.parseCallback(window.location.href);
if ('code' in result) {
const tokens = await client.oauth.exchangeCode(result.code);
// 3. If an access token was issued, use the RPC client to sign events.
const rpc = client.createRpc(tokens);
if (rpc) {
const pubkey = await rpc.getPublicKey();
const signed = await rpc.signEvent({
kind: 1,
content: 'Hello from Divine!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
// pubkey is filled in automatically when omitted
});
}
}When storage is provided, getAuthorizationUrl() persists the PKCE verifier so exchangeCode(code) can complete after a full-page redirect. Without storage, pass the verifier explicitly: exchangeCode(code, pkce.verifier) using the pkce returned from getAuthorizationUrl().
- OAuth 2.0 + PKCE — authorization-code flow with automatic S256 PKCE generation and verification.
- REST RPC signing — a low-latency alternative to NIP-46 relay-based signing; the method surface mirrors NIP-46.
- Session management — automatic token storage, expiry-aware refresh, and silent re-authentication via an authorization handle.
- Resilient RPC — 429 retry with exponential backoff and
Retry-Aftersupport, 30s request timeouts, and anonUnauthorizedhook that transparently refreshes an expired token and retries. - Cross-tab safe — concurrent refreshes are collapsed within an instance and serialized across same-origin tabs with the Web Locks API when available.
- BYOK — import an existing
nseckey. - Pluggable — swap the storage backend and
fetchimplementation; works withlocalStorage,sessionStorage, or a custom store.
import { createDivineClient } from '@divinevideo/login';
const client = createDivineClient({
serverUrl: 'https://login.divine.video', // Divine login server
clientId: 'my-app', // your OAuth client ID
redirectUri: 'https://my-app.example/callback',
storage: localStorage, // optional; defaults to in-memory
fetch: window.fetch, // optional custom fetch
});createDivineClient returns { oauth, createRpc }. oauth is a DivineOAuth instance; createRpc(tokens) builds a DivineRpc from a token response, or returns null when the response has no access_token.
After a successful exchangeCode, the server may return an authorization_handle, which is stored automatically. Subsequent calls to getAuthorizationUrl() reuse it to re-authenticate without a fresh user prompt. Pass authorizationHandle in the options to override the stored one.
Wire getSessionWithRefresh() into the RPC client's onUnauthorized hook so an expired access token is refreshed and the request retried transparently:
import { DivineRpc } from '@divinevideo/login';
const rpc = new DivineRpc({
nostrApi: 'https://login.divine.video/api/nostr',
accessToken: tokens.access_token!,
onUnauthorized: async () => {
const session = await client.oauth.getSessionWithRefresh();
if (!session?.accessToken) throw new Error('Session expired');
return session.accessToken;
},
});On a 401 the hook runs once, updates the bearer token, and retries. Concurrent 401s are coalesced into a single refresh.
const { url } = await client.oauth.getAuthorizationUrl({
nsec: 'nsec1...', // pubkey is derived automatically via nostr-tools
defaultRegister: true,
});Factory that pairs a DivineOAuth instance with a createRpc helper. config is a DivineClientConfig:
| Field | Type | Notes |
|---|---|---|
serverUrl |
string |
Divine login server, e.g. https://login.divine.video. |
clientId |
string |
Your OAuth client ID. |
redirectUri |
string |
OAuth callback URL. |
storage |
DivineStorage |
Optional. Defaults to an in-memory store. |
fetch |
typeof fetch |
Optional custom fetch. |
createRpc(tokens) returns a DivineRpc or null (when tokens.access_token is absent).
getAuthorizationUrl(options?)— build the authorization URL. Returns{ url, pkce }. Options:scopes?(defaults topolicy:full),nsec?(BYOK),defaultRegister?,authorizationHandle?.exchangeCode(code, verifier?)— exchange an authorization code for aTokenResponseand save the session. The verifier is optional when a stored/in-memory PKCE verifier is available.parseCallback(url)— extract the code from a callback URL. Returns{ code }or{ error, description? }.getSession()— read the stored session synchronously (no refresh). ReturnsStoredCredentials | null.getSessionWithRefresh()— read the session and refresh it if expired or within five minutes of expiry. ReturnsPromise<StoredCredentials | null>.refreshSession(refreshToken)— refresh explicitly; saves and returns the rotated credentials.getAuthorizationHandle()— the stored handle for silent re-auth, if any.isExpired(credentials)— whether stored credentials are past expiry.toStoredCredentials(response)— map aTokenResponsetoStoredCredentials.logout()— clear the session, authorization handle, and PKCE state.
Constructor options: nostrApi (the /api/nostr endpoint), accessToken, optional fetch, and optional onUnauthorized. Methods mirror NIP-46:
getPublicKey()— the user's hex public key (cached after the first call).signEvent(event)— sign an unsigned event;pubkeyis filled fromgetPublicKey()when omitted.nip44Encrypt(pubkey, plaintext)/nip44Decrypt(pubkey, ciphertext)nip04Encrypt(pubkey, plaintext)/nip04Decrypt(pubkey, ciphertext)— legacy.DivineRpc.fromServerUrl(serverUrl, accessToken)— static helper that derives the/api/nostrendpoint from a server URL.
Requests retry up to three times on HTTP 429 (honoring Retry-After) and time out after 30 seconds. A failed request throws RpcError, which carries the HTTP status.
generatePkce(nsec?)— generate a{ verifier, challenge }pair (S256).validatePkce(verifier, challenge, method?)— verify a challenge (S256by default).
DivineClientConfig, DivineStorage, TokenResponse, StoredCredentials, UnsignedEvent, SignedEvent, RpcRequest, RpcResponse, PkceChallenge, and OAuthError are exported for consumers.
- Storage. Any object implementing
getItem/setItem/removeItemworks. Session, authorization handle, PKCE verifier, and OAuth state are stored underdivine_*keys. - Scopes.
getAuthorizationUrldefaults to thepolicy:fullscope; override withoptions.scopes. - Token response.
TokenResponsealways includes abunker_url(NIP-46 bunker for remote signing);access_token,refresh_token,authorization_handle, andscopeare present when granted.
The IIFE build exposes a DivineLogin global and is served from unpkg and jsDelivr:
<script src="https://unpkg.com/@divinevideo/login/dist/index.global.js"></script>
<script>
const client = DivineLogin.createDivineClient({
serverUrl: 'https://login.divine.video',
clientId: 'my-app',
redirectUri: window.location.origin + '/callback',
});
</script>Bundlers can also import the browser build directly via the @divinevideo/login/browser export.
This package is the successor to keycast-login. Backward-compatible aliases keep old imports working:
// Old (still works)
import { createKeycastClient, KeycastOAuth, KeycastRpc } from '@divinevideo/login';
// New (preferred)
import { createDivineClient, DivineOAuth, DivineRpc } from '@divinevideo/login';Storage keys changed from keycast_* to divine_*, so existing sessions need to re-authenticate.
npm install
npm run build # bundle CJS, ESM, and IIFE with type declarations (tsup)
npm run dev # watch build
npm run lint # Biome checks on src/
npm test # Vitest run
npm run test:watch # Vitest watch modeContinuous integration runs lint, tests, and the build on Node 22.
MIT
Part of Divine — your playground for human creativity · Brand guidelines