@gj-kit/expo-auth
Token refresh for Expo, React Native and web — a missed transient branch won’t compile.
- npm
0.1.2 - 140+ tests passing (unit, native, web)
- 19 @ts-expect-error compile guards
- 0 runtime dependencies, 1 optional peer
- 5 test doubles at ./testing, peer-free
Keep learning
Section titled “Keep learning”Why this exists
Section titled “Why this exists”Hand-rolled refresh code classifies a 5xx, a timeout or a CORS failure as a definitive rejection and signs the user out, because the refresh result is consumed by a raw switch that TypeScript never checks for exhaustiveness — and the branch that goes missing is always the transient one. Meanwhile a Platform.OS fork drags expo-secure-store into the web bundle, two tabs race the same single-use refresh token, a boolean re-entry flag lets a 401 retry fall back into the refresh path, and an option-less re-login after signOut silently promotes a session-scoped login to durable storage on a shared machine.
What it does about it
Section titled “What it does about it”- Missing transient case won’t compile — matchRefreshOutcome takes all five endings as handler keys — omit one and the call fails to compile with “Property ‘transient’ is missing”.
- A 5xx leaves stored tokens alone — On a
transientoutcome the core writes nothing to storage, and runAuthorized rethrows your original error instead of routing the user to sign-in. - Retry twice is unrepresentable — runAuthorized requires shouldRetryAfterRefresh with no default, and the one retry it performs runs outside the refresh path — the public API has no re-entry switch.
- signIn cannot forget persistence — persistence is a required option on signIn’s second argument, so “session login → signOut → option-less re-login” cannot silently promote tokens to durable storage.
- No SecureStore in the web bundle — One
./storagesubpath forks through exports conditions, so your app writes zero Platform.OS branches and the browser graph contains no expo-secure-store.
Golden path
Section titled “Golden path”Outcome: One app-owned session that persists login state and coordinates concurrent refreshes.
1. Install
Section titled “1. Install”pnpm add @gj-kit/expo-auth2. Keep the app-owned boundary explicit
Section titled “2. Keep the app-owned boundary explicit”Choose the storage adapter once and connect only your refresh-endpoint callback.
3. Start with the smallest integration
Section titled “3. Start with the smallest integration”Copy this first, then replace only the app-owned values named above.
import { createAuthSession, type RefreshRequest } from '@gj-kit/expo-auth';import { createTokenStorage, createWebLocksRefreshLock } from '@gj-kit/expo-auth/storage';
declare const refresh: RefreshRequest; // Your API client classifies rotated, invalid, or transient.
export const session = createAuthSession({ storage: createTokenStorage({ keyPrefix: 'myapp.auth' }), lock: createWebLocksRefreshLock({ name: 'myapp.auth' }), refresh,});What that looks like
Section titled “What that looks like”The five endings of refresh() are consumed by key, so the omission that turns a network blip into a false logout is rejected at compile time; tokens is reachable only after narrowing to refreshed or adopted, and a refresh callback that throws arrives here as transient with the thrown value in cause.
import { matchRefreshOutcome, type RefreshOutcome } from '@gj-kit/expo-auth';
declare const outcome: RefreshOutcome; // the app owns this — it is `await session.refresh()`declare const goToSignIn: () => null; // the app owns navigationdeclare const report: (cause: unknown) => void; // the app owns telemetry
export const accessToken = matchRefreshOutcome<string | null>(outcome, { refreshed: ({ tokens }) => tokens.accessToken, // `tokens` exists here and on `adopted` only adopted: ({ tokens }) => tokens.accessToken, 'signed-out': () => goToSignIn(), invalid: () => goToSignIn(), // a definitive server rejection transient: ({ cause }) => { report(cause); // stored tokens are left untouched — a 5xx is not a sign-out return null; },});// Delete the `transient` line above and tsc refuses the call:// error TS2345: ... Property 'transient' is missing in type ... but required in type ...Use it when
Section titled “Use it when”Use it when an app needs one reusable token refresh and persistence boundary across mobile and browser clients.
Do not use it when
Section titled “Do not use it when”Do not put application routes, identity-provider policy, telemetry, or API client ownership in the package.
Runtime and peers
Section titled “Runtime and peers”Engines: node >=20
| Peer | Supported range | Required for |
|---|---|---|
expo-secure-store |
>=14.0.0 |
documented optional subpaths only |
Public subpaths
Section titled “Public subpaths”| Entry | Exported symbols |
|---|---|
Root entry |
19 |
./storage |
3 |
./testing |
8 |
Safety boundary
Section titled “Safety boundary”Treat tokens as secrets: use the supplied error contracts and never log token strings or raw authorization responses.
API reference
Section titled “API reference”complete API reference contains every public function, class, type, constant, and error contract generated from release declarations. Each item shows its import path, release signature, and package version.
Package relationships
Section titled “Package relationships”| Relationship | Package | Why combine them |
|---|---|---|
| Standalone | — | This package can be adopted without another GJ Kit package. |
| Optional companion | @gj-kit/expo-media |
Provide a session-owned authorization callback for application media APIs. |