npm package: @ldauth/node
Description: Node.js client library for ldauth permission checking
Author: Bart Riepe
Latest version: 0.10.1

# @ldauth/node

Node.js client library for ldauth permission checking. This library provides a simple way to integrate permission checking into your Node.js applications using the ldauth authentication and authorization service.

## Features

- 🔐 Simple permission checking against ldauth API
- ⚡ Built-in caching for improved performance
- 🎯 Framework integrations for Fastify, Express, and tRPC
- 🚀 Automatic batching for tRPC to minimize API calls
- 🔍 Optional local JWT validation
- 💪 TypeScript support with full type definitions
- 🛡️ Comprehensive error handling

## Installation

```bash
npm install @ldauth/node
# or
yarn add @ldauth/node
# or
pnpm add @ldauth/node
```

## Quick Start

### Basic Usage

```typescript
import { LDAuthClient } from '@ldauth/node';

const ldauth = new LDAuthClient({
  apiUrl: 'https://auth.example.com',
  defaultSchema: 'myapp',
  cacheTimeSeconds: 5 // Cache results for 5 seconds
});

// Check a permission
try {
  await ldauth.checkPermission(token, {
    resource: 'users',
    action: 'read'
  });
  console.log('Permission granted!');
} catch (error) {
  console.error('Permission denied');
}

// Get all user permissions
const permissions = await ldauth.getUserPermissions(token);
console.log(permissions);
// { permissions: { users: ['read', 'write'], posts: ['read'] } }

// Get user info
const userInfo = await ldauth.getUserInfo(token);
console.log(userInfo);
// { id: '123', email: 'user@example.com', groups: ['admin'] }
```

### Fastify Integration

```typescript
import fastify from 'fastify';
import { ldauthFastify } from '@ldauth/node/fastify';

const app = fastify();

// Register the plugin
await app.register(ldauthFastify, {
  apiUrl: 'https://auth.example.com',
  defaultSchema: 'myapp',
  validateTokenLocally: true, // Validate JWT before calling API
  cacheTimeSeconds: 5
});

// Simple permission check
app.get('/users', {
  preHandler: app.ldauth.require('users', 'read')
}, async (request, reply) => {
  return { users: [] };
});

// With user info populated
app.get('/profile', {
  preHandler: [
    app.ldauth.require('profile', 'view'),
    app.ldauth.populateUser()
  ]
}, async (request, reply) => {
  return { 
    email: request.user.email,
    groups: request.user.groups 
  };
});

// Check multiple permissions (AND)
app.post('/admin', {
  preHandler: app.ldauth.requireAll([
    ['admin', 'access'],
    ['settings', 'write']
  ])
}, async (request, reply) => {
  return { success: true };
});

// Check multiple permissions (OR)
app.put('/content', {
  preHandler: app.ldauth.requireAny([
    ['content', 'edit'],
    ['admin', 'access']
  ])
}, async (request, reply) => {
  return { success: true };
});
```

### Express Integration

```typescript
import express from 'express';
import { ldauthExpress } from '@ldauth/node/express';

const app = express();

// Create middleware factory
const ldauth = ldauthExpress({
  apiUrl: 'https://auth.example.com',
  defaultSchema: 'myapp',
  validateTokenLocally: true,
  cacheTimeSeconds: 5
});

// Simple permission check
app.get('/users', 
  ldauth.require('users', 'read'),
  (req, res) => {
    res.json({ users: [] });
  }
);

// With user info populated
app.get('/profile',
  ldauth.require('profile', 'view'),
  ldauth.populateUser(),
  (req, res) => {
    res.json({ 
      email: req.user.email,
      groups: req.user.groups 
    });
  }
);

// Check multiple permissions (AND)
app.post('/admin',
  ldauth.requireAll([
    ['admin', 'access'],
    ['settings', 'write']
  ]),
  (req, res) => {
    res.json({ success: true });
  }
);

// Check multiple permissions (OR)
app.put('/content',
  ldauth.requireAny([
    ['content', 'edit'],
    ['admin', 'access']
  ]),
  (req, res) => {
    res.json({ success: true });
  }
);

// Global middleware for all /api routes
app.use('/api', ldauth.populateUser());
```

### tRPC Integration

```typescript
import { initTRPC } from '@trpc/server';
import { createLDAuthMiddleware, PermissionBatcher } from '@ldauth/node/trpc';
import { LDAuthClient } from '@ldauth/node';

// Initialize tRPC
const t = initTRPC.context().create();

// Create LDAuth client and batcher for automatic batching
const ldauthClient = new LDAuthClient({
  apiUrl: 'https://auth.example.com',
  audience: 'https://api.example.com',
  defaultSchemaName: 'myapp',
  cacheTimeSeconds: 5
});

const batcher = new PermissionBatcher(ldauthClient);

// Create the middleware
const authMiddleware = createLDAuthMiddleware({
  client: batcher,
  defaultSchemaName: 'myapp'
});

// Create a protected procedure
const protectedProcedure = t.procedure.use(authMiddleware);

// Define your router
export const appRouter = t.router({
  // Simple permission check
  getUsers: protectedProcedure
    .meta({
      permissions: {
        resource: 'users',
        action: 'read'
      }
    })
    .query(async ({ ctx }) => {
      // Permission automatically checked
      // Token available in ctx.token
      return { users: [] };
    }),

  // Require all permissions (AND logic)
  adminAction: protectedProcedure
    .meta({
      requireAll: [
        { resource: 'admin', action: 'access' },
        { resource: 'settings', action: 'write' }
      ]
    })
    .mutation(async ({ ctx, input }) => {
      return { success: true };
    }),

  // Require any permission (OR logic)
  editContent: protectedProcedure
    .meta({
      requireAny: [
        { resource: 'content', action: 'edit' },
        { resource: 'admin', action: 'access' }
      ]
    })
    .mutation(async ({ ctx, input }) => {
      return { success: true };
    }),

  // Object-level permission
  editPost: protectedProcedure
    .meta({
      permissions: {
        resource: 'posts',
        action: 'edit',
        instanceId: 'post-123' // Check for specific post
      }
    })
    .mutation(async ({ ctx, input }) => {
      return { success: true };
    }),

  // No permissions required (public endpoint)
  getPublicData: t.procedure
    .query(async () => {
      return { data: 'public' };
    })
});

export type AppRouter = typeof appRouter;
```

**Note:** When tRPC batches multiple procedures in one HTTP request, all permission checks are automatically batched into one or more API calls (grouped by token), significantly improving performance.

## Configuration

### Client Options

```typescript
interface LDAuthClientConfig {
  // Base URL of your ldauth API gateway
  apiUrl?: string; // Can use LDAUTH_API_URL env var
  
  // Default schema for permission checks
  defaultSchema?: string;
  
  // Default profile (defaults to '__default')
  defaultProfile?: string;
  
  // Validate JWT locally before API call
  validateTokenLocally?: boolean; // Default: false

  // Cache results for this many seconds
  cacheTimeSeconds?: number; // Default: 0 (no cache)
  
  // API request timeout in milliseconds
  timeout?: number; // Default: 10000
  
  // Custom headers for API requests
  headers?: Record<string, string>;
}
```

### Environment Variables

- `LDAUTH_API_URL` - Set the API URL via environment variable instead of config

## Error Handling

The library provides specific error classes for different scenarios:

```typescript
import { 
  LDAuthPermissionError,
  LDAuthTokenError,
  LDAuthAPIError,
  LDAuthConfigError 
} from '@ldauth/node';

try {
  await ldauth.checkPermission(token, {
    resource: 'users',
    action: 'delete'
  });
} catch (error) {
  if (error instanceof LDAuthPermissionError) {
    // Permission was denied
    console.log('Required permission:', error.requiredPermission);
  } else if (error instanceof LDAuthTokenError) {
    // Token is invalid or expired
    console.log('Token error:', error.message);
  } else if (error instanceof LDAuthAPIError) {
    // API communication failed
    console.log('API error:', error.statusCode, error.message);
  }
}
```

### Express Error Handler

```typescript
app.use((err, req, res, next) => {
  if (err.name === 'LDAuthPermissionError') {
    res.status(403).json({ 
      error: 'Permission denied',
      required: err.requiredPermission 
    });
  } else if (err.name === 'LDAuthTokenError') {
    res.status(401).json({ 
      error: 'Invalid token' 
    });
  } else {
    next(err);
  }
});
```

## Advanced Usage

### Custom Token Extraction

```typescript
// Fastify
await app.register(ldauthFastify, {
  apiUrl: 'https://auth.example.com',
  extractToken: (request) => {
    // Get token from custom header or cookie
    return request.headers['x-auth-token'] || 
           request.cookies?.authToken;
  }
});

// Express
const ldauth = ldauthExpress({
  apiUrl: 'https://auth.example.com',
  extractToken: (req) => {
    // Get token from custom header or cookie
    return req.headers['x-auth-token'] || 
           req.cookies?.authToken;
  }
});
```

### Object-Level Permissions

```typescript
// Check permission for a specific resource instance
await ldauth.checkPermission(token, {
  resource: 'posts',
  action: 'edit',
  instanceId: 'post-123' // Check for this specific post
});
```

### Custom Schema and Profile

```typescript
// Override default schema and profile
await ldauth.checkPermission(token, {
  resource: 'admin',
  action: 'access',
  schema: 'admin-portal',
  profile: 'elevated'
});
```

## API Reference

### LDAuthClient

#### `checkPermission(token, options)`
Check if the user has a specific permission.

#### `getUserPermissions(token, options?)`
Get all permissions for the user.

#### `getUserInfo(token)`
Get user information from the token.

#### `getClientIdFromToken(token)`
Extract client ID from token without validation.

#### `clearCache()`
Clear all cached results.

### Framework Integrations

**Fastify and Express** integrations provide:

- `require(resource, action, options?)` - Require a single permission
- `requireAll(permissions)` - Require all specified permissions
- `requireAny(permissions)` - Require at least one permission
- `populateUser()` - Add user info to request
- `populatePermissions(options?)` - Add all permissions to request

**tRPC** integration provides:

- Permission checking via procedure `.meta()` configuration
- `permissions` - Single permission requirement
- `requireAll` - Require all specified permissions (AND logic)
- `requireAny` - Require any of the specified permissions (OR logic)
- Automatic batching of permission checks across procedures
- Token automatically added to context after successful validation

## License

MIT