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

@loka-sms/sso

v1.2.0

Published

SSO utilities, hooks, and components for Loka SMS modules (OAuth2 PKCE, cross-app logout, infinite-loop protection)

Readme

@loka-sms/sso

SSO utilities, hooks, and components for Loka SMS modules.

Ticket exchange + localStorage auth for cross-app navigation. OAuth2 PKCE for third-party/external apps.

Install

npm install @loka-sms/sso

Quick Start — Cross-App Ticket Transfer

Gateway issues a short one-time ticket. The target app receives /auth/transfer?ticket=..., exchanges it for accessToken + refreshToken, stores both in localStorage, then redirects.

1. Navigate from source app

import { navigateToApp } from '@loka-sms/sso';

await navigateToApp({
  targetUrl: import.meta.env.VITE_ADMIN_URL,
  apiBase: import.meta.env.VITE_API_URL || '/api',
  clientId: 'core',
  targetBlank: true,
});

navigateToApp() matches the current Gateway contract:

| Step | Request | |------|---------| | Issue ticket | POST /api/sso/issue-ticket with body { client_id, redirect } and Authorization: Bearer <accessToken> | | Open app | <target-origin>/auth/transfer?ticket=<uuid>&next=<path> |

The helper opens about:blank synchronously first when targetBlank=true, so browser popup blockers do not block the cross-app navigation.

Use the lower-level helper if you only need a ticket:

import { issueTicket } from '@loka-sms/sso';

const { ticket, expiresIn } = await issueTicket({
  apiBase: '/api',
  clientId: 'core',
  redirect: 'https://admin.example.com',
});

2. Handle transfer in target app

import { OAuthTransfer } from '@loka-sms/sso';

<Route
  path="/auth/transfer"
  element={<OAuthTransfer apiBase="/api" clientId="administrasi" />}
/>

Supported URL formats:

| URL | Behavior | |-----|----------| | /auth/transfer?ticket=<uuid>&next=/ | Exchanges ticket at POST /sso/exchange, stores tokens, redirects to next | | /auth/transfer?token=<jwt>&refreshToken=<jwt> | Legacy fallback, stores URL tokens directly |

The component is guarded with useRef, so React dev-mode double effects do not consume single-use tickets twice.

Quick Start — Same-Domain App

App runs on same domain as Gateway (e.g. 10.7.1.82). Cookie sms_ac_token can still be used, but localStorage is now the primary app-side cache.

1. Setup API client

// src/api/client.ts
import axios from 'axios';
import { createAuthInterceptor } from '@loka-sms/sso';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || '/api',
  withCredentials: true,
});

api.interceptors.request.use(createAuthInterceptor());
export default api;

Headers automatically attached:

| Header | Source | |--------|--------| | Authorization: Bearer <token> | Cookie sms_ac_token → localStorage fallback | | X-School-ID | localStorage school_sms_id | | X-User-ID | localStorage user_id | | X-User-Role | localStorage user_role | | X-Device-ID | localStorage device_sms_id | | X-Request-ID | crypto.randomUUID() |

2. Setup Auth Store

// src/stores/authStore.ts
function getCookie(name: string): string {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? decodeURIComponent(match[2]) : '';
}

function rehydrateUser() {
  const cached = localStorage.getItem('my_user');
  if (cached) return JSON.parse(cached);

  const token = getCookie('sms_ac_token');
  if (!token) return null;

  const payload = JSON.parse(atob(token.split('.')[1]));
  const user = {
    sub: payload.sub, email: payload.email, fullName: payload.fullName,
    role: payload.role, schoolId: payload.schoolId || '',
  };
  localStorage.setItem('my_user', JSON.stringify(user));
  return user;
}

3. Add routes

import { OAuthCallback } from '@loka-sms/sso';
import SignIn from './pages/SignIn';

<Route path="/signin" element={<SignIn />} />
<Route path="/auth/callback" element={<OAuthCallback clientId="my-module" />} />

4. Listen for cross-app logout

import { useCrossAppLogout } from '@loka-sms/sso';

function App() {
  useCrossAppLogout(); // listens on BroadcastChannel 'loka-sso-logout'
  // ...
}

Quick Start — Third-Party App (OAuth2 PKCE)

App on different domain from Gateway. Full PKCE flow required.

1. Redirect user to authorize

import { generateCodeVerifier, generateCodeChallenge, generateState } from '@loka-sms/sso';

const verifier = generateCodeVerifier();
const state = generateState();
sessionStorage.setItem(`oauth_verifier_${state}`, verifier);

const challenge = await generateCodeChallenge(verifier);
const params = new URLSearchParams({
  client_id: 'my-app',
  redirect_uri: 'https://my-app.com/auth/callback',
  response_type: 'code',
  code_challenge: challenge,
  code_challenge_method: 'S256',
  state,
});
window.location.href = `https://gateway.loka.id/api/oauth/authorize?${params}`;

2. Handle callback

import { OAuthCallback } from '@loka-sms/sso';

<Route path="/auth/callback" element={<OAuthCallback clientId="my-app" />} />

Or use the hook directly:

import { useOAuthCallback } from '@loka-sms/sso';

function AuthCallback() {
  const { error, loading, phase } = useOAuthCallback({
    clientId: 'my-app',
    apiBase: 'https://gateway.loka.id/api',
    redirectPath: '/dashboard',
  });
  // ...
}

API Reference

Interceptor

| Export | Description | |--------|-------------| | createAuthInterceptor() | Axios request interceptor. Reads token cookie-first, attaches Authorization + context headers. |

Hooks

| Export | Description | |--------|-------------| | useCrossAppLogout() | Listens on BroadcastChannel loka-sso-logout. Clears auth state and redirects to /signin. Built-in 2-second dedup to prevent re-broadcast loops. | | useOAuthCallback(input) | Handles OAuth2 PKCE code exchange. Returns { error, loading, phase }. Awaits cookie sync before redirect to prevent landing-page race. | | useIssueTicket(apiBase) | Issues a one-time ticket using current localStorage/cookie access token. | | useNavigateToApp(apiBase) | React state wrapper for navigateToApp(). |

Ticket Utilities

| Export | Description | |--------|-------------| | issueTicket(options) | Calls POST /sso/issue-ticket with { client_id, redirect }. | | exchangeTicket(options) | Calls POST /sso/exchange with { ticket, clientId }. | | navigateToApp(options) | Issues ticket, opens target /auth/transfer?ticket=...&next=.... | | buildTransferUrl(targetUrl, ticket, next?) | Builds the target app transfer URL without network calls. |

useOAuthCallback input:

| Field | Type | Default | Description | |-------|------|---------|-------------| | clientId | string | required | OAuth2 client ID | | apiBase | string | '/api' | Gateway API base URL | | redirectPath | string | '/' | Redirect path after success |

Components

| Export | Description | |--------|-------------| | <OAuthCallback clientId /> | Drop-in OAuth2 PKCE callback page. Wraps useOAuthCallback. | | <OAuthTransfer /> | Handles ticket exchange (?ticket=) and legacy token-in-URL fallback. |

PKCE Utilities

| Export | Description | |--------|-------------| | generateCodeVerifier() | Generate cryptographically random code verifier | | generateCodeChallenge(verifier) | SHA-256 hash → base64url encode | | generateState() | Generate random state string for CSRF protection | | sha256(input) | Pure JS SHA-256 implementation |

Constants

| Export | Description | |--------|-------------| | SSO_STORAGE_KEYS | Standard localStorage key names | | SSO_CHANNELS | BroadcastChannel names (loka-sso-logout, loka-sso-login, loka-school-change) | | API_HEADERS | Standard API header names |

Architecture

Cross-app modules → short ticket in URL → `/sso/exchange` → localStorage tokens
Same-domain apps (10.7.1.82:*) → cookie/localStorage token → auto-login
Third-party apps (external domain) → OAuth2 PKCE → code exchange → token

Ticket URLs should carry only a short one-time ticket, never the full JWT. See docs/flow_baru.md for full architecture.