@startdoing/identity-sdk
v0.1.33
Published
TypeScript SDK for startdoing identity-service
Downloads
817
Maintainers
Readme
@startdoing/identity-sdk
TypeScript client for startdoing Identity Service: OAuth 2.0 Authorization
Code flow with PKCE (browser-safe), token exchange + refresh-with-rotation,
/oauth/userinfo, /oauth/revoke, /oauth/introspect, OIDC
discovery + JWKS, and optional first-party /auth/* cookie session
helpers.
Ecosystem apps should use this package instead of hand-rolling HTTP against the IdP so URLs, JSON fields, PKCE, and error handling stay aligned with the server.
| More reading (repo) | |
| --- | --- |
| OAuth flow, prerequisites, security | docs/integration-guide.md |
| Raw HTTP contracts | docs/api-reference.md |
| Publish / build this package (maintainers) | docs/sdk.md |
Entry point: src/index.ts.
Install
npm install @startdoing/identity-sdkRequires a global fetch (Node 18+, Bun, modern browsers). Pass a custom fetchFn if you need mocks or a polyfill.
Choose how your app talks to the IdP
| Approach | When to use | Secrets | Typical host |
| --- | --- | --- | --- |
| Confidential OAuth client + your backend | Production SSR apps, third-party–style clients, SPAs with a server callback | clientSecret only on the server | API routes / BFF that hold the secret and perform exchangeAuthorizationCode / refreshAccessToken |
| Public OAuth client (PKCE-only) | SPA / mobile / native clients with no server-held secret | None — PKCE S256 mandatory | Browser / device that performs PKCE end-to-end |
| First-party session | Browser app that shares IdP cookies (same eTLD, or SameSite=None IdP cookie + CORS + credentials) | No client secret; user password only in your trusted UI calling the IdP | Browser |
Do not put clientSecret in browser JavaScript for SPA bundles —
register a public client instead.
IdentityServiceClient
import { IdentityServiceClient } from "@startdoing/identity-sdk";
const idp = new IdentityServiceClient({
baseUrl: "https://idp.example.com", // no trailing slash required; normalized
clientId: process.env.IDENTITY_CLIENT_ID,
clientSecret: process.env.IDENTITY_CLIENT_SECRET, // omit in browser-only first-party usage
fetchFn: optionalCustomFetch,
});Constructor options
| Option | Required | Description |
| --- | --- | --- |
| baseUrl | yes | Identity Service origin (e.g. https://idp.example.com). |
| clientId | for OAuth URL + token calls | Default client_id. Overridable per call. |
| clientSecret | confidential clients | Default client_secret for token endpoints. Omit for public (SPA/mobile) clients. |
| tokenEndpointAuthMethod | no | "client_secret_basic" (default for confidential), "client_secret_post", or "none" (auto-selected for public clients). |
| fetchFn | no | Defaults to global fetch. |
If a required field is missing the client throws before the HTTP request.
OAuth: authorize → callback → tokens → userinfo
1. Start login (PKCE + state + optional nonce / scope)
Generate a PKCE pair, a random state, and (for OIDC) a random nonce.
Persist the verifier, state, and nonce for the callback.
import {
IdentityServiceClient,
createPkcePair,
createRandomState,
} from "@startdoing/identity-sdk";
const idp = new IdentityServiceClient({
baseUrl: process.env.IDENTITY_BASE_URL!,
clientId: process.env.IDENTITY_CLIENT_ID!,
clientSecret: process.env.IDENTITY_CLIENT_SECRET, // omit for public clients
});
const { verifier, challenge } = await createPkcePair();
const state = createRandomState();
const nonce = createRandomState();
const authorizeUrl = idp.createAuthorizeUrl({
redirectUri: process.env.IDENTITY_REDIRECT_URI!,
state,
nonce,
scope: "openid email profile",
codeChallenge: challenge,
});2. Callback: exchange code for tokens
After validating state, call:
const tokens = await idp.exchangeAuthorizationCode({
code: callbackCode,
redirectUri: process.env.IDENTITY_REDIRECT_URI!,
codeVerifier: verifier, // required if you sent code_challenge on authorize
});
// tokens: access_token, refresh_token, id_token?, token_type, expires_in3. Load user profile
const user = await idp.getUserInfo(tokens.access_token);
// user: { sub, email }4. Refresh access token
Refresh tokens are rotated: each success returns a new refresh_token;
replace the stored value and stop using the old one. Re-presenting an
already-rotated token revokes the entire refresh-token family server-side.
const refreshed = await idp.refreshAccessToken({
refreshToken: storedRefreshToken,
});5. Revoke + introspect
await idp.revokeToken({
token: storedRefreshToken,
tokenTypeHint: "refresh_token",
});
const meta = await idp.introspectToken({ token: accessToken });
if (!meta.active) {
// token unusable
}revokeToken always resolves successfully (RFC 7009); use it as part of
your logout flow. introspectToken requires confidential client credentials.
6. Discovery + JWKS (verify ID tokens out-of-band)
const meta = await idp.fetchDiscovery();
const jwks = await idp.fetchJwks();Use these to drive a JOSE library that verifies ID tokens against the
published RS256 keys (kid header → JWKS lookup).
Per-call OAuth credentials
You can set clientId and clientSecret only on exchangeAuthorizationCode / refreshAccessToken if the client was constructed without defaults:
await idp.exchangeAuthorizationCode({
code,
redirectUri,
codeVerifier,
clientId: "...",
clientSecret: "...",
});First-party session (cookie flow)
These call /auth/register, /auth/login, /auth/user, /auth/logout with credentials: "include". The IdP must allow your app origin in CORS (CORS_ORIGINS) and you must call from a context that sends cookies (browser, same-site or configured cross-site cookies).
const idp = new IdentityServiceClient({ baseUrl: "https://idp.example.com" });
await idp.register("[email protected]", "SecurePassword123");
await idp.login("[email protected]", "SecurePassword123");
const me = await idp.getSessionUser();
await idp.logout();This path does not use clientSecret. It is not a substitute for OAuth when you need delegated access for a server-side app that does not share the IdP session cookie.
PKCE helpers
All helpers are browser-safe (Web Crypto with a node:crypto fallback).
| Function | Purpose |
| --- | --- |
| createPkceVerifier(length?) | Random verifier string. |
| createPkceChallengeS256(verifier) | code_challenge for S256 (async). |
| createPkcePair() | Convenience: { verifier, challenge } (async). |
| createRandomState(bytes?) | Random state / nonce parameter. |
Errors
Non-2xx responses throw IdentityServiceError (extends Error):
| Property | Meaning |
| --- | --- |
| status | HTTP status. |
| code | Server error field when present (e.g. OAuth error code). |
| requestId | From JSON request_id or X-Request-Id header. |
| body | Parsed response body (JSON or fallback). |
import { IdentityServiceError } from "@startdoing/identity-sdk";
try {
await idp.refreshAccessToken({ refreshToken });
} catch (err) {
if (err instanceof IdentityServiceError) {
console.error(err.status, err.code, err.requestId);
}
throw err;
}Exported types
AuthorizeUrlParams, ExchangeCodeParams, RefreshTokenParams,
RevokeTokenParams, IntrospectTokenParams, IdentityServiceClientOptions,
OAuthTokenResponse, UserInfoResponse, IntrospectionResponse,
DiscoveryDocument, RegisterResponse, LoginResponse, LogoutResponse,
IdentityServiceError.
Environment variables (consuming apps)
Typical server-side OAuth setup:
IDENTITY_BASE_URL— IdP originIDENTITY_CLIENT_IDIDENTITY_CLIENT_SECRETIDENTITY_REDIRECT_URI— must match the registered redirect URI exactly
Security checklist
- Keep
clientSecreton the server. Use a public client (no secret) with PKCE for SPAs. - Perform code exchange and refresh on trusted infrastructure.
- Validate
stateon the OAuth callback before exchanging the code. - For OIDC, validate
nonceon the returnedid_token. - Store refresh tokens securely; rotation is mandatory and the entire family is revoked on reuse.
- Prefer HTTPS for
baseUrland redirect URIs in production.
AI coding agents (TanStack Intent)
This package includes TanStack Intent skills under skills/ (shipped in the npm package). To wire them into your editor or agent, run npx @tanstack/intent@latest install. To list skills from the installed package, run npx @tanstack/intent@latest list.
Monorepo: develop and publish
From the repository root: build with bun run sdk:build, test local tarballs with bun run sdk:pack, publishing is described in docs/sdk.md.
