@idp.global/sdk
Reusable TypeScript SDK for building against idp.global from both sides of the wire: browser apps that need SSO, JWT housekeeping, transfer-token handoff, and typed IdP requests; and server apps that need explicit account storage, local password auth, optional idp.global password verification, and confidential OIDC membership introspection.
The package is intentionally split by runtime. Use @idp.global/sdk/browser in browser bundles and @idp.global/sdk/server in Node.js services. The root @idp.global/sdk export does not expose application APIs.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
What You Get
| Runtime | Import | What it does |
|---|---|---|
| Browser | @idp.global/sdk/browser |
SSO client, JWT and refresh-token storage, login redirects, logout, transfer-token exchange, and typed request shortcuts for user, organization, billing, OIDC, and admin flows. |
| Server | @idp.global/sdk/server |
NoSQL-backed account store, scrypt password hashing, local/idp.global account authentication, a server-side typed-socket client for idp.global password login, and confidential OIDC introspection. |
Install
pnpm add @idp.global/sdk
The server subpath additionally requires the peer dependency @lossless.org/client,
which provides the SmartdataDb the account store persists through:
pnpm add @lossless.org/client
This is an ESM TypeScript package. Published builds expose declaration files and runtime JavaScript through package exports.
Version 17 requires an IdP listener using TypedSocket 8 and TypedRequest 8
(TypedServer 11). Earlier listener and SDK transport majors cannot share the
WebSocket handshake. Upgrade the IdP app, its browser bundle, and services using
IdpGlobalServerClient together; this includes dcrouter's IdP password-login
flow. HTTP typed requests also use TypedRequest 8 response identities. The
confidential OIDC introspection client continues to use its separate HTTP API.
Browser SDK
Import the browser client from the browser subpath:
import { IdpClient } from '@idp.global/sdk/browser';
Create one client per app shell. The receptionBaseUrl is the typed RPC origin or full /typedrequest URL.
The production typed RPC surface is private at https://app.idp.global; public relying parties should use dynamic OIDC discovery at https://idp.global/.well-known/openid-configuration.
Use loginBaseUrl to send interactive login redirects to public https://login.idp.global.
const idp = new IdpClient(
'https://app.idp.global',
{ appUrl: 'https://app.example.com/' },
{ loginBaseUrl: 'https://login.idp.global' },
);
await idp.enableTypedSocket();
const loggedIn = await idp.determineLoginStatus(true);
if (loggedIn) {
const whoIsResult = await idp.whoIs();
console.log(whoIsResult.user);
}
When determineLoginStatus(true) cannot recover a valid JWT from browser storage, a refresh token, or a transfertoken query parameter, it redirects the user to the configured loginBaseUrl with the minimal { appUrl } transfer target encoded as transfertarget.
transport: 'websocket' is the default and serves the private application API.
The hosted login UI uses transport: 'http', which creates same-origin HTTP TypedRequests and does not open a WebSocket.
Both transports use the same typed request factories.
Token Handling
The browser client stores auth state in @push.rocks/webstore using the idpglobalStore store and main database.
| Method | Purpose |
|---|---|
setJwt(jwt) / getJwt() / deleteJwt() |
Manage the current JWT. |
setRefreshToken(token) / getRefreshToken() / deleteRefreshToken() |
Manage the refresh token. |
clearAuthState() |
Remove both JWT and refresh token. |
getJwtData() |
Decode the current JWT through @push.rocks/webjwt. |
performJwtHousekeeping() |
Refresh a JWT after its refreshFrom timestamp or recover from an expired JWT through the refresh token. |
refreshJwt(refreshToken?) |
Exchange a refresh token for a fresh JWT and update stored auth state. |
checkJwtPresent() |
Return whether a usable JWT can be found or refreshed. |
Login With Username And Password
The browser request wrappers are typed request factories.
Call enableTypedSocket() before firing them only when the client uses the default WebSocket transport.
await idp.enableTypedSocket();
const loginResponse = await idp.requests.loginWithUserNameAndPassword.fire({
username: 'developer@example.com',
password: 'correct horse battery staple',
});
if (loginResponse.twoFaNeeded) {
throw new Error('Two-factor authentication is required for this account.');
}
if (loginResponse.refreshToken) {
await idp.setRefreshToken(loginResponse.refreshToken);
await idp.refreshJwt(loginResponse.refreshToken);
}
Transfer Tokens
Transfer tokens are the handoff mechanism for moving a logged-in user between apps without exposing refresh tokens to URLs.
await idp.getTransferTokenAndSwitchToLocation();
On the receiving app, determineLoginStatus() calls processTransferToken() as part of its normal recovery flow. You can also call processTransferToken() directly when you own the route handling.
The SDK accepts only absolute, credential-free HTTPS transfer targets and uses
the same configured target for token exchange and navigation. The
transfertarget query value is a request, not proof of authorization: the IdP
must match it against an exact registered redirect target and bind it to the
single-use transfer token before issuing that token.
Logout
await idp.logout();
logout() clears local auth state and, when a refresh token is available on the IdP origin, asks idp.global to revoke the session before returning to the IdP origin. It never navigates to a URL-derived transfer target.
Browser API Surface
| API | Description |
|---|---|
new IdpClient(receptionBaseUrl, transferTarget?, options?) |
Configure the RPC URL, minimal { appUrl } return target, transport, and login base URL. |
createTypedRequest(method) |
Create a request over the configured HTTP or WebSocket transport. |
enableTypedSocket() / stop() |
Start the WebSocket transport, or stop the client and close any initialized browser auth-store connection. A later storage access reopens the persisted store. |
determineLoginStatus(requireLogin?) |
Resolve login state from JWT, refresh token, transfer token, or optional redirect. |
statusObservable |
RxJS subject that emits idp.global login status changes during refresh flows. |
whoIs() |
Resolve the current user through the active JWT. |
getRolesAndOrganizations() |
Fetch roles and organizations for the current user. |
createOrganization(name, slug, mode) |
Check organization slug availability or manifest an organization. |
updatePaddleCheckoutId(orgId, checkoutId) |
Send a Paddle checkout ID as the org payment method update. |
getTransferToken() |
Request a transfer token for the configured transfer target. |
getTransferTokenAndSwitchToLocation() |
Request a transfer token and navigate to the same configured target with transfertoken attached. |
processTransferToken() |
Consume a transfertoken from the current URL and refresh local auth state. |
logout() |
Revoke and clear the current login flow. |
Typed Request Shortcuts
idp.requests exposes typed request factories backed by @idp.global/interfaces. The request and response payload types come from that package, so your editor can guide exact payload shapes.
SDK 11 adopts interfaces v13. JWT verification-key distribution is keyset-only
and uses the plural getPublicKeysForValidation and
pushPublicKeysForValidation contracts. Transfer-token exchange accepts only
the minimal transferTarget: { appUrl } destination. OIDC app creation requires explicit client
type, grant types, callback and post-logout URIs, and token lifetime settings.
Public clients do not receive a client secret; confidential client secrets are
returned only once when created or rotated. The unsupported
client_credentials grant and API-token login are not exposed.
getUserActivity and exportUserActivity return structured activity events
with finite action, outcome, target-type, reason-code, and allowlisted-fact
fields; persistence IDs, free text, network data, integrity material, and sink
delivery state are not part of the SDK contract.
| Area | Request getters |
|---|---|
| Registration and login | firstRegistration, afterRegistrationEmailClicked, setData, finishRegistration, loginWithUserNameAndPassword, loginWithEmail, loginWithEmailAfterToken, resetPassword, setNewPassword, obtainDeviceId, attachDeviceId |
| Tokens and OIDC | obtainJwt, obtainOneTimeToken, prepareOidcAuthorization, completeOidcAuthorization |
| MFA and passkeys | getMfaStatus, startTotpEnrollment, finishTotpEnrollment, disableTotp, regenerateBackupCodes, verifyMfaChallenge, startPasskeyRegistration, finishPasskeyRegistration, revokePasskey, startPasskeyLogin, finishPasskeyLogin, startPasskeyMfa, finishPasskeyMfa |
| User profile and sessions | getUserSessions, revokeSession, getUserActivity, exportUserActivity, updateProfile, deleteAccount |
| Organizations, members, and apps | getOrganizationById, updateOrganization, deleteOrganization, exportOrgData, getOrgRoleDefinitions, upsertOrgRoleDefinition, deleteOrgRoleDefinition, createInvitation, getOrgInvitations, getOrgMembers, cancelInvitation, resendInvitation, removeMember, updateMemberRoles, transferOwnership, getInvitationByToken, acceptInvitation, bulkCreateInvitations, getGlobalApps, getAppConnections, toggleAppConnection, updateAppRoleMappings |
| Billing | getBillingPlan, getBillingPlansForOrganizationId, getPaddleConfig |
| Passport and alerts | createPassportEnrollmentChallenge, completePassportEnrollment, getPassportDevices, revokePassportDevice, createPassportChallenge, approvePassportChallenge, rejectPassportChallenge, registerPassportPushToken, listPendingPassportChallenges, getPassportChallengeByHint, markPassportChallengeSeen, getPassportDashboard, listPassportAlerts, getPassportAlertByHint, markPassportAlertSeen, dismissPassportAlert, upsertAlertRule, getAlertRules, deleteAlertRule |
| SSO and SCIM | getSsoConnections, createSsoConnection, updateSsoConnection, deleteSsoConnection, getSsoSpDetails, verifySsoDomain, createScimToken, getScimTokens, revokeScimToken, discoverSsoForEmail |
| Global administration | getGlobalUsers, getGlobalOrgStats, setUserSuspension, setGlobalCapabilityAssignments, deleteSuspendedUser, setOrganizationSuspension, checkGlobalAdmin, getGlobalAppStats, createGlobalApp, updateGlobalApp, deleteGlobalApp, regenerateAppCredentials, rotateOidcSigningKey, approveOidcSigningKeyRotation |
Server SDK
Import server-side primitives from the server subpath:
import {
AccountAuthService,
IdpGlobalServerClient,
SmartdataAccountStore,
} from '@idp.global/sdk/server';
The server SDK is deliberately explicit: it never auto-creates accounts during authentication. Your application decides when an account exists, which auth sources it may use, and whether it is an admin or user.
Account Store
SmartdataAccountStore persists accounts through @lossless.org/client/nosqldb
and the exported IdpSdkAccountDoc collection.
@lossless.org/client is a required peer dependency within major 1. The
SmartdataDb you pass must come from that package: the account document is
decorated by the very same module, so a SmartdataDb from any other package is
rejected at compile time and cannot resolve the model at runtime. Existing SDK
account documents, their id_1 identity index and their scrypt:v1 password
hashes retain their exact stored shape; this dependency migration does not
transform stored accounts.
import * as nosqldb from '@lossless.org/client/nosqldb';
import { SmartdataAccountStore } from '@idp.global/sdk/server';
const smartdataDb = new nosqldb.SmartdataDb({
mongoDbUrl: process.env.MONGODB_URL!,
mongoDbName: 'my-app',
});
await smartdataDb.init();
const accountStore = new SmartdataAccountStore({ smartdataDb });
if (!(await accountStore.hasActiveAdminAccount())) {
await accountStore.createAccount({
email: 'admin@example.com',
name: 'Admin User',
role: 'admin',
authSources: ['local'],
password: process.env.INITIAL_ADMIN_PASSWORD!,
});
}
Account emails are trimmed and normalized to lowercase for lookups. Local passwords are hashed with Node.js crypto.scrypt, a random salt, and timing-safe verification.
| Method | Purpose |
|---|---|
createAccount(options) |
Persist an explicitly created account. Requires a password when authSources includes local. |
getAccountByEmail(email) |
Find an account by normalized email. |
getAccountById(id) |
Find an account by UUID. |
listAccounts() |
Return all persisted accounts. |
hasActiveAdminAccount() |
Return whether at least one active admin account exists. |
verifyLocalPassword(account, password) |
Validate a local password for an active local account. |
updateLoginState(accountId, patch) |
Update lastLoginAt, updatedAt, and optionally idpSubject. |
normalizeEmail(email) |
Normalize an email exactly like the store does internally. |
Account Authentication
AccountAuthService authenticates only existing active accounts. It supports local passwords, idp.global passwords, or auto mode.
import {
AccountAuthService,
IdpGlobalServerClient,
SmartdataAccountStore,
} from '@idp.global/sdk/server';
const accountStore = new SmartdataAccountStore({ smartdataDb });
const idpClient = new IdpGlobalServerClient();
const authService = new AccountAuthService({
store: accountStore,
idpClient,
});
const authResult = await authService.authenticate({
email: 'admin@example.com',
password: 'correct horse battery staple',
authSource: 'auto',
});
if (!authResult) {
throw new Error('Invalid login.');
}
console.log(authResult.account.role, authResult.authSource);
In auto mode, the service tries local auth first when the account allows local; if that fails and the account allows idp.global, it uses the configured IdpGlobalServerClient. IdpGlobalServerClient defaults to the hosted https://app.idp.global TypedSocket endpoint; pass { baseUrl } only for self-hosted or staging IdP instances. Pass { backendToken } when a backend service should fetch JWT validation data or subscribe to key/blocklist pushes. idp.global authentication is accepted only when the returned user email matches the local account email and the returned user ID matches the stored idpSubject when one is already set.
Backend services register for JWT validation updates by constructing a server client with a backend token. The SDK authenticates every physical connection and reconnect through TypedSocket's bounded restoration callback before the connection becomes available:
const idpClient = new IdpGlobalServerClient({
baseUrl: process.env.IDP_URL,
backendToken: process.env.IDP_BACKEND_TOKEN,
});
idpClient.onPublicKeysPush((publicKeys) => {
// cache every current or retiring key by kid
});
idpClient.onBlocklistPush((blockedJwtIds) => {
// replace the local JWT ID blocklist with this complete persisted snapshot
});
const publicKeys = await idpClient.getPublicKeysForValidation();
const blockedJwtIds = await idpClient.getJwtIdBlocklist();
registerBackendService() remains available for an explicit registration. A
successful explicit token becomes the client's restoration state, so every
later physical reconnect authenticates with that token before the connection
becomes available. Both paths fire the exact registerBackendServiceConnection
request; clients never assign the protected backend-service role tag
themselves.
Verification is keyset-only. Replace the cached snapshot on every push, select
the verification key by the JWT header kid, and reject unknown key IDs.
Live Organization Authority
OidcIntrospectionClient calls the public issuer's /oauth/introspect endpoint
with confidential OAuth Basic authentication. Keep the client secret and access
token on the server. The token must belong to that same OAuth client and include
openid organizations; use the exact subject from the validated OIDC login and
the canonical organization ID.
import { OidcIntrospectionClient } from '@idp.global/sdk/server';
const introspection = new OidcIntrospectionClient({
issuer: 'https://idp.global',
clientId: process.env.IDP_OIDC_CLIENT_ID!,
clientSecret: process.env.IDP_OIDC_CLIENT_SECRET!,
});
const authority = await introspection.introspect({
token: serverSession.accessToken,
subject: serverSession.idpSubject,
organizationId: requestedOrganizationId,
});
if (!authority.active) throw new Error('Organization access denied.');
const currentRoles = authority.idp_global_authority.roles;
Every call generates a fresh nonce and validates the exact issuer, client,
subject, organization, response shape, revisions, and current roles. The default
request deadline is five seconds and the response limit is 16 KiB; redirects
are rejected. OidcIntrospectionError exposes only a sanitized code:
invalid_request, invalid_response, or unavailable. Deny the operation when
introspection fails.
An active result observes current authority while the provider holds its
mutation fences. It is not a cached membership grant: membership can change
after that observation. exp bounds token, session, and refresh-family expiry;
it does not promise membership remains valid until then. Keep the application's
existing login and principal binding and make a fresh observation for each
operation requiring live authority. The response contains no identity profile
or email-based organization inference.
Server-held OIDC UserInfo
Use OidcUserInfoClient to obtain current profile and organization picker data
with an access token held by your application's server session:
import { OidcUserInfoClient } from '@idp.global/sdk/server';
const userInfoClient = new OidcUserInfoClient({ issuer: 'https://idp.global' });
const claims = await userInfoClient.getUserInfo({
token: serverSession.accessToken,
subject: serverSession.idpSubject,
});
const ownerOrganizations = claims.organizations?.filter((organization) =>
organization.roles.includes('owner')) ?? [];
The expected subject must come from the verified OIDC login. Keep the token
server-side; the SDK sends it only in the bearer header to the issuer's
/oauth/userinfo endpoint, rejects redirects, and validates the returned subject
and documented claims. Unknown top-level OIDC extension claims are omitted.
The client retains no access token or claim cache. Calls have a five-second
deadline, including body reads, and a 64 KiB response limit. Options allow a
deadline up to 30 seconds and a response limit up to 1 MiB.
Organization claims are display data within the token's original organization
grant. Newly granted organizations require a new OIDC login; removed memberships
or app connections disappear from subsequent responses. Authorize each protected
operation separately with fresh OidcIntrospectionClient introspection. UserInfo
does not turn a picker selection or an observed role into an authorization grant.
OidcUserInfoError.code is invalid_request, invalid_response,
unauthenticated, or unavailable. Provider 401/403 responses indicate that the
server-held token is no longer accepted. Errors omit token and provider content.
Server API Surface
| API | Description |
|---|---|
SmartdataAccountStore |
NoSQL-backed account persistence and local password verification. |
AccountAuthService |
Existing-account authentication orchestration for local, idp.global, and auto. |
IdpGlobalServerClient |
Typed-socket client for idp.global password login, JWT refresh, whoIs lookup, backend JWT validation fetches, and JWT key/blocklist pushes. |
OidcIntrospectionClient |
Confidential server client for an exact live organization authority observation. |
OidcIntrospectionError |
Sanitized introspection failure with a stable error code. |
OidcUserInfoClient |
Subject-bound profile and organization display claims using a server-held OIDC token. |
OidcUserInfoError |
Sanitized UserInfo failure with a stable error code. |
IdpMfaRequiredError |
Password-login error carrying the MFA challenge token and available factor methods. |
defaultIdpGlobalBaseUrl |
Default hosted server origin: https://app.idp.global. |
PasswordHasher |
Static hashPassword() and verifyPassword() helpers using scrypt:v1. |
IdpSdkAccountDoc |
@lossless.org/client/nosqldb document class backing persisted SDK accounts. |
setAccountDocSmartdataDb() |
Configure the active SmartdataDb for IdpSdkAccountDoc. Usually handled by SmartdataAccountStore. |
Types
The server export includes these TypeScript types:
| Type | Values or purpose |
|---|---|
IOidcIntrospectionClientOptions |
Exact HTTPS issuer, confidential client credentials, and optional deadline/response limits. |
IOidcIntrospectionClientRequest |
Server-held access token, exact subject, and canonical organization ID. |
IOidcUserInfoClientOptions |
Exact HTTPS issuer and optional deadline/response limits. |
IOidcUserInfoClientRequest |
Server-held access token and the subject established by verified OIDC login. |
TIdpAccountAuthSource |
`'local' |
TIdpAccountRole |
`'admin' |
TIdpAccountStatus |
`'active' |
IIdpSdkAccount |
Persisted account shape. |
ICreateIdpSdkAccountOptions |
Input for account creation. |
IAuthenticateAccountOptions |
Input for AccountAuthService.authenticate(). |
IAuthenticatedAccountResult |
Successful auth result with account, auth source, and optional idp.global tokens. |
IIdpGlobalServerClientOptions |
Server client configuration with optional baseUrl and backendToken; defaults to https://app.idp.global. |
IIdpPasswordAuthResult |
Privacy-safe current-user DTO, JWT, and refresh-token result. |
Runtime Notes
Use the explicit subpath imports. They keep browser bundles free of Node-only code and keep server processes free of browser storage dependencies.
import { IdpClient } from '@idp.global/sdk/browser';
import { AccountAuthService } from '@idp.global/sdk/server';
The SDK uses typed sockets and typed requests from @api.global/*, shared idp.global request contracts from @idp.global/interfaces, NoSQL persistence from @lossless.org/client/nosqldb, and focused Push Rocks utilities for URL handling, JSON/base64 encoding, observables, JWT decoding, browser storage, and promise coordination.
Testing
pnpm test
The test suite covers the server account store and auth service, browser transfer-token handling, typed administrative request names, and the exact backend JWT keyset and blocklist contracts.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.