Skip to content

@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

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.

  • 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 transient outcome 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 ./storage subpath forks through exports conditions, so your app writes zero Platform.OS branches and the browser graph contains no expo-secure-store.

Outcome: One app-owned session that persists login state and coordinates concurrent refreshes.

Terminal window
pnpm add @gj-kit/expo-auth

Choose the storage adapter once and connect only your refresh-endpoint callback.

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,
});

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 navigation
declare 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 an app needs one reusable token refresh and persistence boundary across mobile and browser clients.

Do not put application routes, identity-provider policy, telemetry, or API client ownership in the package.

Engines: node >=20

Peer Supported range Required for
expo-secure-store >=14.0.0 documented optional subpaths only
Entry Exported symbols
Root entry 19
./storage 3
./testing 8

Treat tokens as secrets: use the supplied error contracts and never log token strings or raw authorization responses.

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.

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.

npm · GitHub