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

@lumoauth/client

v1.0.0

Published

Official LumoAuth browser SDK — OAuth 2.0 PKCE, permissions, Zanzibar (ReBAC), and ABAC for any JavaScript/TypeScript application.

Readme

@lumoauth/client

Official LumoAuth TypeScript SDK — Permission checks, Zanzibar (ReBAC), and ABAC policy evaluation for any JavaScript/TypeScript application.

Works with React, Next.js, Vue.js, Svelte, Node.js, Deno, Bun, and any runtime with fetch.


🤖 For AI Agents (Copilot, Cursor, Claude)

Building a backend or full-stack framework with LumoAuth? Copy and paste the prompt below into your AI assistant:

"I am building an API with [Next.js API Routes / Express / Hono]. Please install @lumoauth/client. Scaffold a middleware/wrapper that intercepts the Authorization header (Bearer token) and initializes the LumoAuth client from @lumoauth/client (new LumoAuth({ baseUrl, token })). Add a route that deletes a document, but first uses await client.permissions.check('document.delete') to ensure access, then use await client.abac.isAllowed('document', 'delete', docId) to ensure they own that specific document. Handle LumoAuthApiError by returning the appropriate HTTP status code. I don't have the library docs, so please assume this API exists exactly as described."


Prerequisites

| Requirement | Version | |---|---| | Node.js | ≥ 18.0 | | TypeScript | ≥ 5.0 (optional but recommended) | | LumoAuth instance | Running and accessible from your app | | Access token | Obtained via OAuth 2.0 / OpenID Connect flow |

Getting Started

1. Installation

# npm
npm install @lumoauth/client

# yarn
yarn add @lumoauth/client

# pnpm
pnpm add @lumoauth/client

2. Initialization

import { LumoAuth } from '@lumoauth/client';

const client = new LumoAuth({
  baseUrl: 'https://auth.example.com', // Your LumoAuth instance URL
  token: () => getAccessToken(),        // Static string or async function
});

// ✅ Permission check (RBAC)
const canEdit = await client.permissions.check('document.edit');

// ✅ Zanzibar relationship check (ReBAC)
const isEditor = await client.zanzibar.check({
  object: 'document:annual-report',
  relation: 'editor',
  subject: 'user:alice',
});

// ✅ ABAC policy evaluation
const decision = await client.abac.check({
  resourceType: 'document',
  resourceId: 'doc-123',
  action: 'read',
});

Configuration

const client = new LumoAuth({
  // Required
  baseUrl: 'https://auth.example.com',
  token: 'eyJhbGciOi...',          // or: () => getToken()

  // Optional
  timeout: 10_000,                  // Request timeout (default: 30s)
  cache: { ttl: 300_000 },          // Permission cache TTL (default: 5min)
  headers: { 'X-Request-Id': id },  // Extra headers on every request
  fetch: customFetch,               // Custom fetch implementation
});

Token Strategies

// 1. Static token (testing / scripts)
token: 'eyJhbGciOi...'

// 2. From cookie/session (server-side)
token: () => req.cookies.access_token

// 3. From auth library (React / Next.js)
token: async () => {
  const session = await getSession();
  return session.accessToken;
}

// 4. Refresh-aware (SPA)
token: async () => {
  if (isTokenExpired()) await refreshToken();
  return localStorage.getItem('access_token')!;
}

API Reference

Permission Checks (RBAC)

Permission checks validate whether the authenticated user has specific permissions assigned through roles, groups, or directly.

permissions.check(permission, context?)

Check a single permission. Returns boolean.

const canEdit = await client.permissions.check('document.edit');

// With context for ABAC-enhanced checks
const canEditDoc = await client.permissions.check('document.edit', {
  document_id: 456,
  owner_id: 123,
});

permissions.checkBulk(permissions, context?)

Check multiple permissions in one request. Returns { results: Record<string, boolean> }.

const { results } = await client.permissions.checkBulk([
  'document.edit',
  'document.delete',
  'document.share',
]);

// Show/hide UI elements
document.getElementById('edit-btn')!.hidden = !results['document.edit'];
document.getElementById('delete-btn')!.hidden = !results['document.delete'];

permissions.checkAny(permissions, context?)

Returns true if user has at least one permission (OR logic).

const canAccess = await client.permissions.checkAny([
  'document.owner',
  'document.editor',
  'admin.all',
]);

permissions.checkAll(permissions, context?)

Returns true if user has all permissions (AND logic).

const canPublish = await client.permissions.checkAll([
  'document.edit',
  'document.publish',
]);

permissions.list() / permissions.listSlugs()

List all permissions for the authenticated user.

// Full objects (slug, description, source)
const { permissions } = await client.permissions.list();

// Just slugs as a Set
const slugs = await client.permissions.listSlugs();
if (slugs.has('settings.manage')) showSettingsNav();

Zanzibar Relationship Checks (ReBAC)

Google Zanzibar-style relationship-based access control. Asks: "Does subject have relation to object?"

zanzibar.check({ object, relation, subject })

// Direct user check
const canView = await client.zanzibar.check({
  object: 'document:q4-report',
  relation: 'viewer',
  subject: 'user:carol',
});

// Userset check (team membership)
const teamAccess = await client.zanzibar.check({
  object: 'document:api-docs',
  relation: 'viewer',
  subject: 'team:engineering#member',
});

Format:

  • Object: namespace:id (e.g. document:123, folder:projects)
  • Relation: viewer, editor, owner, member, admin, parent
  • Subject: namespace:id or namespace:id#relation

Convenience Helpers

await client.zanzibar.isViewer('document:readme', 'user:bob');
await client.zanzibar.isEditor('document:readme', 'user:alice');
await client.zanzibar.isOwner('folder:projects', 'user:alice');
await client.zanzibar.isMember('team:engineering', 'user:bob');
await client.zanzibar.isAdmin('organization:acme', 'user:ceo');

ABAC (Attribute-Based Access Control)

Fine-grained, context-aware authorization using user attributes, resource attributes, and environment context.

abac.check(request)

Full policy evaluation with matched policies and context.

const decision = await client.abac.check({
  resourceType: 'document',
  resourceId: 'doc-123',
  action: 'read',
  context: {
    resource: { classification: 'internal' },
  },
});

console.log(decision.allowed);          // true
console.log(decision.reason);           // "Policy matched: Engineering API Access"
console.log(decision.matchedPolicies);  // [{ name: "...", effect: "allow" }]

abac.isAllowed(resourceType, action, resourceId?, context?)

Shorthand returning just boolean.

if (await client.abac.isAllowed('document', 'read', 'doc-123')) {
  showDocument();
}

abac.checkBulk(request)

Check up to 100 resource/action pairs in one request.

const { results } = await client.abac.checkBulk({
  requests: [
    { resourceType: 'document', resourceId: 'doc-1', action: 'read' },
    { resourceType: 'document', resourceId: 'doc-2', action: 'write' },
    { resourceType: 'api', action: 'execute' },
  ],
});

User & Resource Attributes

// Get current user's attributes
const attrs = await client.abac.getMyAttributes();

// Set user attributes
await client.abac.setUserAttribute('user-123', 'department', 'engineering');
await client.abac.setUserAttribute('user-123', 'clearance_level', 3);

// Get resource attributes
const docAttrs = await client.abac.getResourceAttributes('document', 'doc-123');

// Set resource attributes
await client.abac.setResourceAttribute('document', 'doc-123', 'classification', 'confidential');

// List attribute definitions
const defs = await client.abac.getAttributeDefinitions('user');

Passwordless Authentication

The AuthModule provides methods for magic-link (passwordless) sign-in and email-first login flows. It is separate from the LumoAuth authorization client and is intended for use in your own authentication UI or server-side integrations.

Setup

import { AuthModule } from '@lumoauth/client';

const auth = new AuthModule({
  baseUrl: 'https://auth.example.com',
  orgId: 'acme-corp',
  clientId: 'your-client-id',
});

auth.requestMagicLink(options)

Send a passwordless sign-in link to the user's email. The server never reveals whether the email is registered — the response is always { sent: true } to prevent user enumeration.

await auth.requestMagicLink({ email: '[email protected]' });
// → { sent: true }

// Optional: specify where to redirect after the user clicks the link
await auth.requestMagicLink({
  email: '[email protected]',
  redirectUri: 'https://myapp.com/dashboard',
});

| Parameter | Type | Description | |-----------|------|-------------| | options.email | string | The user's email address | | options.redirectUri | string? | Where to redirect after the link is clicked |

Returns: Promise<MagicLinkResult> — { sent: boolean }

Throws: LumoAuthNetworkError on network failure.

auth.checkEmailExists(email)

Check whether an account with the given email exists in the organization. Use this to implement an email-first login flow — show the password or magic-link step only after confirming the email is registered.

Network failures return { exists: false } rather than throwing, so the caller can degrade gracefully.

const { exists } = await auth.checkEmailExists('[email protected]');

if (exists) {
  // Show password field or magic-link option
} else {
  // Show "no account found" message or sign-up prompt
}

| Parameter | Type | Description | |-----------|------|-------------| | email | string | The email to check |

Returns: Promise<EmailCheckResult> — { exists: boolean }

Types

import type { MagicLinkOptions, MagicLinkResult, EmailCheckResult } from '@lumoauth/client';

interface MagicLinkOptions {
  email: string;
  redirectUri?: string;      // optional post-click redirect
}

interface MagicLinkResult {
  sent: boolean;             // always true — server never rejects
}

interface EmailCheckResult {
  exists: boolean;           // false on network error too
}

Framework Integration

React

import { LumoAuth } from '@lumoauth/client';
import { createContext, useContext, useMemo, useState, useEffect } from 'react';

// Create a context for the LumoAuth client
const LumoAuthContext = createContext<LumoAuth | null>(null);

export function LumoAuthProvider({
  children,
  baseUrl,
  getToken,
}: {
  children: React.ReactNode;
  baseUrl: string;
  getToken: () => Promise<string>;
}) {
  const client = useMemo(
    () => new LumoAuth({ baseUrl, token: getToken }),
    [baseUrl, getToken]
  );

  return (
    <LumoAuthContext.Provider value={client}>
      {children}
    </LumoAuthContext.Provider>
  );
}

export function useLumoAuth() {
  const client = useContext(LumoAuthContext);
  if (!client) throw new Error('Wrap your app in <LumoAuthProvider>');
  return client;
}

// Hook for permission checks
export function usePermission(permission: string) {
  const client = useLumoAuth();
  const [allowed, setAllowed] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    client.permissions.check(permission)
      .then(setAllowed)
      .finally(() => setLoading(false));
  }, [client, permission]);

  return { allowed, loading };
}

// Usage in component
function EditButton() {
  const { allowed, loading } = usePermission('document.edit');
  if (loading || !allowed) return null;
  return <button>Edit</button>;
}

Next.js (App Router)

// lib/lumoauth.ts
import { LumoAuth } from '@lumoauth/client';
import { getServerSession } from 'next-auth';

// Server-side client (API routes / Server Components)
export async function getLumoAuth() {
  const session = await getServerSession();
  return new LumoAuth({
    baseUrl: process.env.LUMOAUTH_URL!,
    token: session?.accessToken ?? '',
  });
}
// app/documents/[id]/page.tsx
import { getLumoAuth } from '@/lib/lumoauth';
import { notFound } from 'next/navigation';

export default async function DocumentPage({ params }: { params: { id: string } }) {
  const client = await getLumoAuth();

  const canView = await client.abac.isAllowed('document', 'read', params.id);
  if (!canView) notFound();

  const { results } = await client.permissions.checkBulk([
    'document.edit',
    'document.delete',
  ]);

  return (
    <div>
      <h1>Document {params.id}</h1>
      {results['document.edit'] && <button>Edit</button>}
      {results['document.delete'] && <button>Delete</button>}
    </div>
  );
}

Next.js Middleware

// middleware.ts
import { LumoAuth } from '@lumoauth/client';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('access_token')?.value;
  if (!token) return NextResponse.redirect(new URL('/login', request.url));

  const client = new LumoAuth({
    baseUrl: process.env.LUMOAUTH_URL!,
    token,
  });

  // Protect admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    const isAdmin = await client.permissions.check('admin.access');
    if (!isAdmin) {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }
  }

  return NextResponse.next();
}

export const config = { matcher: ['/admin/:path*', '/dashboard/:path*'] };

Vue.js (Composition API)

// composables/useLumoAuth.ts
import { inject, provide, ref, type InjectionKey } from 'vue';
import { LumoAuth } from '@lumoauth/client';

const LumoAuthKey: InjectionKey<LumoAuth> = Symbol('LumoAuth');

export function provideLumoAuth(baseUrl: string, getToken: () => Promise<string>) {
  const client = new LumoAuth({ baseUrl, token: getToken });
  provide(LumoAuthKey, client);
  return client;
}

export function useLumoAuth() {
  const client = inject(LumoAuthKey);
  if (!client) throw new Error('Call provideLumoAuth() in a parent component');
  return client;
}

export function usePermission(permission: string) {
  const client = useLumoAuth();
  const allowed = ref(false);
  const loading = ref(true);

  client.permissions.check(permission)
    .then(result => { allowed.value = result; })
    .finally(() => { loading.value = false; });

  return { allowed, loading };
}
<!-- components/EditButton.vue -->
<script setup>
import { usePermission } from '@/composables/useLumoAuth';
const { allowed, loading } = usePermission('document.edit');
</script>

<template>
  <button v-if="allowed && !loading">Edit</button>
</template>

Svelte

// lib/lumoauth.ts
import { LumoAuth } from '@lumoauth/client';
import { writable } from 'svelte/store';

export const lumoAuth = new LumoAuth({
  baseUrl: import.meta.env.VITE_LUMOAUTH_URL,
  token: () => localStorage.getItem('access_token') ?? '',
});

// Reactive permission store
export function createPermissionStore(permission: string) {
  const store = writable({ allowed: false, loading: true });

  lumoAuth.permissions.check(permission)
    .then(allowed => store.set({ allowed, loading: false }))
    .catch(() => store.set({ allowed: false, loading: false }));

  return store;
}
<!-- EditButton.svelte -->
<script>
  import { createPermissionStore } from '$lib/lumoauth';
  const perm = createPermissionStore('document.edit');
</script>

{#if !$perm.loading && $perm.allowed}
  <button>Edit</button>
{/if}

Node.js / Express

import express from 'express';
import { LumoAuth, LumoAuthApiError } from '@lumoauth/client';

const app = express();

// Middleware: attach LumoAuth client to request
app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'No token' });

  req.lumoAuth = new LumoAuth({
    baseUrl: process.env.LUMOAUTH_URL!,
    token,
  });
  next();
});

// Helper middleware: require a permission
function requirePermission(...permissions: string[]) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    try {
      const allowed = await req.lumoAuth.permissions.checkAll(permissions);
      if (!allowed) return res.status(403).json({ error: 'Forbidden' });
      next();
    } catch (err) {
      if (err instanceof LumoAuthApiError) {
        return res.status(err.statusCode).json({ error: err.message });
      }
      next(err);
    }
  };
}

// Use it
app.delete('/api/documents/:id', requirePermission('document.delete'), async (req, res) => {
  // User has document.delete permission — proceed
  res.json({ deleted: true });
});

AAuth — AI Agent Identity (@lumoauth/agent)

Cryptographic agent identity for AI agents (LangChain.js, Vercel AI SDK, MCP TypeScript servers): Ed25519/RSA keypairs, RFC 9421 HTTP Message Signatures, proof-of-possession tokens, and the full AAuth token dance (auth / code / exchange / refresh). Node.js ≥ 18 only (uses node:crypto) — imported via its own subpath so the package root stays browser-safe.

import { AAuthClient, generateKeypair, verifyAuthToken } from '@lumoauth/agent';

// One-time: generate a keypair, register the JWKS with your LumoAuth org
const { privateKeyPem, jwks } = generateKeypair();

// At runtime: mint proof-of-possession auth tokens and call resources
const agent = new AAuthClient({
  agentIdentifier: 'https://my-agent.example.com',
  privateKeyPem: process.env.AGENT_PRIVATE_KEY!,
  baseUrl: 'https://app.lumoauth.dev',
  orgId: 'acme-corp',
});

const result = await agent.requestAuthToken({ resourceToken, scope: 'read write', agentToken });
if (!('authorizationRequired' in result)) {
  const resp = await agent.signedRequest('GET', 'https://api.example.com/v1/data', {
    authToken: result.authToken,
  });
}

// Resource-server side: verify incoming auth+jwt tokens against the org JWKS
const claims = await verifyAuthToken(bearer, {
  issuer: 'https://app.lumoauth.dev/orgs/acme-corp/api/v1',
  resource: 'https://api.example.com',
});

Also exported: exchangeCode (consent flow), exchangeToken (multi-hop agent→agent), refresh, revoke, discovery helpers (discoverIssuer / discoverAgents / discoverResource), jwkThumbprint, and the raw RFC 9421 signing primitives (signRequest, buildSignatureBase, verifySignatureBase, contentDigestSha256). See the AAuth Quickstart — JavaScript/Node.js in the product docs for the full walkthrough.


Caching

The SDK caches permission check results in memory (5-minute TTL by default). This dramatically reduces API calls in UI apps.

// Disable caching
const client = new LumoAuth({ baseUrl, token, cache: false });

// Custom TTL (10 minutes)
const client = new LumoAuth({ baseUrl, token, cache: { ttl: 600_000 } });

// Manually clear cache (e.g. after role changes)
client.clearCache();

Error Handling

All SDK errors extend LumoAuthError and include a code field:

import {
  LumoAuthError,
  LumoAuthApiError,
  LumoAuthAuthError,
  LumoAuthNetworkError,
  LumoAuthValidationError,
  LumoAuthConfigError,
} from '@lumoauth/client';

try {
  await client.permissions.check('document.edit');
} catch (err) {
  if (err instanceof LumoAuthAuthError) {
    // 401 — redirect to login
    redirectToLogin();
  } else if (err instanceof LumoAuthApiError) {
    // 4xx/5xx — server error
    console.error(err.code, err.statusCode, err.body);
  } else if (err instanceof LumoAuthNetworkError) {
    // Network failure / timeout
    showOfflineMessage();
  } else if (err instanceof LumoAuthValidationError) {
    // Response didn't match expected schema
    console.error('Unexpected API response:', err.issues);
  }
}

Zod Schemas

All request and response types are defined as Zod schemas. You can use them for your own validation:

import {
  ZanzibarCheckRequestSchema,
  AbacCheckRequestSchema,
  CheckPermissionResponseSchema,
} from '@lumoauth/client';

// Validate user input
const parsed = ZanzibarCheckRequestSchema.parse(userInput);

// Infer types
type AbacReq = z.infer<typeof AbacCheckRequestSchema>;

TypeScript Support

The SDK is written in TypeScript with full type inference. All methods, parameters, and responses are strongly typed — no any types.

// Types are exported for use in your app
import type {
  CheckPermissionResponse,
  ZanzibarCheckRequest,
  AbacCheckResponse,
  AbacCondition,
  PermissionObject,
} from '@lumoauth/client';

Best Practices

  1. Use dynamic tokens — pass a function to token so the SDK always uses a fresh access token
  2. Leverage checkBulk — fetch all needed permissions in one call when loading a page
  3. Cache wisely — the default 5-min cache is good for most UIs; lower it for admin panels
  4. Clear cache on role changes — call client.clearCache() after user roles are updated
  5. Use ABAC for context-aware checks — when access depends on resource properties or time
  6. Use Zanzibar for sharing — when modeling document ownership, team hierarchies, or org structures
  7. Handle errors gracefully — always catch LumoAuthAuthError to redirect to login

License

MIT