npm package: @newgameplusinc/odyssey-organization-sdk
Description: Official SDK for the Odyssey Organization backend
Author: New Game Plus Inc
Latest version: 1.0.6

# @newgameplusinc/odyssey-organization-sdk

[![npm version](https://img.shields.io/npm/v/@newgameplusinc/odyssey-organization-sdk)](https://www.npmjs.com/package/@newgameplusinc/odyssey-organization-sdk)
[![npm downloads](https://img.shields.io/npm/dt/@newgameplusinc/odyssey-organization-sdk)](https://www.npmjs.com/package/@newgameplusinc/odyssey-organization-sdk)

Official TypeScript SDK for the Odyssey Organization backend. Provides a fully typed, modular HTTP client for health checks, organizations, invites, projects, spaces, space invites, and uploads.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
- [Debug Logging](#debug-logging)
- [API Reference](#api-reference)
  - [Health](#health)
  - [Organizations](#organizations)
  - [Organization Invites](#organization-invites)
  - [Projects](#projects)
  - [Spaces](#spaces)
  - [Space Invites](#space-invites)
  - [Upload](#upload)
- [Types Reference](#types-reference)
  - [Config](#config)
  - [Common](#common)
  - [Organization Types](#organization-types)
  - [Project Types](#project-types)
  - [Space Types](#space-types)
  - [Upload Types](#upload-types)
  - [Health Types](#health-types)
  - [Error Types](#error-types)
- [Versioning](#versioning)

---

## Installation

```bash
npm install @newgameplusinc/odyssey-organization-sdk
# or
yarn add @newgameplusinc/odyssey-organization-sdk
# or
pnpm add @newgameplusinc/odyssey-organization-sdk
```

---

## Quick Start

```typescript
import { createOrganizationSDK } from "@newgameplusinc/odyssey-organization-sdk";

const sdk = createOrganizationSDK({ apiKey: "your-api-key" });

const health = await sdk.health.getHealth();
console.log(health.status);

const organization = await sdk.organization.create({
  name: "New Game Plus",
});

const projects = await sdk.projects.getByOrg(organization.id, {
  page: 1,
  limit: 10,
});

const space = await sdk.spaces.create({
  orgId: organization.id,
  name: "Main Lobby",
  spaceTemplateId: "template-id",
  unrealProject: {
    unrealProjectId: "uproject-id",
    unrealProjectVersionId: "uproject-version-id",
  },
});
```

You can also use the namespace default export:

```typescript
import OrganizationSDK from "@newgameplusinc/odyssey-organization-sdk";

const sdk = OrganizationSDK.create({ apiKey: "your-api-key" });
```

---

## Architecture

The SDK is structured as a set of focused modules. Each module handles one domain of the backend API, and all modules share a single configured HTTP client created once at initialization.

```text
createOrganizationSDK({ apiKey })
│
├── sdk.health               → /health
├── sdk.organization         → /organizations
├── sdk.organizationInvites  → /organizations-invite
├── sdk.projects             → /projects
├── sdk.spaces               → /spaces
├── sdk.spaceInvites         → /space-invite
└── sdk.upload               → /upload
```

**Key design decisions:**

- The backend base URL is baked into the SDK at build time and is not configured by consumers.
- Your API key is passed once at initialization and injected automatically as the `x-org-sdk-key` header on every request.
- All request failures are normalized into typed `OdysseyError` objects with stable `name` and `code` fields.
- Debug logging is opt-in and silent by default.

---

## Configuration

```typescript
import {
  createOrganizationSDK,
  OrganizationSDKConfig,
} from "@newgameplusinc/odyssey-organization-sdk";

const sdk = createOrganizationSDK({
  apiKey: "your-api-key",
  timeout: 60_000,
});
```

| Option    | Type     | Required | Default | Description                                              |
| --------- | -------- | -------- | ------- | -------------------------------------------------------- |
| `apiKey`  | `string` | Yes      | -       | API key sent as the `x-org-sdk-key` header on every request. |
| `timeout` | `number` | No       | `30000` | Request timeout in milliseconds.                         |

---

## Error Handling

All SDK errors are instances of `OdysseyError`. You can use `isOdysseyError()` to safely narrow the type in catch blocks.

```typescript
import { isOdysseyError } from "@newgameplusinc/odyssey-organization-sdk";

try {
  const organization = await sdk.organization.getById("org-id");
} catch (err) {
  if (isOdysseyError(err)) {
    console.log(err.name);
    console.log(err.code);
    console.log(err.message);
  }
}
```

### Error Types

| `err.name`               | `err.code`         | When it is thrown                                                      |
| ------------------------ | ------------------ | ---------------------------------------------------------------------- |
| `OdysseyConfigError`     | `INVALID_CONFIG`   | API key is missing or the SDK build has no base URL configured.        |
| `OdysseyAuthError`       | `AUTH_ERROR`       | Server returns `401` or `403`.                                         |
| `OdysseyValidationError` | `VALIDATION_ERROR` | Server returns `400` or `422`.                                         |
| `OdysseyUploadError`     | `UPLOAD_ERROR`     | Upload-specific failure category exported by the SDK error helpers.    |
| `OdysseyRequestError`    | `REQUEST_ERROR`    | Network failure, timeout, or any other non-validation request failure. |
| `OdysseySDKError`        | `SDK_ERROR`        | Generic fallback error.                                                |

---

## Debug Logging

The SDK ships with a built-in debug logger that is silent by default. Enable it to see outgoing requests, response statuses, and module-level operations.

**In Node.js:**

```bash
ODYSSEY_DEBUG=true node your-script.js
```

**In the browser:**

```javascript
window.ODYSSEY_DEBUG = true;
```

Example log output:

```text
[14:23:01.452][SDK:HTTP] → POST /organizations
[14:23:01.891][SDK:HTTP] ← 201 /organizations
[14:23:01.892][SDK:Organization] create { id: "..." }
```

---

## API Reference

### Health

#### `sdk.health.getHealth()`

Check if the backend service is running.

```typescript
const health = await sdk.health.getHealth();
```

**Returns:** `Promise<HealthResponse>`

---

#### `sdk.health.getRedisHealth()`

Check Redis health and latency.

```typescript
const redis = await sdk.health.getRedisHealth();
```

**Returns:** `Promise<RedisHealthResponse>`

---

#### `sdk.health.testRedis()`

Run a Redis test operation.

```typescript
await sdk.health.testRedis();
```

**Returns:** `Promise<void>`

---

### Organizations

#### `sdk.organization.create(data)`

Create a new organization.

```typescript
const organization = await sdk.organization.create({
  name: "New Game Plus",
});
```

**Returns:** `Promise<Organization>`

---

#### `sdk.organization.getAll(query?)`

Fetch paginated organizations.

```typescript
const result = await sdk.organization.getAll({
  page: 1,
  limit: 10,
});
```

**Returns:** `Promise<OrganizationListResult>`

---

#### `sdk.organization.getById(id)`

Fetch a single organization by ID.

```typescript
const organization = await sdk.organization.getById("org-id");
```

**Returns:** `Promise<Organization>`

---

#### `sdk.organization.getByUser()`

Fetch organizations for the current authenticated user.

```typescript
const organizations = await sdk.organization.getByUser();
```

**Returns:** `Promise<Organization[]>`

---

#### `sdk.organization.getBySdkKey()`

Fetch organizations by `x-org-sdk-key`.

```typescript
const result = await sdk.organization.getBySdkKey();
```

**Returns:** `Promise<OrganizationsBySdkKeyResult>`

---

#### `sdk.organization.getUsers(orgId)`

Fetch organization users by organization ID.

```typescript
const users = await sdk.organization.getUsers("org-id");
```

**Returns:** `Promise<OrganizationUser[]>`

---

#### `sdk.organization.update(id, data)`

Update organization fields.

```typescript
const updated = await sdk.organization.update("org-id", {
  name: "Updated Name",
  domain: "new-domain",
  logoSmallUrl: "https://example.com/logo.png",
});
```

**Returns:** `Promise<Organization>`

---

#### `sdk.organization.updateUser(userId, data)`

Update an organization user.

```typescript
const user = await sdk.organization.updateUser("user-id", {
  roleId: "role-id",
  avatarReadyPlayerMeImg: "https://example.com/avatar.png",
});
```

**Returns:** `Promise<OrganizationUser>`

---

#### `sdk.organization.delete(id)`

Delete an organization.

```typescript
const deleted = await sdk.organization.delete("org-id");
```

**Returns:** `Promise<Organization>`

---

#### `sdk.organization.deleteUser(userId)`

Delete an organization user.

```typescript
const deleted = await sdk.organization.deleteUser("user-id");
```

**Returns:** `Promise<OrganizationUser>`

---

### Organization Invites

#### `sdk.organizationInvites.create(data)`

Create an organization invite.

```typescript
const invite = await sdk.organizationInvites.create({
  orgId: "org-id",
  email: "user@example.com",
  roleId: "role-id",
});
```

**Returns:** `Promise<OrganizationInvite>`

---

#### `sdk.organizationInvites.updateRole(inviteId, data)`

Update the `roleId` for a pending organization invite.

```typescript
const invite = await sdk.organizationInvites.updateRole("invite-id", {
  roleId: "new-role-id",
});
```

**Returns:** `Promise<OrganizationInvite>`

---

#### `sdk.organizationInvites.accept(inviteId)`

Accept an organization invite.

```typescript
const invite = await sdk.organizationInvites.accept("invite-link-id");
```

**Returns:** `Promise<OrganizationInvite>`

---

#### `sdk.organizationInvites.reject(inviteId)`

Reject an organization invite.

```typescript
const invite = await sdk.organizationInvites.reject("invite-link-id");
```

**Returns:** `Promise<OrganizationInvite>`

---

#### `sdk.organizationInvites.getPendingUsers(orgId)`

Fetch pending invites for an organization.

```typescript
const invites = await sdk.organizationInvites.getPendingUsers("org-id");
```

**Returns:** `Promise<OrganizationInvite[]>`

---

#### `sdk.organizationInvites.getPendingUserByInviteId(inviteId)`

Fetch a pending organization invite by invite link ID.

```typescript
const invite =
  await sdk.organizationInvites.getPendingUserByInviteId("invite-link-id");
```

**Returns:** `Promise<OrganizationInvite | null>`

---

### Projects

#### `sdk.projects.create(data)`

Create a project.

```typescript
const project = await sdk.projects.create({
  orgId: "org-id",
  name: "Project Alpha",
  description: "Primary experience",
});
```

**Returns:** `Promise<Project>`

---

#### `sdk.projects.getByOrg(orgId, query?)`

Fetch paginated projects by organization ID.

```typescript
const projects = await sdk.projects.getByOrg("org-id", {
  page: 1,
  limit: 10,
});
```

**Returns:** `Promise<ProjectListResult>`

---

#### `sdk.projects.getById(projectId)`

Fetch a project by ID.

```typescript
const project = await sdk.projects.getById("project-id");
```

**Returns:** `Promise<Project>`

---

#### `sdk.projects.update(projectId, data)`

Update a project.

```typescript
const project = await sdk.projects.update("project-id", {
  name: "Updated Project",
  description: "Updated description",
  isActive: true,
});
```

**Returns:** `Promise<Project>`

---

#### `sdk.projects.delete(projectId)`

Delete a project.

```typescript
const deleted = await sdk.projects.delete("project-id");
```

**Returns:** `Promise<Project>`

---

### Spaces

#### `sdk.spaces.create(data)`

Create a space.

```typescript
const space = await sdk.spaces.create({
  orgId: "org-id",
  name: "Main Lobby",
  spaceTemplateId: "template-id",
  unrealProject: {
    unrealProjectId: "uproject-id",
    unrealProjectVersionId: "uproject-version-id",
  },
});
```

**Returns:** `Promise<Space>`

---

#### `sdk.spaces.getByOrg(orgId)`

Fetch spaces by organization ID.

```typescript
const spaces = await sdk.spaces.getByOrg("org-id");
```

**Returns:** `Promise<Space[]>`

---

#### `sdk.spaces.getByProject(projectId)`

Fetch spaces by project ID.

```typescript
const spaces = await sdk.spaces.getByProject("project-id");
```

**Returns:** `Promise<Space[]>`

---

#### `sdk.spaces.getUsers(spaceId)`

Fetch users for a space.

```typescript
const users = await sdk.spaces.getUsers("space-id");
```

**Returns:** `Promise<SpaceUser[]>`

---

#### `sdk.spaces.update(spaceId, data)`

Update a space.

```typescript
const updated = await sdk.spaces.update("space-id", {
  name: "Updated Space",
  description: "Updated description",
  thumb: "https://example.com/thumb.png",
});
```

**Returns:** `Promise<Space>`

---

#### `sdk.spaces.updateSetting(spaceId, data)`

Update a space setting record.

```typescript
const setting = await sdk.spaces.updateSetting("space-id", {
  isPublic: true,
  allowAnonymousUsers: false,
  odysseyMobileControls: "ON",
  avatarControlSystem: "EVENT_MODE",
});
```

**Returns:** `Promise<SpaceSetting>`

---

#### `sdk.spaces.updateUser(userId, data)`

Update a space user.

```typescript
const user = await sdk.spaces.updateUser("space-user-id", {
  roleId: "role-id",
  avatarUrl: "https://example.com/avatar.png",
});
```

**Returns:** `Promise<SpaceUser>`

---

#### `sdk.spaces.delete(spaceId)`

Delete a space.

```typescript
const deleted = await sdk.spaces.delete("space-id");
```

**Returns:** `Promise<Space>`

---

#### `sdk.spaces.deleteUser(userId)`

Delete a space user.

```typescript
const deleted = await sdk.spaces.deleteUser("space-user-id");
```

**Returns:** `Promise<SpaceUser>`

---

### Space Invites

#### `sdk.spaceInvites.create(data)`

Create a space invite.

```typescript
const invite = await sdk.spaceInvites.create({
  spaceId: "space-id",
  email: "user@example.com",
  roleId: "role-id",
});
```

**Returns:** `Promise<SpaceInvite>`

---

#### `sdk.spaceInvites.accept(inviteId)`

Accept a space invite.

```typescript
const invite = await sdk.spaceInvites.accept("invite-link-id");
```

**Returns:** `Promise<SpaceInvite>`

---

#### `sdk.spaceInvites.reject(inviteId)`

Reject a space invite.

```typescript
const invite = await sdk.spaceInvites.reject("invite-link-id");
```

**Returns:** `Promise<SpaceInvite>`

---

#### `sdk.spaceInvites.getPendingUsers(spaceId)`

Fetch pending invites for a space.

```typescript
const invites = await sdk.spaceInvites.getPendingUsers("space-id");
```

**Returns:** `Promise<SpaceInvite[]>`

---

#### `sdk.spaceInvites.getPendingUserByInviteId(inviteId)`

Fetch a pending space invite by invite link ID.

```typescript
const invite =
  await sdk.spaceInvites.getPendingUserByInviteId("invite-link-id");
```

**Returns:** `Promise<SpaceInvite | null>`

---

### Upload

#### `sdk.upload.getPresignedUrl(fileType)`

Create a presigned URL to upload a file to storage.

```typescript
const presigned = await sdk.upload.getPresignedUrl("image/png");

// Upload from Node.js (example)
await fetch(presigned.url, {
  method: presigned.method,
  headers: presigned.headers,
  body: yourFileBuffer,
});
```

**Returns:** `Promise<UploadPresignedUrl>`

---

## Types Reference

All types are exported directly from the package root:

```typescript
import type { ... } from "@newgameplusinc/odyssey-organization-sdk";
```

---

### Config

```typescript
type OrganizationSDKConfig = {
  apiKey: string;
  timeout?: number;
};
```

---

### Common

```typescript
type PaginationQuery = {
  page?: number;
  limit?: number;
};

type PaginationMeta = {
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

type InviteStatus = "PENDING" | "ACCEPTED" | "REJECTED" | "EXPIRES";

type InviteType = "EMAIL" | "LINK";

type OdysseyMobileControls = "ON" | "OFF" | "JOYSTICK_ONLY";

type AvatarType = "STANDARD" | "AEC";

type AvatarControlSystem = "EVENT_MODE" | "GAME_MODE" | "FLIGHT_MODE";
```

---

### Organization Types

```typescript
type Organization = {
  id: string;
  name: string;
  domain: string;
  logoSmallUrl: string | null;
  sdkKeyId?: string | null;
  createdAt: string;
  updatedAt: string;
};

type OrganizationUser = {
  id: string;
  orgId: string;
  email: string;
  roleId: string;
  avatarReadyPlayerMeImg: string | null;
  createdAt: string;
  updatedAt: string;
  organization?: Organization;
};

type OrganizationConfiguration = {
  id: string;
  orgId: string;
  unrealImageId: string;
  unrealImageRepo: string;
  workloadClusterProvider: string;
  createdAt: string;
  updatedAt: string;
  organization?: Organization;
};

type OrganizationByUserResult = {
  organization: Organization;
};

type CreateOrganizationInput = {
  name: string;
};

type UpdateOrganizationInput = {
  name?: string | null;
  domain?: string | null;
  logoSmallUrl?: string | null;
};

type UpdateOrganizationUserInput = {
  roleId?: string;
  avatarReadyPlayerMeImg?: string;
};

type OrganizationInvite = {
  id: string;
  orgId: string;
  email: string;
  roleId: string;
  inviteLinkId: string;
  avatarReadyPlayerMeImg: string | null;
  actionAt: string | null;
  status: InviteStatus;
  type: InviteType;
  createdAt: string;
  updatedAt: string;
  organization?: Organization;
};

type CreateOrganizationInviteInput = {
  orgId: string;
  email: string;
  roleId: string;
};

type UpdateOrganizationInviteRoleInput = {
  roleId: string;
};

type OrganizationListResult = {
  data: Organization[];
  meta: PaginationMeta;
};

type OrganizationsBySdkKeyResult = {
  organization: Organization[];
};
```

---

### Project Types

```typescript
type Project = {
  id: string;
  orgId: string;
  name: string;
  description: string | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
  organization?: Organization;
};

type CreateProjectInput = {
  orgId: string;
  name: string;
  description?: string | null;
};

type UpdateProjectInput = {
  name?: string;
  description?: string | null;
  isActive?: boolean;
};

type ProjectListResult = {
  data: Project[];
  meta: PaginationMeta;
};
```

---

### Space Types

```typescript
type UnrealProject = {
  unrealProjectId: string;
  unrealProjectVersionId: string;
};

type Space = {
  id: string;
  orgId: string;
  projectId: string;
  name: string;
  description: string | null;
  unrealProject: UnrealProject | Record<string, unknown>;
  currentParticipantSum: number;
  spaceTemplateId: string;
  thumb: string | null;
  spaceItemSum: number;
  createdAt: string;
  updatedAt: string;
  organization?: Organization;
  spaceSetting?: SpaceSetting | null;
};

type SpaceSetting = {
  id: string;
  spaceId: string;
  isPublic: boolean;
  afkTimer: number;
  maxSessionLength: number;
  allowAnonymousUsers: boolean;
  allowEmbed: boolean;
  allowConfigurationToolbarForAllUsers: boolean;
  disableChat: boolean;
  disableComms: boolean;
  enableSharding: boolean;
  isLiveStreamActive: boolean;
  showHelpMenu: boolean;
  showLoadingBackground: boolean;
  showLoadingBackgroundBlur: boolean;
  showOdysseyEditorMenu: boolean;
  showSpaceInformation: boolean;
  notViewerBuddle: boolean;
  maximumResolution?: unknown;
  odysseyMobileControls: OdysseyMobileControls;
  avatarType: AvatarType;
  avatarControlSystem: AvatarControlSystem;
  createdAt: string;
  updatedAt: string;
  space?: Space;
};

type SpaceUser = {
  id: string;
  spaceId: string;
  email: string;
  roleId: string;
  isPending: boolean;
  avatarUrl: string | null;
  createdAt: string;
  updatedAt: string;
  space?: Space;
};

type SpaceInvite = {
  id: string;
  spaceId: string;
  email: string;
  roleId: string;
  inviteLinkId: string;
  avatarReadyPlayerMeImg: string | null;
  actionAt: string | null;
  status: InviteStatus;
  type: InviteType;
  createdAt: string;
  updatedAt: string;
  space?: Space;
};

type CreateSpaceInput = {
  orgId: string;
  name: string;
  spaceTemplateId: string;
  unrealProject: {
    unrealProjectId: string;
    unrealProjectVersionId: string;
  };
};

type UpdateSpaceInput = {
  name?: string;
  description?: string | null;
  thumb?: string | null;
};

type UpdateSpaceSettingInput = {
  isPublic?: boolean;
  afkTimer?: number;
  maxSessionLength?: number;
  allowAnonymousUsers?: boolean;
  allowEmbed?: boolean;
  allowConfigurationToolbarForAllUsers?: boolean;
  disableChat?: boolean;
  disableComms?: boolean;
  enableSharding?: boolean;
  isLiveStreamActive?: boolean;
  showHelpMenu?: boolean;
  showLoadingBackground?: boolean;
  showLoadingBackgroundBlur?: boolean;
  showOdysseyEditorMenu?: boolean;
  showSpaceInformation?: boolean;
  notViewerBuddle?: boolean;
  maximumResolution?: unknown;
  odysseyMobileControls?: OdysseyMobileControls;
  avatarType?: AvatarType;
  avatarControlSystem?: AvatarControlSystem;
};

type UpdateSpaceUserInput = {
  roleId?: string;
  avatarUrl?: string;
};

type CreateSpaceInviteInput = {
  spaceId: string;
  email: string;
  roleId: string;
};
```

---

### Upload Types

```typescript
type UploadPresignedUrl = {
  url: string;
  method: "PUT";
  headers: {
    "Content-Type": string;
  };
};
```

---

### Health Types

```typescript
type HealthResponse = {
  status: string;
  timestamp: string;
};

type RedisHealthResponse = {
  status: string;
  latencyMs?: number;
};
```

---

### Error Types

```typescript
interface OdysseyError extends Error {
  readonly code: string;
}

function isOdysseyError(err: unknown): err is OdysseyError;
```

---

## Versioning

This SDK follows semantic versioning:

| Bump              | When                                                       |
| ----------------- | ---------------------------------------------------------- |
| `patch` - `x.x.1` | Bug fixes with no public API changes.                      |
| `minor` - `x.1.0` | New methods or fields added in a backwards-compatible way. |
| `major` - `2.0.0` | Breaking changes to existing method signatures or types.   |

---

## License

MIT