@kgen-protocol/auth-core
v1.0.12
Published
Framework-agnostic KGeN login logic — auth state, API flows, token management
Readme
@kgen-protocol/auth-core
Framework-agnostic KGeN login logic — auth state, API flows, and token management. No UI, no routing, no analytics. Works with Next.js, Vite, or any React 18+ app.
Installation
This package is published to GitHub Packages. You need a
GITHUB_TOKENwithread:packagesscope.
Add to your .npmrc:
@kgen-protocol:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Then install:
pnpm add @kgen-protocol/auth-core
# peer deps
pnpm add react axios jotaiSetup
Call initAuthCore once at your app root before using any hooks.
// Next.js — app/providers.tsx
import { initAuthCore } from "@kgen-protocol/auth-core";
initAuthCore({
baseApiUrl: process.env.NEXT_PUBLIC_BASE_API_URL!,
onLoginSuccess: (tokens) => {
// tokens are already saved to cookies — fire analytics here
analytics.track("login_success");
router.replace("/home");
},
onLogout: (reason) => {
// reason: "token_expired" | "account_deleted" | "manual"
router.replace("/login");
},
onShieldBlock: (message, status) => {
// show your shield/restriction overlay
showShieldModal(message);
},
getErrorMessage: (errorCode, fallback, params) => {
// return a localised string — or skip this and get raw API messages
return t(`errors.${errorCode}`) ?? fallback;
},
});// Vite — src/main.tsx
initAuthCore({
baseApiUrl: import.meta.env.VITE_BASE_API_URL,
});Phone OTP Login
import {
useRegisterOtp,
useVerifyOtp,
useResendOtp,
useUpdatePhoneNumberAtom,
useUpdateLoginFlowAtom,
useTimer,
} from "@kgen-protocol/auth-core";
function LoginForm() {
const { setPhoneNumber } = useUpdatePhoneNumberAtom();
const { setCountryCode, state } = useUpdateLoginFlowAtom();
const { secondsLeft, isExpired } = useTimer(30, otpSent);
const registerOtp = useRegisterOtp();
const verifyOtp = useVerifyOtp();
const resendOtp = useResendOtp();
return (
<input
onChange={(e) => {
setPhoneNumber(e.target.value); // update atom on every keystroke
setCountryCode("+91");
}}
/>
);
}Important: Call
setPhoneNumberandsetCountryCodeononChange, not immediately before callingregisterOtp(). Atom updates trigger a re-render — calling the hook on the same tick will read the stale value.
Social Auth
import { useSocialAuthorize, useVerifySocialAuth, SOCIAL_PROVIDERS } from "@kgen-protocol/auth-core";
// Step 1 — get the provider redirect URL
const getRedirectUrl = useSocialAuthorize({
provider: SOCIAL_PROVIDERS.GOOGLE,
host: "INDIGG",
platform: "WEB",
});
const url = await getRedirectUrl();
if (url) window.location.href = url;
// Step 2 — on callback page, exchange the code
const verify = useVerifySocialAuth({
onSuccess: (tokens) => router.replace("/home"),
onError: () => setError("Login failed"),
});
// Read params from URL (use your own router)
const code = new URLSearchParams(location.search).get("code");
await verify({ provider: "GOOGLE", platform: "WEB", code, redirectUri });Discord OAuth
Discord uses a separate hook that sends the auth code as an HTTP header:
import { useSocialDiscordVerify } from "@kgen-protocol/auth-core";
const verifyDiscord = useSocialDiscordVerify({
onSuccess: (tokens) => router.replace("/home"),
onError: () => setError("Discord login failed"),
});
// The hook is a no-op when localStorage's socialLoginMethod is not "discord"
const code = new URLSearchParams(location.search).get("code");
await verifyDiscord(code);Logout
import { useLogout } from "@kgen-protocol/auth-core";
function Header() {
const logout = useLogout();
return (
<button onClick={() => logout("manual")}>Sign out</button>
);
}logout(reason) clears all auth cookies, resets all auth atoms, and fires config.onLogout(reason).
| Reason | When |
|---|---|
| "manual" | User clicked sign out |
| "token_expired" | Access token expired |
| "account_deleted" | Account was deleted |
Token Utilities
import {
isAuthenticated,
getAuthTokens,
clearAuthTokens,
parseJwt,
isTokenExpired,
saveAuthTokensToCookies,
} from "@kgen-protocol/auth-core";
isAuthenticated(); // true if accessToken cookie exists
getAuthTokens(); // { accessToken, refreshToken, idToken }
clearAuthTokens(); // removes all auth cookies + clears localStorage
parseJwt(token); // decoded payload object
isTokenExpired(token); // true if exp < nowAuth State Hook
import { useAuthState, LoginStatus } from "@kgen-protocol/auth-core";
function Header() {
const { isLoggedIn, userId, countryCode } = useAuthState();
if (isLoggedIn === LoginStatus.LoggedIn) {
return <span>Welcome, {userId}</span>;
}
return <LoginButton />;
}| LoginStatus value | Meaning |
|---|---|
| LoginStatus.Pending | Initial state — not yet determined |
| LoginStatus.LoggedIn | User is authenticated |
| LoginStatus.LoggedOut | User is not authenticated |
All Exports
Initialisation
| Export | Description |
|---|---|
| initAuthCore(config) | Initialise with config — call once at app root |
| isAuthCoreInitialised() | Returns true if already initialised |
Config type
| Field | Type | Required | Description |
|---|---|---|---|
| baseApiUrl | string | Yes | Base URL for all KGeN API calls |
| onLoginSuccess | (tokens) => void | No | Called after a successful login |
| onLogout | (reason) => void | No | Called when user is logged out |
| onShieldBlock | (message, status) => void | No | Called on a 403 Shield block |
| getErrorMessage | (code, fallback?, params?) => string | No | Return a localised error string |
Hooks — state
| Hook | Returns | Description |
|---|---|---|
| useAuthState() | { isLoggedIn, userId, countryCode, ipAddress, deviceData, error, emailOptIn } | Read-only auth state |
| useUpdateLoginFlowAtom() | { state, setCountryCode, setError, setErrorFlow, setIsLoggedIn, ... } | Read/write login flow state |
| useUpdatePhoneNumberAtom() | { phoneNumber, setPhoneNumber, setAuthCode, ... } | Read/write phone number state |
| useUpdateOtpAtom() | { otp, setOtp, ... } | Read/write OTP state |
| useUpdateModalAtom() | { step, isOpen, setStep, handleModal, ... } | Read/write modal/step state |
| useUpdateUserAtom() | { userId, countryCode, setUser, ... } | Read/write user atom |
| useLogout() | (reason?) => void | Returns logout function |
| useTimer(seconds, active) | { secondsLeft, isExpired, reset } | Countdown timer |
| useUserLocationApi() | () => Promise<void> | Fetches and stores user IP + location |
| useMonitorAtomState(atom) | void | Dev utility — logs atom state changes |
Hooks — flows
| Hook | Description |
|---|---|
| useRegisterOtp(props?) | Sends OTP to phone number |
| useVerifyOtp(options?) | Verifies OTP and logs the user in |
| useResendOtp() | Resends OTP to the current phone number |
| useSocialAuthorize(options) | Returns OAuth redirect URL for a provider |
| useVerifySocialAuth(options?) | Exchanges OAuth code for tokens (Google/Twitter) |
| useSocialDiscordVerify(options?) | Exchanges Discord OAuth code for tokens |
Atoms (advanced)
Raw Jotai atoms are exported for consumers that need direct atom access:
| Export | Description |
|---|---|
| userAtom | User login status, userId, IP, device data |
| userLocationAtom | Country code, country name |
| loginFlowAtom | Login flow state (step, errors, country, etc.) |
| loginInitialState | Initial value for loginFlowAtom |
| phoneNumberFlowAtom | Phone number and auth code |
| otpFlowAtom | OTP input state |
| modalAtom | Modal open/step/flow state |
| shieldAtom | Shield block state |
| socialFlowAtom | Active social login method |
Constants
| Export | Description |
|---|---|
| SOCIAL_PROVIDERS | { GOOGLE, TWITTER, DISCORD, OTPLESS } |
| LOGIN_METHOD | { google, twitter, discord, phone } |
| COUNTRY_CODES | Dial codes — IN: "+91", BR: "+55", BD: "+880", NP: "+977" |
| COUNTRY_NAMES | Country name mapping by dial code |
| LOCAL_STORAGE_KEYS | All localStorage key strings |
| SESSION_STORAGE_KEYS | { SHIELD_BLOCKED_MODE, SHIELD_ERROR_STATUS, SHIELD_MESSAGE } |
| TOKEN_TYPE | { ACCESS, REFRESH, ID } |
| LOGIN_FLOW | Step constants for the modal flow |
| API_URLS | All API endpoint strings |
| OTPLESS_FLOW_TYPE | { VERIFY_USER_NUMBER } |
| ERROR_MESSAGE | Generic error message strings |
| errorsMapping | Error messages keyed by API status code |
| SHIELD_FRAUDS | Shield fraud type constants |
| countryCodes | Full country dial-code list |
| phoneRegex | Phone number regex for India |
| internationalPhoneRegex | International phone number regex |
| PHONE_NUMBER_REGEX_BY_COUNTRY_CODE | Phone validation regex per country code |
Token utilities
| Export | Description |
|---|---|
| isAuthenticated() | true if accessToken cookie is present |
| getAuthTokens() | { accessToken, refreshToken, idToken } from cookies |
| saveAuthTokensToCookies(tokens, isNewUser?) | Writes auth tokens to cookies |
| saveSessionToken(tokens, isNewUser?) | Writes session tokens to cookies (secondary login) |
| clearAuthTokens(keysToKeep?) | Removes all auth cookies and clears localStorage |
| clearLocalStorageExcept(keysToKeep) | Clears localStorage, preserving specified keys |
| getReceivedToken() | { accessToken, refreshToken } from cookies |
| readCookie(name) | Reads a cookie by name |
| setCookie(name, value, days?) | Writes a cookie (default 7-day expiry) |
| removeCookie(name) | Expires a cookie immediately |
Utilities
| Export | Description |
|---|---|
| getCurrentPlatform() | Returns current platform string (gamer, airdrop, etc.) |
| getSourceAsPerWebsiteUrl() | Returns source identifier based on window.location |
| isGamerPlatform() | true if current platform is gamer |
| isDesktop() | true if running on a desktop browser |
| isMobileDevice() | true if running on a mobile browser |
| getDeviceType() | Returns "mobile" or "desktop" |
| parseJwt(token) | Decodes a JWT — returns payload object |
| parseUserIdFromJwt(token) | Extracts userId claim from a JWT |
| isTokenExpired(token) | true if JWT exp is in the past |
| isIndianUser() | true if stored country code is +91 |
| isBrazilUser() | true if stored country code is +55 |
| validatePhoneNumberByCountry(phone, countryCode) | Returns error key string or null |
| countryOptions | Array of { label, value } for country selector |
| getLocalStorageItem(key) | Safe localStorage.getItem (SSR-safe) |
| setLocalStorage(key, value) | Safe localStorage.setItem (SSR-safe) |
| removeLocalStorage(key) | Safe localStorage.removeItem (SSR-safe) |
| getSessionStorageItem(key) | Safe sessionStorage.getItem (SSR-safe) |
| setSessionStorageItem(key, value) | Safe sessionStorage.setItem (SSR-safe) |
| removeSessionStorageItem(key) | Safe sessionStorage.removeItem (SSR-safe) |
| getIPAndLocationFromStorage() | Returns stored IP/location headers object |
| ipAddressRef | Mutable ref holding the current IP address |
API layer (advanced)
| Export | Description |
|---|---|
| apiCalls | Axios wrapper with .get, .post, .put, .delete methods |
| getAxiosInstance() | Returns the underlying Axios instance |
Types
Key exported types:
| Type | Description |
|---|---|
| AuthCoreConfig | Config object passed to initAuthCore |
| Tokens | { accessToken, refreshToken, idToken? } |
| LoginFlowType | Shape of loginFlowAtom state |
| OTPFlowState | Shape of otpFlowAtom state |
| PhoneNumberFlowState | Shape of phoneNumberFlowAtom state |
| ModalManageState | Shape of modalAtom state |
| UserAtomType | Shape of userAtom state |
| UserLocation | { countryName, countryCode, continentCode, countryCodeISO3 } |
| SocialAuthResponse | API response from social auth verify |
| RegisterApiResponse | { authCode, isNewUser } |
| VerifyOtpApiResponse | Tokens + status fields |
| ApiCallResponse<T> | SuccessApiResponse<T> \| FailedApiResponse<T> |
| Flows | Enum of flow types (Login, VerifyOtp, etc.) |
| LoginStatus | Enum: Pending \| LoggedIn \| LoggedOut |
| ErrorFlow | Enum: PhoneNumber \| Otp \| Twitter \| Discord |
| IDeviceInfo | Device info shape |
Peer Dependencies
| Package | Required version |
|---|---|
| react | >= 18.0.0 |
| axios | >= 1.6.0 |
| jotai | >= 2.0.0 |
Build
pnpm build # ESM + CJS + type declarations
pnpm typecheck # tsc --noEmit
pnpm test # vitest runPublishing
Publishing is handled by GitHub Actions on a version tag:
git tag @kgen-protocol/[email protected]
git push --tags