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

@last1id/sdk-react-native

v0.1.0

Published

React Native / Expo companion for @last1id/sdk: PKCE flow via expo-web-browser, refresh tokens persisted to expo-secure-store (or any injected secure store)

Readme

@last1id/sdk-react-native

React Native / Expo companion to @last1id/sdk. Wraps the boilerplate around:

  • PKCE flow via expo-web-browser
  • Refresh token persistence via expo-secure-store (iOS Keychain / Android Keystore)
  • Auto-refresh with rotated tokens written back to secure storage transparently

The base SDK handles the OAuth/OIDC mechanics. This package handles the mobile-specific glue.

Install

npm install @last1id/sdk @last1id/sdk-react-native expo-secure-store expo-web-browser

expo-secure-store and expo-web-browser are peer dependencies — pass them in, the companion doesn't bundle them. Non-Expo React Native projects can wire react-native-keychain and react-native-inappbrowser-reborn by implementing the SecureTokenStore interface (see below).

Quick start

// app/_layout.tsx or wherever you initialize Last1
import * as SecureStore from "expo-secure-store";
import * as WebBrowser from "expo-web-browser";
import {
  createExpoSecureStoreAdapter,
  loadSecureLast1Client,
  beginPkceFlow,
  completePkceFlow,
  clearSecureLast1Tokens,
} from "@last1id/sdk-react-native";

const ISSUER = "https://last1.id";
const CLIENT_ID = "radiocheck";
const REDIRECT_URI = "radiocheck://last1-id/callback";

const storage = createExpoSecureStoreAdapter(SecureStore);

// On app boot — restore any prior session.
const session = await loadSecureLast1Client({
  issuerUrl: ISSUER,
  clientId: CLIENT_ID,
  storage,
});

if (session) {
  // Already linked. Use session.client for partner-read calls:
  const creds = await session.client.listCredentials();
  // ... refresh happens automatically + persists transparently
}

Linking a new user

async function linkLast1() {
  // 1. Build authorize URL + PKCE pair.
  const flow = await beginPkceFlow({
    issuerUrl: ISSUER,
    clientId: CLIENT_ID,
    redirectUri: REDIRECT_URI,
    scopes: ["openid", "profile", "credentials:read"],
    // offline_access is added automatically — it's required for a refresh token.
  });

  // 2. Open the auth session (ASWebAuthenticationSession on iOS).
  const result = await WebBrowser.openAuthSessionAsync(flow.authorizeUrl, REDIRECT_URI);
  if (result.type !== "success") return;

  const callbackUrl = new URL(result.url);
  const code = callbackUrl.searchParams.get("code");
  const returnedState = callbackUrl.searchParams.get("state");

  // 3. Verify state to prevent CSRF.
  if (returnedState !== flow.state) throw new Error("OAuth state mismatch");
  if (!code) throw new Error("OAuth callback missing code");

  // 4. Exchange the code for tokens, persist to Keychain, return primed client.
  const { client, identityId } = await completePkceFlow({
    issuerUrl: ISSUER,
    clientId: CLIENT_ID,
    redirectUri: REDIRECT_URI,
    code,
    codeVerifier: flow.codeVerifier,
    storage,
  });

  // 5. Profit. The user is linked. From here on, just call client methods.
  const me = await client.fetchUserinfo();
  console.log("Linked", { identityId, me });
}

Unlinking / logout

async function unlinkLast1() {
  // Optionally revoke server-side first (RFC 7009):
  // (the base SDK ships revokeToken() — call it before clearing storage)

  await clearSecureLast1Tokens({ storage });
  // The next loadSecureLast1Client() call will return null.
}

Error handling

import { Last1AuthError, Last1HttpError } from "@last1id/sdk-react-native";

try {
  const creds = await session.client.listCredentials();
} catch (err) {
  if (err instanceof Last1AuthError) {
    // Refresh chain dead — user revoked consent or token was rotated
    // out from under us. Clear local tokens and route to the
    // re-link CTA.
    await clearSecureLast1Tokens({ storage });
    return router.replace("/link-last1");
  }
  if (err instanceof Last1HttpError && err.status === 403 && err.code === "insufficient_scope") {
    // User granted credentials:read but not, say, trust_events:read.
    // Surface a "re-link with extra permissions" flow.
  }
  // Any other error: transient. Show a retry.
  throw err;
}

| Error | Meaning | Recovery | | ---------------------------------------- | ------------------------------------------- | ------------------------------ | | Last1AuthError | Refresh chain dead | Clear tokens + re-link | | Last1HttpError 403 insufficient_scope | User didn't grant a required scope | Re-link with extra scopes | | Last1HttpError 404 (on getCredential) | Credential not found | null is returned, not thrown | | Network / 5xx | Transient upstream issue | Retry |

Custom storage adapter

Don't use Expo? Implement SecureTokenStore:

import * as Keychain from "react-native-keychain";
import type { SecureTokenStore } from "@last1id/sdk-react-native";

const keychainAdapter: SecureTokenStore = {
  async getItem(key) {
    const result = await Keychain.getInternetCredentials(key);
    return result ? result.password : null;
  },
  async setItem(key, value) {
    await Keychain.setInternetCredentials(key, key, value, {
      accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    });
  },
  async deleteItem(key) {
    await Keychain.resetInternetCredentials(key);
  },
};

In-memory storage (tests / "don't remember me")

import { createMemoryTokenStore } from "@last1id/sdk-react-native";

const storage = createMemoryTokenStore();
// or seed:
const storage = createMemoryTokenStore({ "last1.id.refreshToken": "RT-1" });

Multi-tenant isolation

If your app talks to two Last1 deployments (e.g. staging + prod side-by-side), use distinct prefixes:

const stagingSession = await loadSecureLast1Client({
  issuerUrl: STAGING_URL,
  clientId: STAGING_CLIENT,
  storage,
  secureStorageKeyPrefix: "last1.staging",
});

const prodSession = await loadSecureLast1Client({
  issuerUrl: PROD_URL,
  clientId: PROD_CLIENT,
  storage,
  secureStorageKeyPrefix: "last1.prod",
});

Security notes

  • Refresh tokens never live in AsyncStorage. AsyncStorage is plaintext on disk. The default adapter writes to WHEN_UNLOCKED_THIS_DEVICE_ONLY Keychain on iOS — survives reboots but only readable when the device is unlocked.
  • Backup behavior: WHEN_UNLOCKED_THIS_DEVICE_ONLY is NOT included in iCloud Keychain backup, so a stolen iCloud password doesn't give an attacker your users' Last1 refresh tokens.
  • No biometric gate by default. Backgrounded silent refreshes would otherwise demand FaceID, which is the wrong UX. Layer biometric gating at the call site if you need it for high-value operations.
  • The onAuthError hook never throws. Persist failures are logged via the hook (if you wire one) but never break the outer request — the in-memory token pair is still valid for the current session.

Migration: RadioCheck-style bridge → full PKCE

If your app currently uses last1.id's /api/identity/authorize/complete bridge endpoint (returns just identity_id, no OAuth tokens), the migration to this package looks like:

  1. Register your redirectUri on the app_clients row at last1.id.
  2. Swap connectToLast1Id() for beginPkceFlow() + WebBrowser.openAuthSessionAsync() + completePkceFlow().
  3. Replace /api/identity/* reads with client.fetchUserinfo() / client.listCredentials() / etc.

The on-screen UX doesn't have to change.

Versioning

@last1id/sdk-react-native tracks the base @last1id/sdk major version. ^0.1.0 requires @last1id/sdk@^0.2.0.

License

MIT