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

@locins/id

v3.0.3

Published

Official SDK for LocinsID — OAuth2/PKCE, hosted login UI, user & consent APIs for SPAs, Next.js, Expo & Capacitor

Readme

@locins/id

Official SDK for LocinsID — OAuth2/PKCE authentication, hosted login UI, user & consent APIs. Drop "Sign in with Locins" into any web or mobile app and let us run the password reset emails, MFA, social logins and consent screens.

npm downloads license TypeScript ESM


Why LocinsID

You build the app. We run the identity stack: WorkOS-backed user accounts, password reset, MFA, social logins (Apple/Google), session management, OAuth2/PKCE, consent management — all behind one button.

  • No backend required — PKCE means a SPA, mobile app, or AI-builder-generated site can authenticate users without you running a server.
  • One package, four platforms — works in browser SPAs, React Native (Expo), Next.js, Capacitor, and any Node.js backend.
  • TypeScript-first — full types ship in the package, autocomplete works out of the box.
  • Hosted login UIhttps://id.locins.com/login handles the entire register/login/MFA/consent flow. Your user clicks "Sign in with Locins", we show our UI, then bounce them back to your callback with tokens.

Getting Credentials

Register your app in our developer portal — self-service, free.

  1. Sign up at https://id.locins.com (you'll need a Locins account; create one if needed).
  2. Go to https://id.locins.com/developer/apps.
  3. Click New App, fill in:
    • App name (shown on the consent screen)
    • App type: public (SPA/mobile, no backend) or confidential (with backend that can keep a secret)
    • Redirect URIs (the URL we send users back to after login)
    • Scopes (which user data your app can access)
  4. Submit for review. Admins approve within 24-48 hours.
  5. Once approved, copy your client_id from the app detail page. Confidential apps additionally generate a client_secret.

No email, no Discord, no waiting list. The portal is the only way in.


App Types

Choose based on whether your app has a backend that can keep secrets.

Public (SPA / Mobile / Capacitor / AI-builders like Lovable)

Apps that run entirely on the user's device. You get a client_id only — no secret. PKCE protects the token exchange instead.

User        Your App (Browser/Mobile)           LocinsID
  |  click login  |                                  |
  |               |  generate PKCE verifier          |
  |               |                                  |
  |               |  redirect with code_challenge    |
  |               |--------------------------------->|
  |  login + consent in hosted UI                    |
  |<----- redirect with code ------------------------|
  |               |  exchange code + verifier        |
  |               |--------------------------------->|
  |               |  access_token                    |
  |               |<---------------------------------|

Use LocinsAuth (browser) or LocinsExpoAuth (React Native).

Confidential (Backend / SSR / Workers)

Apps with a backend that can keep a client_secret safely. Token exchange happens server-side; the SPA frontend only sees access tokens.

You get client_id + client_secret.

Use LocinsAuthCore on the server. Still requires PKCE (OAuth 2.1 mandates it for all clients).


Installation

npm install @locins/id
# or
pnpm add @locins/id
# or
yarn add @locins/id

Requirements: Node.js ≥ 18 (Web Crypto API + fetch)


Subpath Exports

| Import | Platform | Peer deps | | ------------------ | ------------------- | ------------------ | | @locins/id | Universal | — | | @locins/id/react | React 18+ | react | | @locins/id/expo | Expo / React Native | expo-web-browser |


Quick Start — Browser SPA (public client)

PKCE is handled automatically.

import { LocinsSDK } from '@locins/id'

const sdk = new LocinsSDK({
  clientId: 'app_abc123',
  redirectUri: 'https://myapp.com/callback',
  scope: ['identity', 'profile:read'],
})

// Popup login (recommended — preserves app state)
const { user, tokens } = await sdk.auth.loginWithPopup()

// Or redirect login (navigates away)
await sdk.auth.login()

// After redirect login, on your /callback route:
const result = await sdk.auth.handleCallback()

// Authenticated API calls
const profile = await sdk.users.me()
const consents = await sdk.consent.list()

redirectUri defaults to window.location.origin + '/callback' when omitted — great for AI-builder previews.

Quick Start — Backend (confidential client)

import { LocinsAuthCore } from '@locins/id'

const auth = new LocinsAuthCore({
  clientId: 'app_abc123',
  clientSecret: process.env.LOCINS_CLIENT_SECRET!,
  redirectUri: 'https://myapp.com/api/callback',
  scope: ['identity', 'profile:read'],
})

// Build auth URL (you also need a PKCE code_challenge per session)
const authUrl = auth.buildAuthUrl(csrfState, codeChallenge)

// Exchange the code server-side
const { user, tokens } = await auth.exchangeCode(code, codeVerifier)

// Refresh when needed
const newTokens = await auth.refreshToken()

React

import { LocinsProvider, useLocins } from '@locins/id/react'

function App() {
  return (
    <LocinsProvider clientId="app_abc123" redirectUri="https://myapp.com/callback">
      <Dashboard />
    </LocinsProvider>
  )
}

function Dashboard() {
  const { user, isLoggedIn, isLoading, loginWithPopup, logout } = useLocins()

  if (isLoading) return <p>Loading...</p>
  if (!isLoggedIn) {
    return <button onClick={() => loginWithPopup()}>Sign in with Locins</button>
  }
  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Sign out</button>
    </div>
  )
}

useLocins()

| Property | Type | Description | | ------------------ | --------------------------------------------- | ---------------------------------------- | | user | LocinsUser \| null | Authenticated user profile | | isLoggedIn | boolean | Whether the user is authenticated | | isLoading | boolean | true during init and callback handling | | organizations | LocinsOrganization[] | User's organizations | | login(opts?) | (o?: { state?, purpose? }) => Promise<void> | Redirect login | | loginWithPopup() | (o?: { purpose? }) => Promise<void> | Popup login | | logout() | () => Promise<void> | Clear tokens + best-effort server revoke | | auth | LocinsAuth | Direct access to auth client |

LocinsProvider Props

| Prop | Type | Default | Description | | ------------- | ------------------------ | ------------------------- | ------------------------------- | | clientId | string | — | Your app's client ID (required) | | redirectUri | string | window.origin + /callback | OAuth callback URL | | baseUrl | string | https://id.locins.com | LocinsID API URL | | scope | string[] | [] | Scopes to request | | storage | TokenStorage | LocalStorageAdapter | Custom token storage | | onError | (error: Error) => void | — | Error callback |


Expo / React Native

import { LocinsExpoAuth } from '@locins/id/expo'
import * as WebBrowser from 'expo-web-browser'

const auth = new LocinsExpoAuth({
  clientId: 'app_abc123',
  redirectUri: 'myapp://callback',
  scope: ['identity'],
  webBrowser: WebBrowser,
})

const { user, tokens } = await auth.login()

PKCE is generated automatically. Tokens are stored in memory by default — pass a storage adapter (e.g. wrapping expo-secure-store) for persistence.


API Reference

LocinsSDK

Top-level convenience facade. Wraps LocinsAuth plus the typed API clients.

const sdk = new LocinsSDK({
  clientId: string,         // Required — issued by the developer portal
  redirectUri?: string,     // Optional — defaults to window.location.origin + '/callback' in browsers
  baseUrl?: string,         // Optional — defaults to https://id.locins.com
  scope?: string[],         // Optional — defaults to []
  storage?: TokenStorage,   // Optional — LocalStorageAdapter in browsers, MemoryStorageAdapter in Node
})

redirectUri is optional in browser contexts (the SDK reads window.location.origin and appends /callback). It is required in Node/SSR/Worker contexts where there is no window to read from.

LocinsSDK is browser-targeted and intentionally has no clientSecret field. Confidential clients (those with a server-side secret) must use LocinsAuthCore directly — see the "Backend (confidential client)" example above.

| Property | Type | Description | | --------------- | -------------- | -------------------------------- | | sdk.auth | LocinsAuth | Login, logout, token management | | sdk.users | UsersApi | User profile operations | | sdk.consent | ConsentApi | Consent management | | sdk.directory | DirectoryApi | Public LocinsID directory search |

LocinsAuth / LocinsAuthCore

| Method | Returns | Description | | ------------------------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------- | | login(opts?: {state?, purpose?}) | Promise<void> | Redirect login | | loginWithPopup(opts?: {purpose?}) | Promise<AuthCallbackResult> | Popup login (browser only) | | handleCallback() | Promise<AuthCallbackResult \| null> | Handle redirect callback | | exchangeCode(code, verifier?) | Promise<AuthCallbackResult> | Exchange code for tokens (server-side) | | refreshToken() | Promise<LocinsTokens> | Refresh access token | | loadGrantedScopes() | Promise<string[]> | Fetch admin-approved scope set; throws AppNotReadyError if app is not approved | | getUser(token?) | Promise<LocinsUser> | Fetch user from /oauth/userinfo | | getTokens() | LocinsTokens \| null | Current token pair | | initialize() | Promise<void> | Load tokens from storage (idempotent) | | logout() | Promise<void> | Clear tokens + best-effort server revoke | | buildAuthUrl(state, challenge, purpose?) | string | Build authorization URL (PKCE required) | | authenticatedFetch(url, init?) | Promise<Response> | fetch with auto-refresh on 401 |

The purpose parameter

login() and buildAuthUrl() accept an optional purpose: 'private' | 'developer' | 'company'. LocinsID's hosted UI branches its onboarding/login flow based on this signal. Use it when your app is purpose-specific (e.g., a developer dashboard passes 'developer', a consumer app passes 'private').

UsersApi

| Method | Returns | Description | | --------------------- | --------------------- | ------------------------------------ | | me() | Promise<LocinsUser> | Current user profile | | updateProfile(data) | Promise<LocinsUser> | Update name, bio, locale, avatar_url |

ConsentApi

| Method | Returns | Description | | ---------------------- | ------------------------------- | ------------------------ | | list() | Promise<ConsentEntry[]> | All active consents | | getForApp(appId) | Promise<ConsentEntry \| null> | Consent for specific app | | grant(appId, scopes) | Promise<ConsentEntry> | Grant consent | | revoke(appId) | Promise<void> | Revoke consent |

DirectoryApi

Public, unauthenticated reads of LocinsID's directory.

| Method | Returns | Description | | ----------------------------- | ---------------------------------- | ------------------------------------------------- | | searchPublic({ q, limit? }) | Promise<DirectorySearchResult[]> | Search profiles by name/username | | getPublicProfile(handle) | Promise<PublicProfile \| null> | Public profile by handle (null when not findable) | | getPublicDeck(handle) | Promise<PublicDeck \| null> | A user's public ID-card deck (null when missing) |


Types

interface LocinsUser {
  id: string
  sub: string
  email: string
  email_verified: boolean
  name: string | null
  display_name: string | null
  username: string | null
  first_name: string | null
  last_name: string | null
  avatar_url: string | null
  locale: string
  bio?: string | null
  occupation?: string | null
  organizations?: LocinsOrganization[]
  family_members?: LocinsFamilyMember[]
}

interface LocinsOrganization {
  id: string
  name: string
  slug: string
  role: 'owner' | 'member'
}

interface LocinsTokens {
  access_token: string
  refresh_token: string
  expires_in?: number
  issued_at?: number
}

interface AuthCallbackResult {
  user: LocinsUser
  tokens: LocinsTokens
}

interface TokenStorage {
  getItem(key: string): Promise<string | null>
  setItem(key: string, value: string): Promise<void>
  removeItem(key: string): Promise<void>
}

type LocinsPurpose = 'private' | 'developer' | 'company'

Custom Token Storage

By default, tokens are stored in localStorage (browser) or in-memory (Node/Expo). Provide an adapter for SecureStore, AsyncStorage, etc.:

import { LocinsSDK } from '@locins/id'

const storage: TokenStorage = {
  getItem: (key) => AsyncStorage.getItem(key),
  setItem: (key, value) => AsyncStorage.setItem(key, value),
  removeItem: (key) => AsyncStorage.removeItem(key),
}

const sdk = new LocinsSDK({
  clientId: 'app_abc123',
  redirectUri: 'https://myapp.com/callback',
  storage,
})

Built-in adapters exported by the package:

  • MemoryStorageAdapter — non-persistent (Node default, Expo default)
  • SessionStorageAdaptersessionStorage (clears on tab close)
  • LocalStorageAdapterlocalStorage (browser default for LocinsAuth)

Error Handling

All SDK errors throw LocinsAuthError with a code and message. App-status and scope mismatches throw narrower subclasses.

import { LocinsAuthError, AppNotReadyError, ScopeNotGrantedError } from '@locins/id'

try {
  await sdk.auth.loginWithPopup()
} catch (err) {
  if (err instanceof AppNotReadyError) {
    // App is not yet approved by Locins admin
  } else if (err instanceof ScopeNotGrantedError) {
    // You requested a scope the admin did not approve
  } else if (err instanceof LocinsAuthError) {
    switch (err.code) {
      case 'popup_blocked':
        /* fallback to redirect login */ break
      case 'popup_closed':
        /* user closed the popup */ break
      case 'auth_failed':
        /* code exchange or state mismatch */ break
      case 'refresh_failed':
        /* refresh expired/revoked — re-login */ break
      case 'timeout':
        /* 10s timeout */ break
    }
  }
}

| Code | Description | | ----------------- | ------------------------------------------------- | | auth_failed | Code exchange or authentication failed | | no_token | No access/refresh token available | | refresh_failed | Token refresh failed — user needs to log in again | | userinfo_failed | Failed to fetch user profile | | popup_blocked | Browser blocked the auth popup | | popup_closed | User closed the auth popup | | callback_failed | Native auth session cancelled or returned no code | | invalid_config | Missing required config (e.g., PKCE challenge) | | timeout | Request timed out (10s default) |


Scopes

Request only what you need. Users approve each scope on the consent screen.

| Scope | Description | Tier | | ------------------- | ----------------------------------------------- | ------- | | identity | ID, email, name, avatar | Public | | profile:read | Bio, occupation, languages | Public | | profile:write | Update profile information | Trusted | | health:read | Allergies, blood type, medications | Public | | health:write | Update health card | Trusted | | family:read | Partner, children, pets | Public | | family:write | Update family information | Trusted | | company:read | Which companies the user is a member of | Public | | consent:read | View which apps have access | Public | | consent:manage | Grant or revoke app access | Trusted | | travel:read | Travel identity: visited countries, bucket list | Public | | travel:write | Update travel identity | Trusted | | connections:read | Connections and encounters | Public | | connections:write | Create and manage connections | Trusted | | directory:search | Search the Locins directory (SDK-only) | Public |

Trusted tier requires app_type: confidential AND a verified domain. Public apps cannot request trusted scopes; they will be rejected at admin review.


Security

| | | | --------------------- | ------------------------------------------------------------------------ | | Authentication | Every app must have a registered, admin-approved client_id | | PKCE | Mandatory for all clients (S256, OAuth 2.1) | | Client Secret | Confidential clients only; bcrypt-hashed server-side; 24h rotation grace | | Redirect URI | Exact-match against registered URIs (HTTPS in production) | | State Validation | CSRF-protected via state parameter | | Scope Enforcement | Server rejects scopes outside the admin-approved set | | Token Storage | LocalStorageAdapter browser default — override for stricter contexts | | Access Token | JWT, 15 min expiry | | Refresh Token | Rotating UUID, 30 day expiry, single-use, bound to a WorkOS session | | Token Refresh | Automatic on 401, concurrent-safe (deduplicated) | | Request Timeouts | All API calls timeout after 10 seconds | | Rate Limiting | 20 req/min per client_id on /oauth/token |


Versioning

Semver. Breaking changes only on majors. See CHANGELOG.md.


Sign in with Locins (one-liner)

import { LocinsProvider, SignInWithLocinsButton } from '@locins/id/react'

function App() {
  return (
    <LocinsProvider clientId="app_abc123">
      <SignInWithLocinsButton />
    </LocinsProvider>
  )
}

Props: variant: 'light' | 'dark' | 'outline', size: 'sm' | 'md' | 'lg', label?: string, purpose?: 'private' | 'developer' | 'company', usePopup?: boolean, onError?: (err) => void. Ships its own CSS — works without Tailwind. See brand/BUTTON_GUIDELINES.md for design rules.


Next.js (App Router)

LocinsProvider is a Client Component. Mark it explicitly with 'use client', then drop it into your root layout.

// app/locins-provider.tsx
'use client'
import { LocinsProvider } from '@locins/id/react'

export function Provider({ children }: { children: React.ReactNode }) {
  return (
    <LocinsProvider
      clientId={process.env.NEXT_PUBLIC_LOCINS_CLIENT_ID!}
      redirectUri="https://yourapp.com/callback"
      scope={['identity', 'profile:read']}
    >
      {children}
    </LocinsProvider>
  )
}
// app/layout.tsx
import { Provider } from './locins-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  )
}
// app/callback/page.tsx
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useLocins } from '@locins/id/react'

export default function CallbackPage() {
  const { isLoading, isLoggedIn } = useLocins()
  const router = useRouter()
  useEffect(() => {
    if (!isLoading && isLoggedIn) router.replace('/dashboard')
  }, [isLoading, isLoggedIn, router])
  return <p>Signing you in…</p>
}

Set NEXT_PUBLIC_LOCINS_CLIENT_ID=app_abc123 in .env.local. No backend required.


React SPA + NestJS Backend

Two patterns:

Pattern A — frontend-only PKCE (simplest): the SPA does the full login flow with LocinsSDK; the Nest.js backend validates each request by calling /oauth/userinfo with the access token, or by verifying JWTs locally against our JWKS at https://id.locins.com/.well-known/jwks.json.

Pattern B — confidential token exchange (most secure): the SPA redirects to LocinsID, the SPA forwards the code to your Nest.js backend, the backend uses LocinsAuthCore with clientSecret to exchange the code for tokens, and stores the refresh token in an HttpOnly cookie. Tokens never touch the browser.

Pattern A is enough for most apps. Use B if you need refresh tokens off the client.

// nest backend — pattern B
import { LocinsAuthCore } from '@locins/id'

const auth = new LocinsAuthCore({
  clientId: process.env.LOCINS_CLIENT_ID!,
  clientSecret: process.env.LOCINS_CLIENT_SECRET!,
  redirectUri: 'https://yourapp.com/api/callback',
})

// POST /api/callback { code, codeVerifier }
@Post('callback')
async callback(@Body() body: { code: string; codeVerifier: string }) {
  const { user, tokens } = await auth.exchangeCode(body.code, body.codeVerifier)
  // store refresh in HttpOnly cookie, return access token to SPA
}

Capacitor (iOS + Android)

Capacitor's WebView blocks cross-origin popups, so use the system browser via @capacitor/browser instead of loginWithPopup. Build the auth URL yourself with PKCE.

import { Browser } from '@capacitor/browser'
import { LocinsAuth } from '@locins/id'

const auth = new LocinsAuth({
  clientId: 'app_abc123',
  redirectUri: 'com.yourapp://callback', // deep link
})

await Browser.open({ url: await buildAuthUrlWithPkce(auth) })

// In your deep-link handler:
App.addListener('appUrlOpen', async (event) => {
  const url = new URL(event.url)
  const code = url.searchParams.get('code')
  if (code) {
    await Browser.close()
    const result = await auth.exchangeCode(code, verifier)
  }
})

Register the deep-link scheme in capacitor.config.ts and add it as a redirect URI in the developer portal.


AI Builders (Lovable, Bolt, v0, Replit Agent)

@locins/id is designed to drop straight into AI-built React/Vite/Next sites. Three things to know:

  1. No backend required. PKCE means the SPA handles the entire flow.
  2. Inline the clientId. AI builders don't always set up .env files. const CLIENT_ID = 'app_abc123' in a .ts file is fine.
  3. Register your preview AND production URLs. Builder preview URLs often live on a shared domain (e.g. *.lovable.app). The developer portal accepts wildcard redirect URIs on trusted AI-builder hosts — register https://*.lovable.app/callback (or bolt.new, v0.dev, replit.app) alongside your production URL.

LLM prompt for AI builders: copy https://id.locins.com/developer/docs → "Copy LLM prompt" → paste into your AI builder before asking it to integrate.


License

MIT