@xrpl-commons/auth-sdk
v0.13.0
Published
XRP Account SDK for TypeScript apps (Nuxt, Vue, React, Next.js)
Downloads
1,104
Readme
@xrpl-commons/auth-sdk
SDK for integrating with the XRPL Commons Auth and Account platform.
Auth: OAuth 2.0 / OIDC onhttps://identity.xrpl.inAccount: shared profile and app data onhttps://profile.xrpl.in
This package supports both:
- consumer apps that want XRP login and Account reads
- downstream apps that want to integrate their own app-owned data into Account
Quick integration steps
Consumer app
- Register an OAuth client in XRP Profile admin with your exact callback URL.
- Configure your app with:
authBaseUrl: https://identity.xrpl.inaccountBaseUrl: https://profile.xrpl.in- your
clientId - your exact redirect URI
- Start login with PKCE and request only the app read scopes you need.
- Exchange the authorization code for tokens.
- Call
POST /account/user-infothroughAccountClientto read Account plus app-owned data.
Downstream app
- Keep your own business logic, role gating, and schema ownership.
- Implement
POST /api/account/user-info. - Validate the forwarded bearer token and resolve the user from XRP Identity.
- Return only your app-owned
datafields or a standardized relayerror. - Register the app's relay URL on the linked OAuth client in XRP Profile admin.
Install
bun add @xrpl-commons/auth-sdkOr:
npm install @xrpl-commons/auth-sdkWhat Account owns
Account is the single consumer-facing integration point.
Consumers call:
POST /account/user-infoAccount owns:
- auth and scope enforcement
- the outer request and response envelope
- fan-out to downstream apps
- partial success and per-app error handling
Downstream apps own:
- their own field list
- their own data semantics
- their own role gating
- their own
/api/account/user-infoimplementation
Core clients
import { createAuthClient, createAccountClient } from '@xrpl-commons/auth-sdk'
const auth = createAuthClient({
authBaseUrl: 'https://identity.xrpl.in',
clientId: 'my-client-id',
redirectUri: 'https://my-app.example.com/api/auth/xrpl-auth/callback',
defaultScopes: ['openid', 'email', 'profile']
})
const account = createAccountClient({
accountBaseUrl: 'https://profile.xrpl.in'
})Auth usage
const authorizationUrl = auth.getAuthorizationUrl({
state: '/dashboard',
apps: ['learn', 'glow'],
codeChallenge: '<pkce-code-challenge>',
codeChallengeMethod: 'S256'
})
const userinfo = await auth.getUserinfo('<access-token>')Server-side code exchange:
import { createServerAuthClient } from '@xrpl-commons/auth-sdk/server'
const serverAuth = createServerAuthClient({
authBaseUrl: 'https://identity.xrpl.in',
clientId: 'my-client-id',
clientSecret: 'my-client-secret',
redirectUri: 'https://my-app.example.com/api/auth/xrpl-auth/callback',
defaultScopes: ['openid', 'email', 'profile']
})Browser PKCE helpers
For pure browser apps or frontend-managed PKCE flows:
import {
beginBrowserPkceLogin,
completeBrowserPkceLogin,
createAuthClient
} from '@xrpl-commons/auth-sdk'
const auth = createAuthClient({
authBaseUrl: 'https://identity.xrpl.in',
clientId: 'my-public-client-id',
redirectUri: 'https://project-x.example.com/callback',
defaultScopes: ['openid', 'email', 'profile']
})
await beginBrowserPkceLogin(auth, {
returnUrl: '/dashboard',
apps: ['learn', 'glow', 'hackathon']
})On callback:
const params = new URLSearchParams(window.location.search)
const result = await completeBrowserPkceLogin(auth, {
code: params.get('code')!,
state: params.get('state')!
})
result.tokens.access_token
result.userinfo
result.session.returnUrlSession helpers:
import { loadBrowserAuthSession, clearBrowserAuthSession } from '@xrpl-commons/auth-sdk'Account user-info v1
Canonical consumer endpoint:
POST /account/user-infoRequest shape:
const response = await account.getUserInfo(
{
apps: {
items: [
{ appId: 'learn', fields: ['summary', 'progress'] },
{ appId: 'glow', fields: ['profile', 'roles', 'cohorts', 'judging'] },
{ appId: 'hackathon', fields: ['roles', 'teams', 'projects'] }
],
limit: 25
}
},
{ accessToken: '<access-token>' }
)Canonical request payload:
{
"apps": {
"items": [
{ "appId": "learn", "fields": ["summary", "progress"] },
{ "appId": "glow", "fields": ["profile", "roles", "cohorts", "judging"] },
{ "appId": "hackathon", "fields": ["roles", "teams", "projects"] }
],
"limit": 25
}
}Continuation request:
const nextPage = await account.getUserInfo(
{
apps: {
cursor: response.meta.nextAppsCursor
}
},
{ accessToken: '<access-token>' }
)Response:
response.account
response.apps
response.meta.contract // 'account.user-info.v1'
response.meta.requestedAppCount
response.meta.returnedAppCount
response.meta.nextAppsCursorCanonical response payload:
{
"account": {
"id": "69b1bca1763be50a6457b595",
"email": "[email protected]",
"emailVerified": true,
"status": "active"
},
"apps": [
{
"appId": "learn",
"label": "Learn",
"scope": "learn:read",
"access": { "mode": "scope" },
"data": {
"summary": {
"completedLessons": 1,
"currentStreak": 1,
"totalXp": 100
},
"progress": [
{
"resourceId": "lesson-blockchain-and-crypto-basics-what-is-a-blockchain",
"status": "completed",
"updatedAt": "2026-01-01T00:04:00.000Z"
}
]
}
},
{
"appId": "hackathon",
"label": "Hackathon",
"scope": "hackathon:read",
"access": { "mode": "scope" },
"error": {
"code": "user_not_linked",
"message": "This XRP Identity account is not linked to a Hackathon user."
}
}
],
"meta": {
"contract": "account.user-info.v1",
"requestedAppCount": 2,
"returnedAppCount": 2,
"nextAppsCursor": null
}
}App entries can return either data or error:
for (const app of response.apps) {
if (app.error) {
console.error(app.appId, app.error.code, app.error.message)
continue
}
console.log(app.appId, app.data)
}App request fields
These are example apps Identity currently hosts and their published fields — they are not enforced or defined by the SDK.
appIdandfieldsare plain strings; any app Identity exposes works without an SDK change. Field lists are owned by each app (discoverable via Identity), not shipped here.
Current app-owned public fields:
Learn
['summary', 'progress']Glow
['profile', 'roles', 'cohorts', 'nominations', 'judging', 'grants', 'admin']Hackathon
['roles', 'hackathons', 'teams', 'projects', 'judging', 'payouts', 'admin']These inner payloads are app-owned. Account does not define their business semantics.
Helper utilities
import {
appReadScope,
buildAppReadScopes,
mergeScopes
} from '@xrpl-commons/auth-sdk'
const scopes = mergeScopes(
['openid', 'email', 'profile'],
buildAppReadScopes(['learn', 'glow'])
)App access
App data is opaque to the SDK — appId is any string and data defaults to
Record<string, unknown>. Apps are discovered/relayed by Identity; the SDK does
not hardcode which apps exist. Supply your own type when you know an app's shape:
import { findAppData } from '@xrpl-commons/auth-sdk'
interface GlowData {
roles?: Record<string, boolean>
cohorts?: { id: string; name: string }[]
}
// works for built-in apps and ad-hoc ones alike (e.g. 'regul8')
const glow = findAppData<GlowData>(response, 'glow')
if (glow?.data) {
glow.data.roles
glow.data.cohorts
}Nuxt module
export default defineNuxtConfig({
modules: ['@xrpl-commons/auth-sdk'],
xrplAuth: {
authBaseUrl: 'https://identity.xrpl.in',
accountBaseUrl: 'https://profile.xrpl.in',
clientId: 'my-client-id',
redirectUri: 'https://my-app.example.com/api/auth/xrpl-auth/callback',
defaultScopes: ['openid', 'email', 'profile']
}
})const { login } = useXRPLAuth()
await login({ returnUrl: '/progress', apps: ['learn'] })const { getUserInfo, getAppData, findAppData, hasAppData } = useXRPLAccount()
// pass the fields you want; supply a type for the returned data if you have one
const learn = await getAppData('learn', { fields: ['summary', 'progress'] })Downstream app integration contract
If your app wants to expose app-owned data through XRP Account, implement:
POST /api/account/user-infoAccount calls it with:
Authorization: Bearer <user-access-token>
Content-Type: application/jsonRequest body:
{
"contract": "account.app-user-info.v1",
"appId": "glow",
"fields": ["profile", "roles", "cohorts"],
"context": {
"accountId": "507f1f77bcf86cd799439011"
}
}Minimal implementation steps:
- Accept only
POST. - Require
Authorization: Bearer <user-access-token>. - Validate:
contract === "account.app-user-info.v1"- your
appId - requested
fields
- Resolve the user from the bearer token, not from the request body alone.
- Build only the role-eligible fields for that user.
- Omit role-ineligible sections entirely.
- Return either a
dataobject or anerrorobject.
Rules:
contractmust beaccount.app-user-info.v1appIdmust match your appfieldsmust be validated against your app contract- the bearer token is the source of user identity
- role-ineligible fields must be omitted entirely
Success response:
{
"contract": "account.app-user-info.v1",
"appId": "glow",
"data": {
"profile": {},
"roles": {},
"cohorts": []
},
"meta": {
"servedAt": "2026-05-08T12:00:00.000Z"
}
}Error response:
{
"contract": "account.app-user-info.v1",
"appId": "glow",
"error": {
"code": "forbidden_field_access",
"message": "Requested fields are not available for this user role."
}
}Supported downstream error codes:
type RelayErrorCode =
| 'invalid_token'
| 'insufficient_scope'
| 'invalid_request'
| 'unknown_field'
| 'forbidden_field_access'
| 'user_not_linked'
| 'data_unavailable'
| 'temporarily_unavailable'
| 'internal_error'Architecture notes
- PKCE with
S256is mandatory for public browser flows. - Client secrets must never be used in browser code.
- Account is the only consumer-facing aggregator.
- Learn, Glow, Hackathon, and future apps own their own field visibility and role gating.
- Account may return partial success, so callers must handle per-app
errorentries.
