Skip to content

Repository files navigation

@choiceform/shared-auth

A shared authentication library based on Better Auth + Legend State.

Architecture

┌─────────────────────────────────────────────────────────────┐
│ createAuth() / initAuth() │
│ (core.ts) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Store │ │ Service │ │ API │ │ Hooks │ │
│ │ Layer │ │ Layer │ │ Layer │ │ Layer │ │
│ │ │ │ │ │ │ │ │ │
│ │authStore │ │authServ. │ │apiClient │ │useAuthSync │ │
│ │storeAct. │ │callback │ │authApi │ │useProtected │ │
│ │computed │ │companion │ │orgApi │ │useEmailVer. │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Four-Layer Architecture

LayerResponsibilityFiles
Store LayerReactive state managementstore/state.ts, store/actions.ts, store/computed.ts, store/utils.ts
Service LayerBusiness logicservices/auth-service.ts, services/callback-service.ts, services/companion-team.ts
API LayerHTTP requestsapi/client.ts, api/auth-api.ts, api/organization-api.ts, api/team-api.ts
Hooks LayerReact-specifichooks/use-auth-sync.ts, hooks/use-protected-route.ts, hooks/use-email-verification.ts

Installation

pnpm add @choiceform/shared-auth

Peer Dependencies

{
"@legendapp/state": "v3.0.0-beta.30",
"better-auth": "^1.4.4",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}

Quick Start

Option 1: initAuth (Recommended)

Quick initialization with default configuration, automatically includes magicLinkClient and organizationClient plugins:

import{initAuth}from"@choiceform/shared-auth"constauth=initAuth({baseURL: "https://api.example.com",// Optional configurationtokenStorageKey: "auth-token",// localStorage key, defaults to "auth-token"skipTokenCleanupOnError: false,// Set to true for development})export{auth}

Option 2: createAuth (Custom Configuration)

Use when you need custom Better Auth plugins:

import{createAuth}from"@choiceform/shared-auth"import{magicLinkClient,organizationClient}from"better-auth/client/plugins"constauth=createAuth({baseURL: "https://api.example.com",plugins: [magicLinkClient(),organizationClient({teams: {enabled: true}}),// Other plugins...],tokenStorageKey: "auth-token",})export{auth}

AuthInstance API

The instance returned by createAuth() / initAuth() includes:

constauth=initAuth({baseURL: "..."})// API Layerauth.apiClient// HTTP clientauth.authApi// Auth APIauth.organizationApi// Organization APIauth.teamApi// Team API// Store Layerauth.authStore// Legend State Observableauth.authComputed// Computed propertiesauth.tokenStorage// Token storageauth.storeActions// State actions// Service Layerauth.authService// Auth business logic// Active State Managementauth.setActiveOrganization(request)auth.setActiveTeam(request)auth.setActiveOrganizationAndTeam(orgId,teamId)// Shortcut Methodsauth.getCurrentUser()// Get current userauth.getCurrentUserId()// Get current user IDauth.isAuthenticated()// Check if authenticatedauth.isLoading()// Check if loadingauth.isLoaded()// Check if loadedauth.getAuthToken()// Get tokenauth.getAuthHeaders()// Get auth headersauth.waitForAuth()// Wait for auth to completeauth.userManager// User manager// Better Auth Client (Advanced)auth.authClient// Raw Better Auth client

Hooks (Recommended)

useAuthSync

Sync authentication state, replaces manual Better Auth session sync:

import{useAuthSync}from"@choiceform/shared-auth"functionApp(){useAuthSync(auth,{skipCompanionTeamPaths: ['/auth/callback','/auth/delete-success'],onAuthChange: (isAuthenticated)=>{if(isAuthenticated){syncTheme()syncLanguage()}}})return<YourApp/>}

useProtectedRoute

Route protection, checks authentication status and email verification:

import{useProtectedRoute}from"@choiceform/shared-auth"functionProtectedRoute({ children }){constnavigate=useNavigate()constlocation=useLocation()const{ status, shouldRender, redirectPath }=useProtectedRoute(auth,{pathname: location.pathname,publicRoutePrefixes: ['/resources','/public'],requireEmailVerified: true,lang: 'us'})useEffect(()=>{if(redirectPath){navigate(redirectPath,{replace: true})}},[redirectPath])if(!shouldRender){return<Loading/>}return<>{children}</>}

useEmailVerification

Email verification flow:

import{useEmailVerification}from"@choiceform/shared-auth"functionVerifyEmailPage({ email, lang }){constnavigate=useNavigate()const{
isLoading,
isAlreadyVerified,
isCountingDown,
countdown,
resendVerification,
changeEmail
}=useEmailVerification(auth,{
email,
lang,onRedirect: (path)=>navigate(path),onSendSuccess: (email)=>toast.success(`Verification email sent to ${email}`),onSendError: ()=>toast.error('Failed to send'),onAlreadyVerified: ()=>navigate('/community')})return(<div>{isAlreadyVerified ? (<p>Emailverified,redirecting...</p>) : (<><buttononClick={resendVerification}disabled={isLoading||isCountingDown}>{isCountingDown ? `Retry in ${countdown}s` : 'Resend'}</button><buttononClick={changeEmail}>Usedifferentemail</button></>)}</div>)}

useAuthInit

Initialize authentication state:

import{useAuthInit,initializeAuth}from"@choiceform/shared-auth"// Hook approachfunctionApp(){useAuthInit(auth)return<YourApp/>}// Or manual callawaitinitializeAuth(auth)

Service Layer

authService

Auth business logic wrapper:

// OAuth sign inawaitauth.authService.signInWithOAuth("github",callbackURL,newUserCallbackURL,errorCallbackURL)// Magic Link sign inawaitauth.authService.signInWithMagicLink(email,callbackURL,name,newUserCallbackURL)// Email/password sign inconstresult=awaitauth.authService.signInWithEmail(email,password)// Email/password sign upconstresult=awaitauth.authService.signUpWithEmail(email,password,name,callbackURL)// Sign outawaitauth.authService.signOut("/sign-in")// Delete accountawaitauth.authService.deleteUser(callbackURL,password)// Fetch session with tokenawaitauth.authService.fetchAndSetSession(token)

callbackService

Handle various auth callbacks (OAuth, email verification, user deletion, etc.):

import{createCallbackService}from"@choiceform/shared-auth"constcallbackService=createCallbackService(auth,{lang: 'us',defaultRedirect: '/',signInPath: '/sign-in',linkExpiredPath: '/auth/link-expired',deleteSuccessPath: '/auth/delete-success',})// Handle OAuth callbackconstresult=awaitcallbackService.handleOAuthCallback(token,isNewUser)// Handle email verification callbackconstresult=awaitcallbackService.handleEmailVerificationCallback(token)// Handle delete user callbackconstresult=awaitcallbackService.handleDeleteUserCallback(token,userEmail)// Unified handlerconstresult=awaitcallbackService.handleCallback(type,token,{ isNewUser, userEmail, invitationId })

companionTeam

Companion team setup:

import{setupCompanionTeam}from"@choiceform/shared-auth"awaitsetupCompanionTeam(auth,options)

Store Layer

authStore (Legend State Observable)

Reactive state, can be used in React components:

import{use$}from"@legendapp/state/react"functionMyComponent(){constuser=use$(auth.authStore.user)constisAuthenticated=use$(auth.authStore.isAuthenticated)constloading=use$(auth.authStore.loading)consterror=use$(auth.authStore.error)constisLoaded=use$(auth.authStore.isLoaded)// Computed stateconstisReady=use$(auth.authComputed.isReady)constactiveOrganizationId=use$(auth.authComputed.activeOrganizationId)}

storeActions

State management actions:

// Readauth.storeActions.getUser()auth.storeActions.getUserId()auth.storeActions.isAuthenticated()auth.storeActions.isLoading()auth.storeActions.isLoaded()// Updateauth.storeActions.setUser(user)auth.storeActions.updateUser({name: "New Name"})auth.storeActions.setLoading(true)auth.storeActions.setError("Error message")auth.storeActions.clearAuth()auth.storeActions.handleUnauthorized()// Active stateauth.storeActions.setActiveOrganizationId(orgId)auth.storeActions.setActiveTeamId(teamId)

Store Utilities

Standalone utility functions for non-component scenarios:

import{getCurrentUser,getCurrentUserId,isAuthenticated,isLoading,isLoaded,waitForAuth,getAuthToken,getAuthTokenSync,getAuthHeaders,getAuthHeadersSync,handle401Response,createUserManager,}from"@choiceform/shared-auth"// Wait for auth to completeawaitwaitForAuth(auth.authStore)// Get auth headers (sync)constheaders=getAuthHeadersSync(auth.tokenStorage)// Handle 401 responsehandle401Response(response,auth.storeActions)

Utilities

import{// EnvironmentgetEnvVar,getAuthBaseUrl,// ValidationisValidEmail,// Error parsingparseAuthError,isTokenExpiredError,AUTH_ERROR_CODES,// URL utilitiesgetNameFromEmail,buildAuthUrl,buildAuthPath,clearAuthParams,// User mappingextractSessionUser,}from"@choiceform/shared-auth"// Validate emailif(isValidEmail(email)){// ...}// Parse errorconst{ code, message, isKnownError }=parseAuthError(error)// Check if token expiredif(isTokenExpiredError(error)){// Re-authenticate}// Build URLconsturl=buildAuthPath('/verify-email',{lang: 'us', email })

API Layer

Low-level API access:

import{createApiClient,createAuthApi,createOrganizationApi,createTeamApi,parseErrorResponse,}from"@choiceform/shared-auth"// Use APIs directlyconstresponse=awaitauth.authApi.getSession()constorgs=awaitauth.organizationApi.listOrganizations()constteams=awaitauth.teamApi.listTeams()

Directory Structure

src/
├── api/ # API Layer
│ ├── client.ts # HTTP client
│ ├── auth-api.ts # Auth API
│ ├── organization-api.ts # Organization API
│ ├── team-api.ts # Team API
│ └── index.ts
├── services/ # Service Layer
│ ├── auth-service.ts # Auth business logic
│ ├── callback-service.ts # Callback handling
│ ├── companion-team.ts # Companion team setup
│ └── index.ts
├── store/ # Store Layer
│ ├── state.ts # State definition
│ ├── actions.ts # State actions
│ ├── computed.ts # Computed properties
│ ├── utils.ts # Utility functions
│ └── index.ts
├── hooks/ # React Hooks
│ ├── use-auth-init.ts # Auth initialization
│ ├── use-auth-sync.ts # State sync
│ ├── use-protected-route.ts
│ ├── use-email-verification.ts
│ └── index.ts
├── utils/ # Utilities
│ ├── auth-utils.ts # Auth utilities
│ ├── user-mapper.ts # User mapping
│ ├── date.ts # Date utilities
│ ├── env.ts # Environment variables
│ └── index.ts
├── types/ # Type definitions
│ ├── auth.ts
│ ├── callback.ts
│ ├── organization.ts
│ ├── team.ts
│ ├── user.ts
│ └── index.ts
├── lib/ # Better Auth client
│ └── auth-client.ts
├── core.ts # Core entry (createAuth)
├── init.ts # Quick init (initAuth)
├── config.ts # Default config
└── index.ts # Export entry

Type Exports

importtype{// CoreAuthInstance,AuthState,AuthConfig,SessionUser,SessionUserMetadata,Session,// ServicesAuthService,AuthServiceConfig,CallbackService,CallbackType,CallbackResult,CallbackConfig,// HooksUseAuthSyncConfig,UseAuthSyncResult,UseProtectedRouteConfig,UseProtectedRouteResult,ProtectionStatus,UseEmailVerificationConfig,UseEmailVerificationResult,// OrganizationOrganization,OrganizationMetadata,FullOrganization,CreateOrganizationRequest,UpdateOrganizationRequest,Member,MemberWithUser,MemberRole,Invitation,InvitationDetail,InvitationStatus,// TeamTeam,TeamMetadata,TeamMember,CreateTeamRequest,UpdateTeamRequest,// APIApiClient,ApiClientConfig,ApiResponse,TokenStorage,AuthApi,OrganizationApi,TeamApi,// StoreStoreActions,// UtilitiesAuthErrorCode,ParsedAuthError,}from"@choiceform/shared-auth"

Development

# Install dependencies
pnpm install
# Development mode
pnpm dev
# Build
pnpm build
# Test
pnpm test# Watch tests
pnpm test:watch

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages