@cyberia-auth/auth
v0.1.18
Published
Cyberia Auth React components and provider
Maintainers
Readme
@cyberia-auth/auth
React auth SDK for Cyberia Auth.
It includes:
- Full-page auth —
CyberiaAuth(sign-in / sign-up, email + OAuth). - Provider + session —
CyberiaAuthProvider,useCyberiaAuth()(modal auth, OAuth return handling, optional embedded RBAC whenapiTokenis set). - Workspace invites —
CyberiaWorkspaceInvitationfor Auth Admin URLs such as/invite?invite_status=ready&invite_token=.... - Route / layout guards —
Protect,SignedIn,SignedOut,Show. - Chrome —
UserButton,SignInButton,SignUpButton,SignOutButton. - Permission helpers (re-exported) —
hasEmbeddedAppPermission,normalizeEmbeddedMatrix,DEFAULT_EMBEDDED_MATRIX,DEFAULT_EMBEDDED_RESOURCE_CATALOG, and types for building your own settings UI or tests. - App user management (admin API) —
CyberiaUserSettings/UserSettings— list end users, pending approvals, add/remove memberships for one app.
Installation
npm install @cyberia-auth/authPeer dependencies:
react^18 || ^19react-dom^18 || ^19
Components and exports (quick map)
| Export | Type | When to use |
|--------|------|-------------|
| CyberiaAuthProvider | Provider | Wrap the app; holds session, theme, optional embedded matrix from public config. |
| useCyberiaAuth | Hook | Read session, open modals, signOut, canAppEmbedded, setAppOperatorRole, … |
| CyberiaAuth | Page / modal body | Full login/register card (used inside the provider modal or on a dedicated /login route). |
| CyberiaWorkspaceInvitation | Page | Auth Admin “accept workspace invite” flow. |
| Protect | Guard | Render children or fallback by sign-in state (e.g. React Router). |
| SignedIn / SignedOut | Guard | Render children only when signed in or signed out. |
| Show | Guard | Same idea with an explicit when="signed-in" \| "signed-out". |
| UserButton | UI | Avatar menu + manage-account shell. |
| SignInButton / SignUpButton / SignOutButton | UI | Trigger modals or sign-out. |
| hasEmbeddedAppPermission, … | Pure helpers | Unit tests or server-free UI previews of permission matrices. |
| CyberiaUserSettings / UserSettings | Page | apiToken + user JWT (userToken or adminToken / localStorage — user type is detected) or admin JWT + apiToken / app picker. |
Environment variables example
VITE_CYBERIA_APP_TOKEN=app_your_token_here
VITE_CYBERIA_OAUTH_REDIRECT=https://your-app-domain.com/protectedQuick start (provider + route guard)
import {
CyberiaAuthProvider,
SignedIn,
SignedOut,
SignInButton,
SignUpButton,
UserButton,
Protect,
} from '@cyberia-auth/auth';
const backendUrl = import.meta.env.VITE_CYBERIA_BACKEND_URL;
const apiToken = import.meta.env.VITE_CYBERIA_APP_TOKEN;
const oauthRedirectUri = import.meta.env.VITE_CYBERIA_OAUTH_REDIRECT;
function App() {
return (
<CyberiaAuthProvider
backendUrl={backendUrl}
apiToken={apiToken}
oauthRedirectUri={oauthRedirectUri}
theme="system"
>
<header style={{ display: 'flex', gap: 8 }}>
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
<Protect fallback={<p>Please sign in.</p>}>
<div>Protected content</div>
</Protect>
</CyberiaAuthProvider>
);
}Embedded page example (CyberiaAuth)
Use this when you want a full login page instead of only modal buttons.
import { type AuthResult, CyberiaAuth, useCyberiaAuth } from '@cyberia-auth/auth';
import { useNavigate } from 'react-router-dom';
const API_TOKEN = import.meta.env.VITE_CYBERIA_APP_TOKEN;
export default function LoginPage() {
const navigate = useNavigate();
const { setSessionFromAuth } = useCyberiaAuth();
return (
<CyberiaAuth
apiToken={API_TOKEN}
oauthRedirectUri={window.location.origin}
initialMode="login"
theme="system"
onAuthSuccess={(result: AuthResult) => {
setSessionFromAuth(result);
navigate('/', { replace: true });
}}
/>
);
}SignedIn / SignedOut example
import { SignedIn, SignedOut, SignInButton, UserButton } from '@cyberia-auth/auth';
function HeaderActions() {
return (
<>
<SignedOut>
<SignInButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</>
);
}Show example (explicit when)
Show is equivalent to SignedIn / SignedOut but accepts a single when prop (useful when the condition is chosen at runtime).
import { Show, UserButton, SignInButton } from '@cyberia-auth/auth';
function Toolbar({ preferAccountMenu }: { preferAccountMenu: boolean }) {
return (
<>
<Show when="signed-out">
<SignInButton />
</Show>
<Show when="signed-in">
{preferAccountMenu ? <UserButton /> : <span>Welcome</span>}
</Show>
</>
);
}Embedded app permissions (canAppEmbedded)
For consumer apps that use the same apiToken as your Cyberia application, the provider loads GET /api/public/app/config and exposes:
embeddedRolePermissionMatrix— effective CRUD matrix merged from server defaults + Admin overrides.embeddedPermissionResources— display metadata (id,label,description) forend_users,invitations,app_settings.appOperatorRole/setAppOperatorRole(role)— role key for the current operator (e.g. staff member). Must match keys in the matrix (owner,admin,collaborator, or custom keys you configure in Cyberia Auth Admin).defaultAppOperatorRole— optional prop onCyberiaAuthProvider(defaultcollaborator) until you callsetAppOperatorRole.canAppEmbedded(resource, action)—resource:'end_users' | 'invitations' | 'app_settings';action:'read' | 'create' | 'update' | 'delete'.
If apiToken is omitted, embeddedRolePermissionMatrix is null and canAppEmbedded returns true for everything (no client-side gating).
Provider + staff role from your API
import { useEffect } from 'react';
import {
CyberiaAuthProvider,
useCyberiaAuth,
} from '@cyberia-auth/auth';
const backendUrl = import.meta.env.VITE_CYBERIA_BACKEND_URL;
const apiToken = import.meta.env.VITE_CYBERIA_APP_TOKEN;
function App() {
return (
<CyberiaAuthProvider
backendUrl={backendUrl}
apiToken={apiToken}
oauthRedirectUri={window.location.origin}
defaultAppOperatorRole="collaborator"
>
<StaffRoleSync />
<YourRoutes />
</CyberiaAuthProvider>
);
}
/** Map your signed-in staff profile to the role keys configured in Cyberia Auth Admin (embedded matrix). */
function StaffRoleSync() {
const { setAppOperatorRole } = useCyberiaAuth();
useEffect(() => {
const roleKey = getStaffEmbeddedRoleKeyFromYourApp();
if (roleKey) {
setAppOperatorRole(roleKey);
}
}, [setAppOperatorRole]);
return null;
}
function getStaffEmbeddedRoleKeyFromYourApp(): string | null {
// Example: read from your JWT, session store, or GET /api/me — must match matrix keys (e.g. admin, collaborator).
return null;
}Gating buttons and routes
import { useCyberiaAuth } from '@cyberia-auth/auth';
function InvitationsCard() {
const { canAppEmbedded } = useCyberiaAuth();
return (
<section>
<h2>Invitations</h2>
{canAppEmbedded('invitations', 'read') && <InvitationList />}
{canAppEmbedded('invitations', 'create') && (
<button type="button">Invite user</button>
)}
{canAppEmbedded('invitations', 'delete') && (
<button type="button">Revoke selected</button>
)}
</section>
);
}Labels for a “Roles & permissions” settings screen
import { useCyberiaAuth } from '@cyberia-auth/auth';
function PermissionLegend() {
const { embeddedPermissionResources } = useCyberiaAuth();
return (
<ul>
{embeddedPermissionResources.map((r) => (
<li key={r.id}>
<strong>{r.label}</strong> — {r.description}
</li>
))}
</ul>
);
}Testing and previews without the provider
import {
hasEmbeddedAppPermission,
normalizeEmbeddedMatrix,
type EmbeddedRolePermissionMatrix,
} from '@cyberia-auth/auth';
const matrix: EmbeddedRolePermissionMatrix = normalizeEmbeddedMatrix({
collaborator: { invitations: { delete: true } },
});
const canDelete = hasEmbeddedAppPermission(matrix, 'collaborator', 'invitations', 'delete');Configure matrices in Cyberia Auth Admin: organization Role permissions (workspace) vs application Embedded role permissions (this SDK reads the app matrix from public config for your apiToken).
App user management (CyberiaUserSettings / UserSettings)
Two ways to authenticate:
- App user session (recommended for product UIs) — pass
userToken={token}fromuseCyberiaAuth()(atype: userJWT) plusapiToken. Calls/api/public/app/app-users?apiToken=…with your user session. Workspace admins must enablecanManageAppUserson your app membership (PATCH /api/admin/app-users/:idin Cyberia). - Workspace admin — pass
adminToken(type: adminJWT) and eitherapiToken(resolves app viaGET /v1/environment) orappId/ app dropdown.
If apiToken is set and the bearer you pass as adminToken (or via tokenFromLocalStorage) is a type: user JWT, the component uses the same public /api/public/app/app-users flow as userToken — so a product page can pass the sign-in session token as adminToken without getting 401 on /api/admin/app-users.
Membership status: active, pending_approval, unregistered.
With useCyberiaAuth() + application token (same session as your app)
import { CyberiaAuthProvider, CyberiaUserSettings, useCyberiaAuth } from '@cyberia-auth/auth';
const APP_TOKEN = import.meta.env.VITE_CYBERIA_APP_TOKEN;
function UserAdminPage() {
const { token } = useCyberiaAuth();
return (
<CyberiaUserSettings
userToken={token ?? ''}
apiToken={APP_TOKEN}
layout="tabs"
theme="system"
branding={{ primaryColor: '#c91111', appName: 'Member management' }}
/>
);
}
export default function App() {
return (
<CyberiaAuthProvider
backendUrl={import.meta.env.VITE_CYBERIA_BACKEND_URL}
apiToken={APP_TOKEN}
oauthRedirectUri={window.location.origin}
>
<UserAdminPage />
</CyberiaAuthProvider>
);
}backendUrl on CyberiaUserSettings is optional (defaults to https://auth.cyberia.host); set it if your provider uses a custom backendUrl.
Workspace admin mode
Use adminToken instead of userToken, with apiToken and/or appId / dropdown. Requires workspace app users permissions. See API_USAGE.md.
Reading the session token from localStorage
tokenFromLocalStorage reads the token field from CyberiaAuthProvider’s session (default key cyberia_auth_session). With apiToken, a type: user JWT is routed to the public member-management API; a type: admin JWT still uses /api/admin/app-users.
Custom sign-out button example
If you already have your own sidebar/menu UI:
import { useNavigate } from 'react-router-dom';
import { useCyberiaAuth } from '@cyberia-auth/auth';
function SidebarLogoutButton() {
const navigate = useNavigate();
const { signOut } = useCyberiaAuth();
return (
<button
type="button"
onClick={() => {
signOut();
navigate('/login', { replace: true });
}}
>
Logout
</button>
);
}React Router example with Protect
import { Navigate, Route, Routes } from 'react-router-dom';
import { Protect } from '@cyberia-auth/auth';
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<Protect fallback={<Navigate to="/login" replace />}>
<HomePage />
</Protect>
}
/>
</Routes>
);
}SSO sign-up vs sign-in
The component sends intent=sign_up on OAuth start when the user is on Sign up, and intent=sign_in on Sign in (matches Cyberia Auth backend behavior).
If the app uses approval_required sign-up mode, SSO registration returns with signup_status=pending_approval (no JWT). The SDK shows a “Sign-up request pending” dialog (via document.body portal) with OK, which closes the notice without forcing another dialog. The same dialog appears after email/password register when the API responds with 202 and pendingApproval.
OAuth redirect URLs should match oauthRedirectUri (usually your app origin or dedicated callback route) so return query params are read correctly.
Workspace invitation page (CyberiaWorkspaceInvitation)
Use a dedicated route (for example /invite) when the Cyberia Auth backend sends invite links to your Auth Admin UI. Set workspaceInvitationAcceptRedirect on each application in Admin (POST / PATCH /api/admin/apps/:id) — the server picks the URL from the invited apps (see Cyberia Auth API_USAGE.md). Example base: https://your-admin.example.com/invite/.
Short links (current backend): the email and GET /api/admin/workspace-invitations/accept redirect only invite_status=ready and invite_token. The component calls GET /api/admin/workspace-invitations/preview?token=... to load email, role, workspace names, org/app scope (so the URL stays small).
Legacy links may still include workspace_names, invite_email, etc. in the query string; those are used directly and preview is skipped.
Prefer searchParams={useSearchParams()[0]} from React Router; if omitted, the component reads window.location.search each render.
| Parameter | Purpose |
|-----------|---------|
| invite_status | Failure: invalid_token vs invalid_or_expired / expired. ready (or omitted with token) → accept flow. |
| invite_token (or token) | Secret token for preview + POST /api/admin/workspace-invitations/accept |
| Other invite_* / workspace_* | Optional; legacy. If workspace_names is absent, details come from preview. |
Minimal page (React Router)
import { useSearchParams } from 'react-router-dom';
import { CyberiaWorkspaceInvitation } from '@cyberia-auth/auth';
export default function AcceptInvite() {
const [searchParams] = useSearchParams();
return (
<CyberiaWorkspaceInvitation
searchParams={searchParams}
backendUrl={import.meta.env.VITE_CYBERIA_BACKEND_URL}
branding={{ appName: 'CYBERIA SOFTWARE', primaryColor: '#c91111' }}
signInHref="/login"
successRedirectHref="/"
onAcceptSuccess={(data) => {
localStorage.setItem(
'cyberia_auth_session',
JSON.stringify({
token: data.token,
user: {
id: data.admin.id,
email: data.admin.email,
displayName: data.admin.name,
},
})
);
}}
/>
);
}successRedirectHref shows a short “You’re in!” state then replaces the location (same idea as a manual setTimeout + navigate). Use onAcceptSuccess only and call useNavigate() yourself if you prefer client-side routing without a full reload.
For tests, pass searchParams={new URLSearchParams('invite_status=ready&invite_token=…')} (or searchParamsOverride — same behavior).
API reference
CyberiaAuth props
backendUrl?: string— backend base URL (default:https://auth.cyberia.host)apiToken?: string— application token forGET /api/public/app/config, public OAuth, and related flows (seeauthScope).authScope?: 'public' | 'admin'— default'public'; controls which auth endpoints the UI calls.allowRegister?: boolean— defaulttrue. Setfalseto hide sign-up (footer “Sign up” and register mode).branding?: { appName?, organizationName?, primaryColor?, secondaryColor?, logoUrl?, oauthProviders?, publicSignUpMode? }— optional UI override;publicSignUpModemirrors backend when you want to force messaging without refetching configoauthRedirectUri?: string— absolute URL the backend redirects to after OAuth (must be allow-listed on the app / server)initialMode?: 'login' | 'register'— default'register'theme?: 'system' | 'light' | 'dark'— default'system'hideModeSwitch?: boolean— hide footer switch between sign in and sign uponAuthSuccess?: (result) => void— callback with{ token, user }whereuserincludesavatarUrlwhen the API provides it (public); admin responses are normalized to the same shape
CyberiaAuthProvider props
backendUrl?: stringapiToken?: string— set for public app branding/OAuth and for embedded permissions (canAppEmbedded); seeauthScope.authScope?: 'public' | 'admin'allowRegister?: boolean— defaulttrue; setfalseto disable sign-up in the modal.branding?: { appName?, organizationName?, primaryColor?, secondaryColor?, logoUrl?, oauthProviders?, publicSignUpMode? }— passed to modal auth UIoauthRedirectUri?: stringtheme?: 'system' | 'light' | 'dark'— applied to modal auth + user menu UIchildren: ReactNodestorageKey?: string— localStorage key (default:cyberia_auth_session)defaultAppOperatorRole?: string— initial role key forcanAppEmbedded(defaultcollaborator); override withsetAppOperatorRolewhen you know the signed-in operator’s role.
CyberiaWorkspaceInvitation props
backendUrl?: stringbranding?: { appName?, organizationName?, primaryColor?, secondaryColor?, logoUrl? }searchParams?: URLSearchParams— recommended; e.g.useSearchParams()[0]from React RoutersearchParamsOverride?: URLSearchParams— deprecated alias ofsearchParamsonAcceptSuccess?: (payload) => void—payloadincludestoken,admin,workspace,workspacesfrom the APIonAcceptError?: (message: string) => void— optional (e.g. toast); errors are also shown inlinesuccessRedirectHref?: string— after success, brief confirmation thenwindow.location.replace(href)successRedirectDelayMs?: number— default1500signInHref?: string— e.g./loginfor Sign in instead / Go to LoginonSignInInstead?: () => void— if set, called instead of navigating tosignInHref
useCyberiaAuth() returns
isLoadedisSignedInuser—{ id, email, displayName, avatarUrl? }(avatarUrlwhen returned by the API or OAuth redirect)tokenopenSignIn()openSignUp()signOut()setSessionFromAuth(result)themeembeddedRolePermissionMatrix— fromGET /api/public/app/configwhenapiTokenis set;nullwithout a token (thencanAppEmbeddedallows all).embeddedPermissionResources— labels forend_users,invitations,app_settings.appOperatorRole/setAppOperatorRole(role)— operator role key for the matrix (see Embedded app permissions above for full examples).canAppEmbedded(resource, action)—resource:'end_users' | 'invitations' | 'app_settings';action:'read' | 'create' | 'update' | 'delete'.
Helper components
SignedIn— render children only when authenticatedSignedOut— render children only when signed outShow—when="signed-in" | "signed-out"(see example above)SignInButton— opens sign-in modalSignUpButton— opens sign-up modalSignOutButton— clears sessionUserButton— avatar dropdown + manage account modalProtect— route/content guard withfallback
Re-exported permission utilities
Useful for tests or custom admin UIs: hasEmbeddedAppPermission, normalizeEmbeddedMatrix, DEFAULT_EMBEDDED_MATRIX, DEFAULT_EMBEDDED_RESOURCE_CATALOG, and types EmbeddedAppResourceId, EmbeddedPermissionAction, EmbeddedRolePermissionMatrix, EmbeddedPermissionResourceMeta, EmbeddedResourceCrud.
CyberiaUserSettings / UserSettings props
backendUrl?: string— omit for defaulthttps://auth.cyberia.host.apiToken?: string/appToken?: string— required for the public member API (withuserTokenor a detectedtype: userbearer); resolvesappIdviaGET /v1/environment?apiToken=…(or passappIdfor admin-only picker mode).userToken?: string— end-user JWT; uses/api/public/app/app-users(needscanManageAppUserson your membership).adminToken?: string— workspace admin JWT when the session is not atype: userJWT; withapiToken, atype: uservalue here still uses the public API.tokenFromLocalStorage?: boolean— readtokenfromlocalStorage[sessionStorageKey](defaultcyberia_auth_session).sessionStorageKey?: string— storage key when usingtokenFromLocalStorage(defaults to the provider’s session key).appId?: string— optional when usingapiToken; otherwise locks to one app or use neither for a dropdown.layout?: 'tabs' | 'accordion'— defaulttabs.theme?: 'system' | 'light' | 'dark'branding?: { primaryColor?, appName? }className?,style?
Exported types: AppUserListItem, AppUserMembershipStatus, ExternalAppOption, CyberiaUserSettingsProps.
OAuth notes
- Configure Google/Microsoft (and other) providers in the Cyberia Admin app (and/or server env) before using social buttons.
oauthRedirectUrishould be an absolute URL in your app.- Which buttons appear: with
apiToken, the SDK loadsoauthProvidersfromGET /api/public/app/config?apiToken=...and only renders SSO for those ids. If the backend returns[], the social row is hidden. Older backends without this field still show sensible defaults. - After OAuth, the provider reads callback query params (
token/admin_token,email/admin_email,displayName/admin_name, optionalavatarUrl) and persists the session automatically.
Branding, theme, and flash behavior
- Component fetches app branding and
oauthProvidersfrom/api/public/app/config. - Sign-in titles use
appNamefrom that response (not organization name). - Branding (including
oauthProviderswhen present) is cached perapiTokenin localStorage to reduce initial flash. - The same automatic config lookup is used for public flows when
apiTokenis provided. - If branding is not available, fallback colors/logo are used.
- Theme supports
system,light, anddark.
Common troubleshooting
GET /api/public/app/config?apiToken=...400: yourapiTokenis empty or missing whileauthScopeexpects a public app token.- Redirect to protected route fails after login: ensure
setSessionFromAuth(result)is called in custom login page flows. - Social login returns but session not applied: verify callback URL and that provider wraps the route tree.
NPM publish steps
1) Build and verify
npm run build
npm pack2) Login to npm
npm login3) Publish
npm publishFor first publish on a scope:
npm publish --access public4) Publish updates
npm run release:patchAvailable scripts:
release:patchrelease:minorrelease:major
Each script runs:
npm version <type>npm publish --access public
prepublishOnly runs npm run build before publish.
