npm package: @sp-uvb/feathers
Description: FeathersJS hooks and services for Universal Verification Broker (UVB)
Author: UVB Team
Latest version: 0.1.0

# @sp-uvb/feathers

FeathersJS hooks and services for Universal Verification Broker (UVB) authentication.

## Installation

```bash
npm install @sp-uvb/feathers
# or
yarn add @sp-uvb/feathers
# or
pnpm add @sp-uvb/feathers
```

## Quick Start

```typescript
import { feathers } from '@feathersjs/feathers';
import { authenticate, requireFactors } from '@sp-uvb/feathers';

const app = feathers();

// Apply authentication to all services
app.hooks({
  before: {
    all: [authenticate({ tenantId: 'my-tenant' })],
  },
});

// Require specific factors for sensitive operations
app.service('admin').hooks({
  before: {
    all: [requireFactors(['totp', 'webauthn'])],
  },
});

// Access session in service methods
app.service('users').hooks({
  before: {
    find: [
      async (context) => {
        const session = context.params.uvbSession;
        console.log('User ID:', session?.userId);
        return context;
      },
    ],
  },
});
```

## API

### `authenticate(options)`

Feathers hook to authenticate requests with UVB.

**Options:**

- `tenantId` (required): Your UVB tenant ID
- `uvbUrl` (optional): UVB server URL, defaults to `http://localhost:8080`
- `apiKey` (optional): API key for server-to-server authentication
- `cookieName` (optional): Cookie name for session token, defaults to `uvb_session`
- `excludeServices` (optional): Array of service paths to exclude from authentication

```typescript
import { authenticate } from '@sp-uvb/feathers';

// Global authentication
app.hooks({
  before: {
    all: [
      authenticate({
        tenantId: 'my-tenant',
        uvbUrl: 'http://localhost:8080',
        excludeServices: ['authentication', 'health'],
      }),
    ],
  },
});

// Per-service authentication
app.service('users').hooks({
  before: {
    all: [authenticate({ tenantId: 'my-tenant' })],
  },
});
```

**Session Access:**

After authentication, `context.params.uvbSession` contains:

```typescript
interface UvbSession {
  userId: string;
  tenantId: string;
  sessionId: string;
  factorsVerified: string[];
  expiresAt: Date;
}
```

### `requireFactors(factors)`

Hook to require specific MFA factors.

```typescript
import { authenticate, requireFactors } from '@sp-uvb/feathers';

app.service('admin').hooks({
  before: {
    all: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp', 'webauthn'])],
  },
});
```

### `requireOwnership(ownerField)`

Hook to require user owns the resource.

```typescript
import { authenticate, requireOwnership } from '@sp-uvb/feathers';

app.service('posts').hooks({
  before: {
    update: [
      authenticate({ tenantId: 'my-tenant' }),
      requireOwnership('userId'), // Checks if record.userId === session.userId
    ],
    remove: [authenticate({ tenantId: 'my-tenant' }), requireOwnership('userId')],
    create: [
      authenticate({ tenantId: 'my-tenant' }),
      requireOwnership('userId'), // Automatically sets record.userId = session.userId
    ],
  },
});
```

### `getSession(context)`

Helper to get UVB session from hook context.

```typescript
import { getSession } from '@sp-uvb/feathers';

app.service('users').hooks({
  before: {
    find: [
      async (context) => {
        const session = getSession(context);
        if (session) {
          context.params.query = {
            ...context.params.query,
            userId: session.userId,
          };
        }
        return context;
      },
    ],
  },
});
```

### `UvbService`

Feathers service for validating and revoking UVB sessions.

```typescript
import { UvbService } from '@sp-uvb/feathers';

// Register service
app.use(
  'uvb',
  new UvbService({
    tenantId: 'my-tenant',
    uvbUrl: 'http://localhost:8080',
  })
);

// Validate session
const session = await app.service('uvb').get(sessionToken);
console.log('User ID:', session.userId);

// Revoke session
await app.service('uvb').remove(sessionId);
```

## Examples

### Protecting All Services Except Authentication

```typescript
import { authenticate } from '@sp-uvb/feathers';

app.hooks({
  before: {
    all: [
      authenticate({
        tenantId: 'my-tenant',
        excludeServices: ['authentication', 'health', 'public'],
      }),
    ],
  },
});
```

### Per-Method Authentication Requirements

```typescript
import { authenticate, requireFactors } from '@sp-uvb/feathers';

app.service('users').hooks({
  before: {
    find: [authenticate({ tenantId: 'my-tenant' })],
    get: [authenticate({ tenantId: 'my-tenant' })],
    create: [authenticate({ tenantId: 'my-tenant' })],
    update: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp'])],
    remove: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp', 'webauthn'])],
  },
});
```

### Filtering Results by User

```typescript
import { authenticate, getSession } from '@sp-uvb/feathers';

app.service('posts').hooks({
  before: {
    find: [
      authenticate({ tenantId: 'my-tenant' }),
      async (context) => {
        const session = getSession(context);
        // Only show user's own posts
        context.params.query = {
          ...context.params.query,
          userId: session.userId,
        };
        return context;
      },
    ],
  },
});
```

### Conditional MFA Requirements

```typescript
import { authenticate, requireFactors, getSession } from '@sp-uvb/feathers';

app.service('transfers').hooks({
  before: {
    create: [
      authenticate({ tenantId: 'my-tenant' }),
      async (context) => {
        const amount = context.data.amount;

        // Require strong auth for large transfers
        if (amount > 10000) {
          await requireFactors(['totp', 'webauthn'])(context);
        }

        return context;
      },
    ],
  },
});
```

### Admin-Only Operations

```typescript
import { authenticate, getSession } from '@sp-uvb/feathers';

const requireAdmin = async (context) => {
  const session = getSession(context);

  if (!session) {
    throw new Error('No session found');
  }

  // Check if user has admin factor
  if (!session.factorsVerified.includes('admin-role')) {
    const error = new Error('Admin access required') as any;
    error.code = 403;
    throw error;
  }

  return context;
};

app.service('admin').hooks({
  before: {
    all: [authenticate({ tenantId: 'my-tenant' }), requireAdmin],
  },
});
```

### Audit Logging with Session

```typescript
import { authenticate, getSession } from '@sp-uvb/feathers';

app.service('logs').hooks({
  after: {
    create: [
      async (context) => {
        const session = getSession(context);

        await app.service('audit-logs').create({
          action: 'log_created',
          userId: session?.userId,
          timestamp: new Date(),
          data: context.result,
        });

        return context;
      },
    ],
  },
});
```

### Using with Feathers Authentication

```typescript
import { authenticate as feathersAuth } from '@feathersjs/authentication';
import { authenticate as uvbAuth } from '@sp-uvb/feathers';

// Use Feathers auth for local auth endpoints
app.service('authentication').hooks({
  before: {
    create: [feathersAuth('local')],
  },
});

// Use UVB auth for everything else
app.hooks({
  before: {
    all: [
      uvbAuth({
        tenantId: 'my-tenant',
        excludeServices: ['authentication'],
      }),
    ],
  },
});
```

### Combining Multiple Security Hooks

```typescript
import { authenticate, requireFactors, requireOwnership } from '@sp-uvb/feathers';

app.service('private-docs').hooks({
  before: {
    all: [authenticate({ tenantId: 'my-tenant' })],
    find: [],
    get: [requireOwnership('userId')],
    create: [requireFactors(['email-verified']), requireOwnership('userId')],
    update: [requireFactors(['totp']), requireOwnership('userId')],
    remove: [requireFactors(['totp', 'webauthn']), requireOwnership('userId')],
  },
});
```

### Custom Session Validation

```typescript
import { UvbService } from '@sp-uvb/feathers';

// Register UVB service
app.use('uvb', new UvbService({ tenantId: 'my-tenant' }));

// Custom authentication hook
const customAuth = async (context) => {
  const token = context.params.headers?.['x-custom-token'];

  if (!token) {
    throw new Error('No token provided');
  }

  try {
    const session = await app.service('uvb').get(token);
    context.params.uvbSession = session;
    return context;
  } catch (error) {
    throw new Error('Invalid token');
  }
};

app.service('custom').hooks({
  before: {
    all: [customAuth],
  },
});
```

## TypeScript

This package includes full TypeScript definitions:

```typescript
import type { UvbSession, UvbAuthenticateOptions } from '@sp-uvb/feathers';

// Extend Feathers Params type (done automatically by the package)
declare module '@feathersjs/feathers' {
  interface Params {
    uvbSession?: UvbSession;
  }
}
```

## License

MIT