npm package: @locins/sdk
Description: Official SDK for LocinsID — Auth, Users, Consent
Latest version: 1.0.11

# @locins/sdk

**Official SDK for LocinsID** — OAuth2/PKCE authentication, user management, and consent APIs for the Locins ecosystem.

[![npm](https://img.shields.io/npm/v/@locins/sdk?color=384535&label=npm)](https://www.npmjs.com/package/@locins/sdk)
[![downloads](https://img.shields.io/npm/dm/@locins/sdk?color=436373)](https://www.npmjs.com/package/@locins/sdk)
[![license](https://img.shields.io/npm/l/@locins/sdk?color=4b4130)](./LICENSE)
![TypeScript](https://img.shields.io/badge/TypeScript-strict-384535)
![ESM](https://img.shields.io/badge/ESM-only-436373)

---

## Getting Credentials

To use this SDK, you need a `client_id` issued by the Locins team. Without credentials, the SDK cannot authenticate.

**Contact [partner@locins.com](mailto:partner@locins.com)** with:
- Your app name and description
- Redirect URIs (callback URLs)
- Requested scopes (see [Scopes](#scopes))
- Your app type (see below)

> Self-service partner portal coming soon.

---

## App Types

When registering your app, you choose one of two types. This determines how your app authenticates on the token endpoint.

### Confidential (Server + Client)

For apps that have a **backend server**. The server can securely store a `client_secret` that never leaves the server. The token exchange happens server-side.

You receive: `client_id` + `client_secret`

```text
User        Your Frontend       Your Backend        LocinsID
  |  click login  |                   |                  |
  |-------------->                    |                  |
  |  redirect to LocinsID             |                  |
  |---------------------------------------------------->|
  |  login + consent                  |                  |
  |<---- redirect with code ---------|------------------|
  |  send code    |                   |                  |
  |-------------->  forward code      |                  |
  |               |------------------>|                  |
  |               |  exchange code + client_secret       |
  |               |                   |----------------->|
  |               |                   |  access_token    |
  |               |                   |<-----------------|
```

### Public (Client only)

For **SPAs and mobile apps** that run entirely on the user's device. These apps cannot store secrets safely — any secret in your JavaScript bundle or app binary is visible to everyone.

Instead of a secret, these apps use **PKCE** (Proof Key for Code Exchange). The SDK generates a unique, one-time cryptographic verifier for each login attempt. Even if an attacker intercepts the authorization code, they cannot exchange it without the verifier.

You receive: `client_id` only (no secret)

```text
User        Your App (Browser/Mobile)           LocinsID
  |  click login  |                                  |
  |               |  generate PKCE verifier          |
  |               |  (stays in memory)               |
  |               |                                  |
  |               |  redirect with code_challenge    |
  |               |--------------------------------->|
  |  login + consent                                 |
  |<----- redirect with code ------------------------|
  |               |                                  |
  |               |  exchange code + verifier         |
  |               |--------------------------------->|
  |               |  SHA256(verifier) == challenge?   |
  |               |  access_token                    |
  |               |<---------------------------------|
```

**Why is this secure?**
- The `code_challenge` goes over the URL but is just a hash — useless without the original verifier
- The `code_verifier` stays in browser memory and is never transmitted until the code exchange
- Each verifier is single-use and bound to one authorization code
- Additionally, your `client_id` must be registered and approved, and the `redirect_uri` must exactly match what was registered

---

## Installation

```bash
npm install @locins/sdk
```

```bash
pnpm add @locins/sdk
```

**Requirements:** Node.js >= 18 (Web Crypto API + `fetch`)

---

## Subpath Exports

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

---

## Usage

### Public Client (SPA / Mobile)

PKCE is handled automatically by the SDK.

```ts
import { LocinsSDK } from '@locins/sdk'

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

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

// Or redirect login
await sdk.auth.login()
```

### Confidential Client (Backend)

```ts
import { LocinsAuthCore } from '@locins/sdk'

const auth = new LocinsAuthCore({
  clientId: 'app_abc123',
  clientSecret: 'secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  redirectUri: 'https://myapp.com/api/callback',
})

// Build auth URL for the frontend to redirect to
const authUrl = auth.buildAuthUrl(state)

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

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

---

## React

```tsx
import { LocinsProvider, useLocins } from '@locins/sdk/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(state?)` | `() => Promise<void>` | Redirect login |
| `loginWithPopup()` | `() => Promise<void>` | Popup login |
| `logout()` | `() => void` | Clear session |
| `auth` | `LocinsAuth` | Direct access to auth client |

### LocinsProvider Props

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

---

## Expo / React Native

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

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

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

PKCE is generated automatically.

---

## API Reference

### `LocinsSDK`

```ts
const sdk = new LocinsSDK({
  clientId: string,         // Required — issued by Locins team
  clientSecret?: string,    // Only for confidential clients
  redirectUri?: string,
  baseUrl?: string,
})
```

| Property | Type | Description |
|----------|------|-------------|
| `sdk.auth` | `LocinsAuth` | Login, logout, token management |
| `sdk.users` | `UsersApi` | User profile operations |
| `sdk.consent` | `ConsentApi` | Consent management |

### `LocinsAuth` / `LocinsAuthCore`

| Method | Returns | Description |
|--------|---------|-------------|
| `login(state?)` | `Promise<void>` | Redirect login |
| `loginWithPopup()` | `Promise<AuthCallbackResult>` | Popup login |
| `handleCallback()` | `Promise<AuthCallbackResult \| null>` | Handle redirect callback |
| `exchangeCode(code, verifier?)` | `Promise<AuthCallbackResult>` | Exchange code for tokens |
| `refreshToken()` | `Promise<LocinsTokens>` | Refresh access token |
| `getUser(token?)` | `Promise<LocinsUser>` | Fetch user from `/oauth/userinfo` |
| `getTokens()` | `LocinsTokens \| null` | Current token pair |
| `initialize()` | `Promise<void>` | Load tokens from storage |
| `logout()` | `Promise<void>` | Clear tokens and user |
| `buildAuthUrl(state?, challenge?)` | `string` | Build authorization URL |

### `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<Consent[]>` | All active consents |
| `getForApp(appId)` | `Promise<Consent>` | Consent for specific app |
| `grant(appId, scopes)` | `Promise<void>` | Grant consent |
| `revoke(appId)` | `Promise<void>` | Revoke consent |

---

## Types

```ts
interface LocinsUser {
  id: string
  email: string
  name: string | null
  avatar_url: string | null
  locale: string
  bio?: string | null
  organizations?: LocinsOrganization[]
  family_members?: LocinsFamilyMember[]
}

interface LocinsTokens {
  access_token: string
  refresh_token: string
}

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>
}
```

---

## Custom Token Storage

Tokens are stored in `sessionStorage` (browser) or in-memory (Node/Expo) by default. Pass a custom adapter for other backends:

```ts
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: '/callback',
  storage,
})
```

---

## Scopes

| Scope | Description | Access |
|-------|-------------|--------|
| `identity` | ID, email, name, avatar | All apps |
| `profile:read` | Bio, occupation, languages | All apps |
| `profile:write` | Update profile | Trusted only |
| `health:read` | Allergies, blood type, medications | All apps |
| `health:write` | Update health data | Trusted only |
| `family:read` | Partner, children, pets | All apps |
| `family:write` | Update family data | Trusted only |
| `org:read` | User organizations | All apps |
| `org:write` | Manage organizations | Trusted only |
| `consent:read` | View app permissions | All apps |
| `consent:manage` | Grant/revoke permissions | Trusted only |

---

## Security

| | |
|---|---|
| **Authentication** | Every app must have a registered and approved `client_id` |
| **PKCE** | Auto-generated for public clients (SHA-256 + base64url) |
| **Client Secret** | Required for confidential clients, verified via bcrypt |
| **Redirect URI** | Must exactly match a registered URI |
| **Scope Enforcement** | Only admin-approved scopes can be requested |
| **Token Storage** | `sessionStorage` or memory — never `localStorage` |
| **Token Refresh** | Automatic on 401, single-use rotation |
| **Access Token** | JWT, 15 min expiry |
| **Refresh Token** | UUID, 30 day expiry, revoked on use |

---

## License

MIT