npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iaurelius/accounts-expo

v0.4.0

Published

Accounts by iAurelius for Expo: sign-in on the hosted page in an authentication session, tokens in SecureStore, useAuth() and getToken() that refreshes itself.

Readme

@iaurelius/accounts-expo

Accounts by iAurelius for Expo (iOS, Android and web).

People sign in on your application's hosted page, with passkeys, email codes and Google. The page opens in the system's authentication session: ASWebAuthenticationSession on iOS, Custom Tabs on Android. The SDK then:

  • keeps the tokens in SecureStore;
  • refreshes the access token when it is about to expire;
  • signs out here and on the hosted pages.

Quick start

npx @iaurelius/accounts init --code aci_…   # the command from the console: Apps and APIs → Integrate your app

What it does:

  • registers your mobile app with its bundle id as the scheme (com.example.app:/oauth/callback, RFC 8252);
  • installs this package and its Expo modules with npx expo install;
  • writes .env.local;
  • adds the scheme to app.json.

Rebuild the app afterwards, because the scheme is native. By hand, the steps are:

npx expo install @iaurelius/accounts-expo expo-crypto expo-secure-store expo-web-browser
// app/_layout.tsx
import { AccountsProvider } from "@iaurelius/accounts-expo";
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <AccountsProvider
      publishableKey={process.env.EXPO_PUBLIC_ACCOUNTS_PUBLISHABLE_KEY!}
      clientId={process.env.EXPO_PUBLIC_ACCOUNTS_CLIENT_ID!}
      scheme="com.example.app"
    >
      <Stack />
    </AccountsProvider>
  );
}
import { SignedIn, SignedOut, useAuth, useUser } from "@iaurelius/accounts-expo";
import { Button, Text } from "react-native";

function Profile() {
  const { signIn, signOut, getToken } = useAuth();
  const { user } = useUser();
  return (
    <>
      <SignedOut><Button title="Sign in" onPress={signIn} /></SignedOut>
      <SignedIn>
        <Text>{user?.email}</Text>
        <Button title="Call my API" onPress={async () => fetch("https://api.example.com/me", { headers: { authorization: `Bearer ${await getToken()}` } })} />
        <Button title="Sign out" onPress={() => signOut()} />
      </SignedIn>
    </>
  );
}

On the web, add the route app/oauth/callback.tsx, which calls completeSignIn() from this package.

Your own sign-in screen

Instead of the hosted page, the app can sign people in on its own screens, with no browser: a code to their email, then the code. Someone new signs up the same way where the application is public; where it has a waitlist, sendEmailCode answers { waitlist: true } and sends no code.

const { sendEmailCode, signInWithEmailCode } = useAuth();
await sendEmailCode(email);             // step 1: the code goes to the email
await signInWithEmailCode(email, code); // step 2: signed in (throws OAuthError: INVALID_OTP, ACCOUNT_LOCKED…)

Only the application's native apps may do this (registered as mobile in the console). The hosted page's bot check (Turnstile) cannot run on a native screen: the per-address and per-IP limits still apply.

  • Both steps throw an OAuthError. A refusal keeps the application's code (INVALID_OTP, ACCOUNT_LOCKED, ACCOUNT_OTP_LIMITED, organization_required…). A limit without a code of its own is too_many_requests (status 429), and a 5xx or no answer temporarily_unavailable; both carry retryAfter in seconds when the application says how long to wait. None of them touches the session the device already had.
  • The code for the session waits up to a minute, like the hosted sign-in's code exchange, and is sent once: the application spends it (and issues a refresh token) as soon as it arrives, so giving up after 10 seconds would waste it and sending it again would only be refused.
  • If the person signs out while the code is on its way, they stay signed out and the refresh token its answer brings is revoked. A refresh of the session before, still on its way, does not bring that one back.

Tokens and the network

  • getToken() gives a token for your audience, refreshed a minute before it expires. While the token still lasts, getToken() gives it at once and the refresh goes on in the background, so a network that hangs holds no call; once it has expired, getToken() waits for the refresh. When your API refuses one (a 401), ask once with getToken({ forceRefresh: true }):
    • Several refusals of the same token make a single refresh, and a token that a forced refresh gave less than 30 seconds ago is given again (an API that always answers 401 cannot use up the application's limit).
    • It resolves to null only when the session is over (then sign the person out). If no new token can be had now (no network, or the application asked to wait), it throws an OAuthError (exported by this package) with error: "temporarily_unavailable": the person is still signed in; try again later.
  • On start, the session kept on the device shows at once (isLoaded and isSignedIn), and its refresh follows. Offline, the person stays signed in with the token they have.
  • Each request to your application gives up after 10 seconds (requestTimeoutMs on <AccountsProvider>), except those to the token endpoint:
    • A refresh that gets no answer in 8 seconds is sent once more, at once and identical, and that second try waits up to a minute. When the application processes a refresh it rotates the refresh token, and it answers the old one again only for a few seconds; after that, presenting the old one ends every session of the person in your app. Sending it again early is what keeps the second try within those seconds.
    • The sign-in's code exchange waits up to a minute and is sent once more if no answer comes. A code sent twice is refused and the refresh token it issued is revoked, so none is left alive on no device.

Security

  • Sign-in: authorization code with PKCE (S256), state and nonce, and RFC 9207's iss checked. The app is public, with no secret on the device.
  • ID token: it comes straight from the token endpoint over TLS, so its issuer, audience, nonce and expiry are checked (OIDC Core 3.1.3.7).
  • On the device: tokens stay in SecureStore, split in parts because Android is unreliable past 2 KB.
  • Revocation: a person who is banned or signed out everywhere is out at the next refresh. Sign-out revokes the refresh token.
  • Refresh: only a refusal for good signs the person out: a 4xx with an OAuth error, such as invalid_grant (revoked, banned), invalid_client, invalid_scope or invalid_target (your audience changed). A 429 (many phones behind one carrier IP), a 5xx, no answer or a 4xx without an OAuth error (a firewall's page) keeps the session and its refresh token; the access token serves while it lasts, and no refresh is tried again before the server's Retry-After. A refreshed ID token must be the same person (iss, sub, aud), and userinfo's sub must match.
  • Sign-out during a refresh or a sign-in: a refresh that was on its way when the person signed out (or signed in again) does not bring the old session back, and a sign-in (hosted page or your own screen) that was on its way when they signed out does not sign them in: its answer is dropped and its refresh token revoked. A sign-in answer whose ID token is not for that sign-in is refused, and its refresh token revoked too.
  • Bundle size: the package uses no Node APIs and no WebCrypto (expo-crypto instead), and does not pull jose into your bundle.

License

MIT