npm package: @archie/node-sdk
Description: Archie BAAS - Node.js server SDK with admin auth, JWT verification & middleware
Author: Archie
Latest version: 0.1.3

# @archie/node-sdk

Node.js server SDK for the Archie BAAS platform. JWT verification, admin user management, service-role GraphQL & REST, and framework middleware for Express, Fastify, and Lambda.

## Install

```bash
# npm
npm install @archie/js-sdk @archie/node-sdk

# pnpm
pnpm add @archie/js-sdk @archie/node-sdk
```

`@archie/js-sdk` is a required peer dependency. Framework integrations (`express`, `fastify`) are optional — install only what you use.

**Requirements:** Node.js >= 18.0.0

---

## Quick Start

```typescript
import { createAdminClient } from '@archie/node-sdk';

const archie = createAdminClient({
  projectId: 'your-project-uuid',
  apiKey: 'your-service-role-api-key',
  apiUrl: 'https://your-project.archiecore.com',
  environment: 'master',
  retries: 3,
  timeout: 15_000,
});

// Health check
const healthy = await archie.ping();

// Verify a JWT
const { user, error } = await archie.auth.verifyToken(token);

// List users
const { data, error: listErr } = await archie.auth.listUsers({ page: 1, limit: 20 });

// Service-role GraphQL
const { data: roles } = await archie.graphql.query('{ roles { items { id name } } }');
```

---

## API Reference

### `createAdminClient(options)`

Creates an admin client for server-side operations using a service-role API key.

```typescript
const archie = createAdminClient({
  projectId: 'uuid',
  apiKey: 'service-role-api-key',
  environment: 'master',
  timeout: 15_000,
  retries: 3,
});
```

| Option        | Type           | Default                    | Description                                                      |
| ------------- | -------------- | -------------------------- | ---------------------------------------------------------------- |
| `projectId`   | `string`       | —                          | **Required.** Project UUID.                                      |
| `apiKey`      | `string`       | —                          | **Required.** Service-role API key.                              |
| `environment` | `string`       | `'master'`                 | Environment name.                                                |
| `apiUrl`      | `string`       | `'https://api.archie.dev'` | Base URL for the API.                                            |
| `authUrl`     | `string`       | same as `apiUrl`           | Base URL for the Auth service.                                   |
| `timeout`     | `number`       | `30000`                    | Request timeout in milliseconds. Set `0` to disable.             |
| `retries`     | `number`       | `0`                        | Max retry attempts for transient errors (429, 500–504, network). |
| `retryDelay`  | `number`       | `200`                      | Initial delay in ms for exponential backoff with jitter.         |
| `fetch`       | `typeof fetch` | `globalThis.fetch`         | Custom fetch implementation for testing or proxies.              |
| `logger`      | `ArchieLogger` | `{}`                       | Logger for request lifecycle events.                             |

Returns an `ArchieAdminClient` with three modules:

- `archie.auth` — JWT verification & admin user operations
- `archie.graphql` — Service-role GraphQL queries & mutations
- `archie.rest` — Service-role REST requests

---

## JWT Verification

Verify JWTs issued by Archie using JWKS (JSON Web Key Set). Keys are cached for 5 minutes with automatic deduplication of concurrent fetches.

### `archie.auth.verifyToken(token, options?)`

```typescript
const { user, error } = await archie.auth.verifyToken('Bearer eyJhbG...');
```

**Parameters:**

| Parameter                      | Type       | Description                                                  |
| ------------------------------ | ---------- | ------------------------------------------------------------ |
| `token`                        | `string`   | JWT string. `Bearer ` prefix is stripped automatically.      |
| `options.requiredRoles`        | `string[]` | Require at least one of these roles. Returns 403 if missing. |
| `options.requireEmailVerified` | `boolean`  | Require `emailVerified: true`. Returns 403 if false.         |
| `options.clockTolerance`       | `number`   | Clock tolerance in seconds. Default: `30`.                   |

**Returns:** `Promise<VerifyTokenResult>`

```typescript
interface VerifyTokenResult {
  user: TokenUser | null;
  error: ArchieError | null;
}

interface TokenUser {
  id: string;
  email: string;
  firstName?: string;
  lastName?: string;
  roles: string[];
  exp: number;
  iat: number;
  emailVerified: boolean;
}
```

**Examples:**

```typescript
// Basic verification
const { user, error } = await archie.auth.verifyToken(jwt);
if (error) {
  console.error(error.code); // AUTH_TOKEN_INVALID, AUTH_TOKEN_EXPIRED, etc.
}

// Require admin role
const { user, error } = await archie.auth.verifyToken(jwt, {
  requiredRoles: ['admin'],
});

// Require verified email
const { user, error } = await archie.auth.verifyToken(jwt, {
  requireEmailVerified: true,
});
```

**JWKS Caching:** The SDK automatically fetches and caches JWKS keys for 5 minutes. Concurrent `verifyToken` calls share a single JWKS fetch, preventing thundering-herd issues.

---

## Admin User Operations

All admin operations use service-role authentication and return `{ data, error }`.

### `archie.auth.createUser(params)`

```typescript
const { data, error } = await archie.auth.createUser({
  email: 'user@example.com',
  password: 'SecurePass123!',
  firstName: 'John', // optional
  lastName: 'Doe', // optional
  roleId: 'role-uuid', // optional
  emailVerified: true, // optional
});
```

### `archie.auth.listUsers(params?)`

```typescript
const { data, error } = await archie.auth.listUsers({
  page: 1, // optional
  limit: 20, // optional
  search: 'john', // optional, searches email/name
  status: 'active', // optional: 'active' | 'locked' | 'unverified'
  orderBy: 'createdAt', // optional
});
// data: { users: User[], total: number, page: number, limit: number }
```

### `archie.auth.getUser(userId)`

```typescript
const { data, error } = await archie.auth.getUser('user-uuid');
// data: User { id, email, firstName, lastName, roles }
```

### `archie.auth.updateUser(userId, updates)`

Accepts `Partial<User>` — any combination of `firstName`, `lastName`, `email`, `roles`, etc.:

```typescript
const { data, error } = await archie.auth.updateUser('user-uuid', {
  firstName: 'Jane',
  lastName: 'Smith',
});
```

### `archie.auth.blockUser(userId)`

```typescript
const { data, error } = await archie.auth.blockUser('user-uuid');
// data: boolean
```

### `archie.auth.unblockUser(userId)`

```typescript
const { data, error } = await archie.auth.unblockUser('user-uuid');
// data: boolean
```

### `archie.auth.deleteUser(userId)`

```typescript
const { data, error } = await archie.auth.deleteUser('user-uuid');
// data: boolean
```

### `archie.auth.resendVerification(userId)`

```typescript
const { data, error } = await archie.auth.resendVerification('user-uuid');
// data: boolean
```

### `archie.auth.resetUserPassword(userId, newPassword)`

```typescript
const { data, error } = await archie.auth.resetUserPassword('user-uuid', 'NewPass123!');
// data: boolean
```

---

## Service-Role GraphQL

Execute GraphQL operations with admin-level permissions.

### `archie.graphql.query<T>(gql, variables?, options?)`

```typescript
const { data, error } = await archie.graphql.query<{ users: User[] }>(
  '{ users { id email roles } }',
);
```

### `archie.graphql.mutate<T>(gql, variables?, options?)`

```typescript
const { data, error } = await archie.graphql.mutate(
  `mutation UpdateRole($userId: ID!, $roleId: ID!) {
    assignRole(userId: $userId, roleId: $roleId) { id roles }
  }`,
  { userId: 'user-uuid', roleId: 'admin-role' },
);
```

### User Impersonation

Execute queries as a specific user while retaining service-role access:

```typescript
const { data } = await archie.graphql.query(
  '{ myOrders { id total } }',
  {},
  { impersonateUserId: 'user-uuid' },
);
```

This adds the `x-impersonate-user` header to the request.

---

## Service-Role REST

Direct REST API access with admin credentials.

```typescript
const items = await archie.rest.get<Item[]>('/api/v1/items');
const created = await archie.rest.post<Item>('/api/v1/items', { name: 'New' });
const updated = await archie.rest.put<Item>('/api/v1/items/1', { name: 'Updated' });
const patched = await archie.rest.patch<Item>('/api/v1/items/1', { status: 'active' });
await archie.rest.delete('/api/v1/items/1');
```

All methods throw `ArchieError` on non-2xx responses with the HTTP status code embedded:

```typescript
try {
  await archie.rest.get('/api/v1/missing');
} catch (err) {
  // err.code === 'HTTP_404'
  // err.status === 404
}
```

**Note:** Endpoints returning `204 No Content` or `Content-Length: 0` resolve with `null` data instead of attempting to parse an empty body.

---

## Express Middleware

```bash
npm install express
```

### `createArchieMiddleware(client, options?)`

Verifies JWT from the `Authorization` header and populates `req.archieUser`.

```typescript
import express from 'express';
import { createAdminClient } from '@archie/node-sdk';
import { createArchieMiddleware, requireRole } from '@archie/node-sdk/express';

const archie = createAdminClient({ projectId: '...', apiKey: '...' });
const app = express();

// Optional auth — req.archieUser may be undefined
app.use(createArchieMiddleware(archie, { required: false }));

app.get('/api/profile', (req, res) => {
  if (!req.archieUser) return res.status(401).json({ error: 'Unauthorized' });
  res.json({ user: req.archieUser });
});

// Required auth — returns 401 automatically
app.use('/api/admin', createArchieMiddleware(archie, { required: true }));

app.get('/api/admin/dashboard', requireRole('admin'), (req, res) => {
  res.json({ admin: true, user: req.archieUser });
});
```

| Option     | Type       | Default | Description                                            |
| ---------- | ---------- | ------- | ------------------------------------------------------ |
| `required` | `boolean`  | `false` | Return 401 for unauthenticated requests.               |
| `onError`  | `function` | —       | Custom error handler: `(err, req, res, next) => void`. |

### `requireRole(...roles)`

Express middleware that checks if `req.archieUser` has at least one of the specified roles. Returns 403 if not. Must be used after `createArchieMiddleware`.

```typescript
app.get('/admin', requireRole('admin'), handler);
app.get('/editor', requireRole('admin', 'editor'), handler);
```

### TypeScript

The middleware augments Express types — `req.archieUser` is typed as `TokenUser | undefined`:

```typescript
app.get('/api/me', (req, res) => {
  const user = req.archieUser; // TokenUser | undefined
  res.json({ user });
});
```

---

## Fastify Plugin

```bash
npm install fastify
```

### `archiePlugin`

Fastify plugin that verifies JWT and decorates `request.archieUser`.

```typescript
import Fastify from 'fastify';
import { createAdminClient } from '@archie/node-sdk';
import { archiePlugin } from '@archie/node-sdk/fastify';

const archie = createAdminClient({ projectId: '...', apiKey: '...' });
const fastify = Fastify();

fastify.register(archiePlugin, {
  client: archie,
  required: false, // set true to enforce auth on all routes
});

fastify.get('/api/profile', async (request) => {
  return { user: request.archieUser };
});
```

---

## Lambda / Serverless

### `withArchieAuth(client, handler)`

Wraps a Lambda handler to verify JWT and inject user into context.

```typescript
import { createAdminClient } from '@archie/node-sdk';
import { withArchieAuth } from '@archie/node-sdk/lambda';

const archie = createAdminClient({ projectId: '...', apiKey: '...' });

export const handler = withArchieAuth(archie, async (event, context) => {
  const { user, client } = context.archie;

  // Access the verified user
  console.log('User:', user.id, user.email);

  // Use admin operations
  const { data } = await client.auth.listUsers();

  return {
    statusCode: 200,
    body: JSON.stringify({ userId: user.id, totalUsers: data?.total }),
  };
});
```

Returns 401 if no `Authorization` header is present or if the token is invalid.

---

## Error Handling

All operations follow consistent error patterns:

- **Auth operations** return `{ data, error }` where `error` is `ArchieError | null`
- **Verify operations** return `{ user, error }` where `error` is `AuthError | null`
- **REST operations** throw `ArchieError` on non-2xx responses
- **GraphQL operations** return `{ data, error }` with structured GraphQL errors

### Error Codes

| Code                      | Status | Description                                            |
| ------------------------- | ------ | ------------------------------------------------------ |
| `AUTH_TOKEN_INVALID`      | 401    | Token is missing, malformed, or has invalid signature. |
| `AUTH_TOKEN_EXPIRED`      | 401    | Token has expired.                                     |
| `AUTH_INSUFFICIENT_ROLE`  | 403    | User lacks a required role.                            |
| `AUTH_EMAIL_NOT_VERIFIED` | 403    | Email is not verified but was required.                |
| `AUTH_JWKS_FETCH_FAILED`  | varies | Failed to fetch JWKS keys.                             |
| `ADMIN_ERROR`             | varies | Admin operation returned an error.                     |
| `HTTP_<status>`           | varies | REST request failed with given HTTP status.            |
| `TIMEOUT`                 | 408    | Request timed out.                                     |
| `NETWORK_ERROR`           | 0      | Network-level failure.                                 |

---

## Retry & Timeout

All HTTP requests go through a centralized engine with configurable retry and timeout.

### Retry

Enable automatic retries for transient failures:

```typescript
const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  retries: 3, // retry up to 3 times
  retryDelay: 200, // start with 200ms delay
});
```

**Retryable conditions:**

- HTTP status codes: `429`, `500`, `502`, `503`, `504`
- Network errors (DNS failures, connection refused, etc.)
- Timeout errors

**Backoff strategy:** Exponential with jitter — `delay × 2^attempt + random jitter`, capped at 30 seconds. When a `429` response includes a `Retry-After` header, the SDK respects it.

### Timeout

All requests have a 30-second timeout by default:

```typescript
const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  timeout: 10_000, // 10s timeout
});
```

Set `timeout: 0` to disable timeouts entirely. When a request times out, an `ArchieError` is thrown with `code: 'TIMEOUT'` and `status: 408`.

---

## Custom Fetch

Inject a custom `fetch` implementation for testing, proxies, or request interceptors:

```typescript
import { createAdminClient } from '@archie/node-sdk';

const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  fetch: myCustomFetch,
});
```

**Use cases:**

- Unit testing with mocked fetch
- HTTP proxy / agent injection (e.g., `undici` with proxy)
- Request/response logging at the transport level

---

## Logger

Plug in any logger (pino, winston, console, custom) to observe request lifecycle events:

```typescript
import pino from 'pino';
import { createAdminClient } from '@archie/node-sdk';

const logger = pino({ level: 'debug' });

const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  logger: {
    debug: (msg, meta) => logger.debug(meta, msg),
    warn: (msg, meta) => logger.warn(meta, msg),
  },
});
```

### `ArchieLogger` interface

```typescript
interface ArchieLogger {
  debug?: (message: string, meta?: Record<string, unknown>) => void;
  info?: (message: string, meta?: Record<string, unknown>) => void;
  warn?: (message: string, meta?: Record<string, unknown>) => void;
  error?: (message: string, meta?: Record<string, unknown>) => void;
}
```

**Events logged:**
| Level | Event | Meta |
|-------|-------|------|
| `debug` | `Request` | `{ method, url }` |
| `debug` | `Response` | `{ url, status }` |
| `warn` | `Retrying request` | `{ url, attempt, delay }` |
| `warn` | `Rate limited, waiting Retry-After` | `{ url, retryAfter }` |
| `warn` | `Network error, will retry` | `{ url, attempt, error }` |

All logger methods are optional — use only the levels you need.

---

## Health Check

Verify API connectivity:

```typescript
const healthy = await archie.ping();
if (!healthy) {
  console.error('Archie API is unreachable');
}
```

`ping()` sends a `GET /health` request and returns `true` if the server responds with 2xx within the configured timeout. Returns `false` on any error.

---

## TypeScript

### Exported Types

```typescript
import type {
  AdminClientOptions,
  ArchieLogger,
  VerifyTokenOptions,
  VerifyTokenResult,
  TokenUser,
  CreateUserParams,
  ListUsersParams,
  ListUsersResult,
  MiddlewareOptions,
} from '@archie/node-sdk';

import type {
  IAdminAuthModule,
  IAdminGraphQLModule,
  IAdminRestModule,
  AdminResult,
} from '@archie/node-sdk';
```

### Classes

The `ArchieAdminClient` class and concrete module classes are exported for `instanceof` checks, advanced typing, or mocking:

```typescript
import {
  ArchieAdminClient,
  AdminAuthModule,
  AdminGraphQLModule,
  AdminRestModule,
} from '@archie/node-sdk';

// Direct instantiation (equivalent to createAdminClient)
const archie = new ArchieAdminClient({ projectId: '...', apiKey: '...' });
```

### Re-exported from `@archie/js-sdk`

```typescript
import type {
  User,
  Session,
  GraphQLResponse,
  ArchieError,
  AuthError,
  GraphQLError,
  NetworkError,
} from '@archie/node-sdk';
```

---

## Playground

An interactive playground is available in `examples/playground-node/`:

```bash
cd examples/playground-node
cp .env.example .env
# Edit .env with your project credentials
pnpm install
pnpm dev
# Open http://localhost:3456
```

The playground provides a web dashboard to test all node-sdk features: JWT verification, admin user operations, service-role GraphQL, REST, and Express middleware.

---

## License

[MIT](../../LICENSE)