# @eduware/oneroster

Type-safe TypeScript SDK for the OneRoster API with LMS-specific client profiles.

<div align="left">
    <a href="https://www.npmjs.com/package/@eduware/oneroster">
        <img src="https://img.shields.io/npm/v/@eduware/oneroster.svg" alt="npm version" />
    </a>
    <a href="https://www.npmjs.com/package/@eduware/oneroster">
        <img src="https://img.shields.io/npm/dm/@eduware/oneroster.svg" alt="npm downloads" />
    </a>
    <a href="https://opensource.org/licenses/MIT">
        <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" />
    </a>
    <a href="#">
        <img src="https://img.shields.io/badge/OneRoster-v1.1%20%7C%20v1.2-green.svg" alt="OneRoster v1.1 | v1.2" />
    </a>
    <a href="#">
        <img src="https://img.shields.io/badge/TypeScript-5.0+-blue.svg" alt="TypeScript 5.0+" />
    </a>
</div>

## Features

- **OneRoster v1.1 & v1.2 support** - Complete implementation of both 1EdTech OneRoster specifications
- **LMS-specific profiles** - Pre-built profiles for ClassLink with type-safe method restrictions
- **Custom profiles** - Create your own profiles using TypeScript's `Pick<>` types
- **OAuth 2.0 & OAuth 1.0a authentication** - OAuth 2.0 for v1.2 (default), OAuth 1.0a for v1.1
- **Automatic pagination** - Async iterables for seamless data fetching
- **TypeScript-first** - Full type safety with comprehensive type definitions
- **Tree-shakeable** - Standalone functions for optimal bundle sizes

## Installation

```bash
# npm
npm add @eduware/oneroster

# pnpm
pnpm add @eduware/oneroster

# yarn
yarn add @eduware/oneroster zod

# bun
bun add @eduware/oneroster
```

> **Note:** Yarn does not install peer dependencies automatically. You will need to install `zod` manually.

## Quick Start

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-oneroster-server.com',
    security: {
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
        tokenURL: 'https://your-oneroster-server.com/oauth/token',
    },
});

// Fetch all users with automatic pagination
const result = await client.usersManagement.getAllUsers({});

for await (const page of result) {
    console.log(page.users);
}
```

## LMS Profiles

The SDK provides LMS-specific client profiles that restrict available methods to match what each provider supports. This gives you compile-time safety and prevents calling unsupported endpoints.

### ClassLink Client

ClassLink provides read-only rostering data. The `ClassLink` client is typed to only expose supported methods:

```typescript
import { ClassLink } from '@eduware/oneroster/clients';

const client = new ClassLink({
    serverURL: 'https://example.classlink.com',
    security: {
        authType: 'bearer',
        bearerToken: process.env.CLASSLINK_BEARER_TOKEN!,
    },
});

// ✅ Read operations work
const users = await client.usersManagement.getAllUsers({});
const schools = await client.schoolsManagement.getAllSchools({});

// ❌ Write operations are not available (TypeScript error)
// client.usersManagement.createUser({});  // Error: Property 'createUser' does not exist
```

**ClassLink supports:**

- Academic Sessions, Classes, Courses, Enrollments
- Grading Periods, Organizations, Schools
- Students, Teachers, Terms, Users
- All read operations and nested queries (e.g., `getStudentsForClass`)

**ClassLink does NOT support:**

- Demographics, Line Items, Results, Categories, Score Scales
- Course Components, Component Resources
- Any create/update/delete operations

### Full Client

For full OneRoster access with all methods available:

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-oneroster-server.com',
    security: {
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
        tokenURL: 'https://your-oneroster-server.com/oauth/token',
    },
});

// All operations available
const users = await client.usersManagement.getAllUsers({});
await client.usersManagement.createUser({
    user: {
        /* ... */
    },
});
```

### Custom Profiles

Create your own profiles by defining an interface that uses `Pick<>` to select specific methods:

```typescript
import { OneRoster } from '@eduware/oneroster';
import type { UsersManagement, ClassesManagement } from '@eduware/oneroster/sdk';
import type { OneRosterClient } from '@eduware/oneroster/clients';

// Define a custom profile with only the methods you need
interface IMyCustomProfile {
    usersManagement: Pick<UsersManagement, 'getAllUsers' | 'getUser'>;
    classesManagement: Pick<ClassesManagement, 'getAllClasses' | 'getClass'>;
}

// Create a typed client
type MyCustomClient = OneRosterClient<IMyCustomProfile>;

const client: MyCustomClient = new OneRoster({
    serverURL: 'https://your-server.com',
    security: {
        /* ... */
    },
});

// Only picked methods are available
const users = await client.usersManagement.getAllUsers({});
// client.usersManagement.createUser({});  // TypeScript error!
```

## OneRoster Versions

The SDK supports both OneRoster v1.1 and v1.2 specifications. The default is v1.2.

### Version 1.2 (Default)

OneRoster v1.2 uses OAuth 2.0 authentication and the `/ims/oneroster/rostering/v1p2/` path format:

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-server.com',
    // oneRosterVersion defaults to '1.2'
    security: {
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
        tokenURL: 'https://your-server.com/oauth/token',
    },
});
```

### Version 1.1

OneRoster v1.1 uses OAuth 1.0a authentication and the `/ims/oneroster/v1p1/` path format:

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-server.com',
    oneRosterVersion: '1.1',
    security: {
        authType: 'oauth1',
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
    },
});
```

> **Note:** OAuth 1.0a is only supported in OneRoster v1.1. Per the 1EdTech specification, v1.2 requires OAuth 2.0.

## Authentication

### OAuth 2.0 (Default for v1.2)

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-server.com',
    security: {
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
        tokenURL: 'https://your-server.com/oauth/token',
    },
});
```

You can also use environment variables:

- `ONEROSTER_CLIENT_ID`
- `ONEROSTER_CLIENT_SECRET`
- `ONEROSTER_TOKEN_URL`

### OAuth 1.0a (v1.1 only)

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-server.com',
    oneRosterVersion: '1.1',
    security: {
        authType: 'oauth1',
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
    },
});
```

### Bearer Token

If you already have a token:

```typescript
import { OneRoster } from '@eduware/oneroster';

const client = new OneRoster({
    serverURL: 'https://your-server.com',
    security: {
        authType: 'bearer',
        bearerToken: 'your-access-token',
    },
});
```

## Pagination

All list endpoints support automatic pagination using async iterables:

```typescript
const result = await client.usersManagement.getAllUsers({
    limit: 100, // Items per page (default: 100)
    offset: 0, // Starting offset
});

// Iterate through all pages automatically
for await (const page of result) {
    for (const user of page.users ?? []) {
        console.log(user.givenName, user.familyName);
    }
}
```

## Filtering & Sorting

```typescript
// Filter by status
const activeUsers = await client.usersManagement.getAllUsers({
    filter: "status='active'",
});

// Multiple filters
const filteredUsers = await client.usersManagement.getAllUsers({
    filter: "status='active' AND role='student'",
});

// Sorting
const sortedUsers = await client.usersManagement.getAllUsers({
    sort: 'familyName',
    orderBy: 'asc',
});

// Select specific fields
const partialUsers = await client.usersManagement.getAllUsers({
    fields: 'sourcedId,givenName,familyName,email',
});
```

## Error Handling

```typescript
import { OneRoster } from '@eduware/oneroster';
import * as errors from '@eduware/oneroster/models/errors';

const client = new OneRoster({
    /* ... */
});

try {
    const user = await client.usersManagement.getUser({
        sourcedId: 'non-existent-id',
    });
} catch (error) {
    if (error instanceof errors.NotFoundResponseError) {
        console.log('User not found');
    } else if (error instanceof errors.UnauthorizedRequestResponseError) {
        console.log('Authentication failed');
    } else if (error instanceof errors.OneRosterError) {
        // Base class for all HTTP errors
        console.log('HTTP Error:', error.statusCode, error.message);
    }
}
```

### Error Classes

| Error                              | Status Code | Description                |
| ---------------------------------- | ----------- | -------------------------- |
| `BadRequestResponseError`          | 400         | Invalid request parameters |
| `UnauthorizedRequestResponseError` | 401         | Authentication failed      |
| `ForbiddenResponseError`           | 403         | Access denied              |
| `NotFoundResponseError`            | 404         | Resource not found         |
| `UnprocessableEntityResponseError` | 422         | Validation error           |
| `TooManyRequestsResponseError`     | 429         | Rate limit exceeded        |
| `InternalServerErrorResponse`      | 500         | Server error               |

## Standalone Functions

For optimal bundle sizes in browser and serverless environments, use standalone functions:

```typescript
import { OneRosterCore } from '@eduware/oneroster/core.js';
import { usersManagementGetAllUsers } from '@eduware/oneroster/funcs/usersManagementGetAllUsers.js';

const client = new OneRosterCore({
    security: {
        clientID: process.env.ONEROSTER_CLIENT_ID!,
        clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
    },
});

const result = await usersManagementGetAllUsers(client, {});

if (result.ok) {
    for await (const page of result.value) {
        console.log(page.users);
    }
} else {
    console.error('Error:', result.error);
}
```

See [FUNCTIONS.md](./FUNCTIONS.md) for the complete list of standalone functions.

## MCP Server

This SDK includes an MCP (Model Context Protocol) server for AI applications:

```json
{
    "mcpServers": {
        "OneRoster": {
            "command": "npx",
            "args": [
                "-y",
                "--package",
                "@eduware/oneroster",
                "--",
                "mcp",
                "start",
                "--client-id",
                "your-client-id",
                "--client-secret",
                "your-client-secret",
                "--token-url",
                "https://your-server.com/oauth/token"
            ]
        }
    }
}
```

## Runtime Support

- **Browsers:** Chrome, Safari, Edge, Firefox (evergreen)
- **Node.js:** v18+ (LTS)
- **Bun:** v1+
- **Deno:** v1.39+

See [RUNTIMES.md](./RUNTIMES.md) for detailed runtime information.

## API Reference

For the complete list of available methods, see [FUNCTIONS.md](./FUNCTIONS.md).

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.

## License

MIT
