npm package: @newgameplusinc/odyssey-asset-manager-sdk
Description: Official SDK for the Odyssey Asset Manager backend
Author: New Game Plus Inc
Latest version: 3.1.1

# @newgameplusinc/odyssey-asset-manager-sdk

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

Official TypeScript SDK for the Odyssey Asset Manager backend. Provides a fully typed, modular HTTP client for all asset management, upload lifecycle, version, and user operations.

---

## 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)
  - [Assets](#assets)
  - [Versions](#versions)
  - [Users](#users)
  - [Upload](#upload)
  - [Sanitization](#sanitization)
- [Types Reference](#types-reference)
  - [Config](#config)
  - [Common](#common)
  - [Asset Types](#asset-types)
  - [Version Types](#version-types)
  - [User Types](#user-types)
  - [Upload Types](#upload-types)
  - [Health Types](#health-types)
  - [Sanitization Types](#sanitization-types)
  - [Error Types](#error-types)
- [Versioning](#versioning)

---

## Installation

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

---

## Quick Start

```typescript
import { createAssetManagerSDK } from "@newgameplusinc/odyssey-asset-manager-sdk";

// Initialize once — pass your API key, that's it.
const sdk = createAssetManagerSDK({ apiKey: "your-api-key" });

// Check server health
const health = await sdk.health.getHealth();
console.log(health.status); // 'ok'

// Fetch all assets for an org
const assets = await sdk.assets.getByOrg("your-org-id");

// Start a multipart upload
const session = await sdk.upload.initiate({
  orgId: "your-org-id",
  projectId: "your-project-id",
  userId: "your-user-id",
  assetType: "UNREAL_PROJECT",
  assetFilename: "MyProject.zip",
  unrealEngineVersion: "5.2.1",
  target: "Development",
  selfPackaged: true,
});
```

You can also use the namespace default export:

```typescript
import AssetManagerSDK from "@newgameplusinc/odyssey-asset-manager-sdk";

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

---

## Architecture

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

```
createAssetManagerSDK({ apiKey })
│
├── sdk.health        → /health
├── sdk.assets        → /assets
├── sdk.versions      → /unrealProjectVersion
├── sdk.users         → /users
├── sdk.upload        → /uploader
└── sdk.sanitization  → /deepSanitization
```

**Key design decisions:**

- The backend base URL is baked into the SDK at build time. You never pass a URL — it is not configurable by consumers.
- Your API key is passed once at initialization and is automatically injected as `x-sdk-key` on every request.
- All errors are normalized into typed `OdysseyError` objects with a `code` and `name` so you can catch them reliably.
- Debug logging is fully opt-in and zero-cost in production.

---

## Configuration

```typescript
import {
  createAssetManagerSDK,
  AssetManagerSDKConfig,
} from "@newgameplusinc/odyssey-asset-manager-sdk";

const sdk = createAssetManagerSDK({
  apiKey: "your-api-key", // Required. Issued after SDK purchase.
  timeout: 60_000, // Optional. Default: 30000ms.
});
```

| Option    | Type     | Required | Default | Description                                                                     |
| --------- | -------- | -------- | ------- | ------------------------------------------------------------------------------- |
| `apiKey`  | `string` | Yes      | —       | API key issued after SDK purchase. Sent as `x-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-asset-manager-sdk";

try {
  const asset = await sdk.assets.getById("some-id");
} catch (err) {
  if (isOdysseyError(err)) {
    console.log(err.name); // e.g. 'OdysseyAuthError'
    console.log(err.code); // e.g. 'AUTH_ERROR'
    console.log(err.message); // human-readable description
  }
}
```

### Error Types

| `err.name`               | `err.code`         | When it is thrown                                           |
| ------------------------ | ------------------ | ----------------------------------------------------------- |
| `OdysseyConfigError`     | `INVALID_CONFIG`   | API key is missing or base URL not configured in the build. |
| `OdysseyAuthError`       | `AUTH_ERROR`       | Server returns `401` or `403` — key is invalid or expired.  |
| `OdysseyValidationError` | `VALIDATION_ERROR` | Server returns `400` or `422` — bad input data.             |
| `OdysseyUploadError`     | `UPLOAD_ERROR`     | Any failure on `/uploader/*` routes.                        |
| `OdysseyRequestError`    | `REQUEST_ERROR`    | Network failure, timeout, or any other `5xx` response.      |
| `OdysseySDKError`        | `SDK_ERROR`        | Generic fallback for uncategorized errors.                  |

---

## Debug Logging

The SDK ships with a built-in debug logger that is completely silent by default. Enable it to see every outgoing request, response status, and module-level operation in your console.

**In Node.js:**

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

**In the browser:**

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

All log lines are prefixed with a timestamp and category:

```
[14:23:01.452][SDK:HTTP] → POST /uploader/initiate
[14:23:01.891][SDK:HTTP] ← 200 /uploader/initiate
[14:23:01.892][SDK:Upload] initiate { orgId: '...', assetId: '...' }
```

---

## API Reference

### Health

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

Check if the backend service is running and healthy.

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

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

```typescript
{
  status: "ok",
  timestamp: "2024-01-01T00:00:00.000Z"
}
```

---

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

Check Redis connection health and measure latency.

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

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

```typescript
{
  status: "ok",
  latencyMs: 2
}
```

---

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

Run a test write/read cycle on Redis. Useful for smoke-testing after deployment.

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

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

---

### Assets

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

Create a new asset record.

```typescript
const asset = await sdk.assets.create({
  name: "My Project",
  orgId: "org-id",
  projectId: "project-id",
  assetType: "UNREAL_PROJECT",
  sourceType: "UNIVERSAL_SCENE",
  storageBucket: "my-bucket",
  storagePath: "path/to/asset",
});
```

| Field           | Type         | Required | Description                         |
| --------------- | ------------ | -------- | ----------------------------------- |
| `name`          | `string`     | Yes      | Display name of the asset.          |
| `orgId`         | `string`     | Yes      | Organization ID (UUID).             |
| `projectId`     | `string`     | Yes      | Project ID.                         |
| `assetType`     | `AssetType`  | Yes      | `"UNREAL_PROJECT"` or `"OTHER_3D"`. |
| `sourceType`    | `SourceType` | Yes      | `"UNIVERSAL_SCENE"`.                |
| `storageBucket` | `string`     | Yes      | GCS bucket name.                    |
| `storagePath`   | `string`     | Yes      | GCS object path.                    |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.getAll()`

Fetch all assets across all organizations.

```typescript
const assets = await sdk.assets.getAll();
```

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

---

#### `sdk.assets.getById(assetId)`

Fetch a single asset by its ID.

```typescript
const asset = await sdk.assets.getById("asset-id");
```

| Parameter | Type     | Description            |
| --------- | -------- | ---------------------- |
| `assetId` | `string` | The asset's unique ID. |

**Returns:** `Promise<AssetWithRelations>`

---

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

Fetch all assets belonging to a specific organization.

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

| Parameter | Type     | Description                   |
| --------- | -------- | ----------------------------- |
| `orgId`   | `string` | The organization's unique ID. |

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

---

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

Fetch all assets belonging to a specific project.

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

| Parameter   | Type     | Description              |
| ----------- | -------- | ------------------------ |
| `projectId` | `string` | The project's unique ID. |

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

---

#### `sdk.assets.update(assetId, data)`

Update one or more fields on an asset.

```typescript
const updated = await sdk.assets.update("asset-id", {
  name: "New Asset Name",
  buildStatus: "COMPLETED",
  uploadId: "gcs-upload-id",
  objectName: "path/to/object.zip",
  unrealProject: {
    displayName: "Updated Display Name",
    unrealProjectVersion: "1.0.1",
  },
});
```

| Parameter | Type               | Description                                |
| --------- | ------------------ | ------------------------------------------ |
| `assetId` | `string`           | The asset's unique ID.                     |
| `data`    | `UpdateAssetInput` | Fields to update. All fields are optional. |

**`UpdateAssetInput` fields:**

| Field              | Type                       | Description                                                       |
| ------------------ | -------------------------- | ----------------------------------------------------------------- |
| `name`             | `string`                   | New display name.                                                 |
| `uploadStatus`     | `UploadStatus`             | `"INITIATED"` \| `"UPLOADING"` \| `"COMPLETED"` \| `"FAILED"`     |
| `validationStatus` | `ValidationStatus`         | `"PENDING"` \| `"VALID"` \| `"INVALID"`                           |
| `buildStatus`      | `BuildStatus`              | `"NOT_APPLICABLE"` \| `"BUILDING"` \| `"COMPLETED"` \| `"FAILED"` |
| `storageBucket`    | `string`                   | GCS bucket name.                                                  |
| `storagePath`      | `string`                   | GCS object path.                                                  |
| `uploadId`         | `string`                   | GCS multipart upload ID.                                          |
| `objectName`       | `string`                   | GCS object name.                                                  |
| `downloadUrl`      | `string`                   | Public download URL.                                              |
| `unrealProject`    | `UpdateUnrealProjectInput` | Nested Unreal project fields to update.                           |
| `other3d`          | `UpdateOther3dInput`       | Nested Other3D fields to update.                                  |
| `thumb`            | `string`                   | Thumbnail URL.                                                    |
| `description`      | `string`                   | Asset description.                                                |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.updateStatus(assetId, data)`

Update only the status fields of an asset.

```typescript
const updated = await sdk.assets.updateStatus("asset-id", {
  uploadStatus: "COMPLETED",
  validationStatus: "VALID",
  buildStatus: "BUILDING",
});
```

| Parameter | Type                     | Description                                       |
| --------- | ------------------------ | ------------------------------------------------- |
| `assetId` | `string`                 | The asset's unique ID.                            |
| `data`    | `UpdateAssetStatusInput` | Status fields to update. All fields are optional. |

**`UpdateAssetStatusInput` fields:**

| Field              | Type               | Description                                                       |
| ------------------ | ------------------ | ----------------------------------------------------------------- |
| `uploadStatus`     | `UploadStatus`     | `"INITIATED"` \| `"UPLOADING"` \| `"COMPLETED"` \| `"FAILED"`     |
| `validationStatus` | `ValidationStatus` | `"PENDING"` \| `"VALID"` \| `"INVALID"`                           |
| `buildStatus`      | `BuildStatus`      | `"NOT_APPLICABLE"` \| `"BUILDING"` \| `"COMPLETED"` \| `"FAILED"` |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.markAsUploaded(assetId)`

Shorthand to set `uploadStatus` to `"COMPLETED"`.

```typescript
await sdk.assets.markAsUploaded("asset-id");
```

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.markAsValidated(assetId)`

Shorthand to set `validationStatus` to `"VALID"`.

```typescript
await sdk.assets.markAsValidated("asset-id");
```

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.markAsFailed(assetId, stage)`

Shorthand to mark an asset as failed at a specific pipeline stage.

```typescript
await sdk.assets.markAsFailed("asset-id", "upload");
```

| Parameter | Type                                  | Description                          |
| --------- | ------------------------------------- | ------------------------------------ |
| `assetId` | `string`                              | The asset's unique ID.               |
| `stage`   | `"upload" \| "validation" \| "build"` | The stage at which the asset failed. |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.uploadThumb(assetId, userId, file)`

Upload a thumbnail image for an asset.

```typescript
const updated = await sdk.assets.uploadThumb(
  "asset-id",
  "user-id",
  file, // File | Blob
);
```

| Parameter | Type           | Description                             |
| --------- | -------------- | --------------------------------------- |
| `assetId` | `string`       | The asset's unique ID.                  |
| `userId`  | `string`       | ID of the user uploading the thumbnail. |
| `file`    | `File \| Blob` | The thumbnail image file.               |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.assets.delete(assetId)`

Permanently delete an asset record. For full deletion including GCS files and database footprint, use `sdk.sanitization.deleteProjectAndFootprint()` instead.

```typescript
await sdk.assets.delete("asset-id");
```

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

---

### Versions

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

Create a new Unreal project version record.

```typescript
const version = await sdk.versions.create({
  orgId: "org-id",
  projectId: "project-id",
  authorUserId: "user-id",
  appType: "unreal",
  unrealProjectId: "unreal-project-id",
  name: "v1.0.0",
  pluginVersionId: "plugin-version-id",
  selfPackaged: true,
  state: "new",
  target: "Development",
  uploader: "uploader-id",
  packageArchiveSha256Sum: "abc123",
  packageArchiveUrl: "https://...",
  symbolsArchiveSha256Sum: "def456",
  symbolsArchiveUrl: "https://...",
  bridgeToolkitFileSettings: {},
  stateChanges: {},
  volumeCopyRegionsComplete: [],
  volumeRegions: ["ORD1"],
  volumeSizeGb: 10,
  unrealEngineVersion: "5.2.1",
});
```

**Required fields:**

| Field                       | Type                         | Description                                |
| --------------------------- | ---------------------------- | ------------------------------------------ |
| `orgId`                     | `string`                     | Organization ID (UUID).                    |
| `projectId`                 | `string`                     | Project ID.                                |
| `authorUserId`              | `string`                     | ID of the user creating the version.       |
| `appType`                   | `string`                     | Application type identifier.               |
| `unrealProjectId`           | `string`                     | Unreal project ID (CUID).                  |
| `name`                      | `string`                     | Version name.                              |
| `pluginVersionId`           | `string`                     | Plugin version ID.                         |
| `selfPackaged`              | `boolean`                    | Whether the project was self-packaged.     |
| `state`                     | `ProjectVersionState`        | Initial lifecycle state. See states below. |
| `target`                    | `UnrealProjectVersionTarget` | `"Development"` or `"Shipping"`.           |
| `uploader`                  | `string`                     | Uploader identifier.                       |
| `packageArchiveSha256Sum`   | `string`                     | SHA-256 checksum of the package archive.   |
| `packageArchiveUrl`         | `string`                     | URL of the package archive.                |
| `symbolsArchiveSha256Sum`   | `string`                     | SHA-256 checksum of the symbols archive.   |
| `symbolsArchiveUrl`         | `string`                     | URL of the symbols archive.                |
| `bridgeToolkitFileSettings` | `Record<string, any>`        | Bridge toolkit file settings map.          |
| `stateChanges`              | `Record<string, any>`        | State change history map.                  |
| `volumeCopyRegionsComplete` | `string[]`                   | Regions where volume copy is complete.     |
| `volumeRegions`             | `string[]`                   | Target volume regions.                     |
| `volumeSizeGb`              | `number`                     | Volume size in GB.                         |

**Optional fields:**

| Field                          | Type                           | Description                            |
| ------------------------------ | ------------------------------ | -------------------------------------- |
| `buildRegion`                  | `string`                       | Preferred build region (max 10 chars). |
| `unrealEngineVersion`          | `SupportedUnrealEngineVersion` | `"5.0.3"` or `"5.2.1"`.                |
| `uploadId`                     | `string`                       | GCS upload ID.                         |
| `objectName`                   | `string`                       | GCS object name.                       |
| `downloadUrl`                  | `string`                       | Public download URL.                   |
| `uploadSha256Sum`              | `string`                       | SHA-256 checksum of the upload.        |
| `unrealProjectDirectoryPath`   | `string`                       | Path to the Unreal project directory.  |
| `disableMultiplayer`           | `boolean`                      | Whether to disable multiplayer.        |
| `lastPingFromBuilder`          | `Date`                         | Last ping timestamp from builder.      |
| `lastPingFromVolumeCopyRegion` | `Date`                         | Last ping from volume copy region.     |
| `levelFilePath`                | `string`                       | Path to the level file.                |
| `levelName`                    | `string`                       | Unreal level name.                     |

**Returns:** `Promise<UnrealProjectVersion>`

---

#### `sdk.versions.getById(versionId)`

Fetch a single Unreal project version by ID.

```typescript
const version = await sdk.versions.getById("version-id");
```

**Returns:** `Promise<UnrealProjectVersion>`

---

#### `sdk.versions.getByAsset(assetId)`

Fetch all versions associated with an asset.

```typescript
const versions = await sdk.versions.getByAsset("asset-id");
```

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

---

#### `sdk.versions.update(versionId, data)`

Update any fields on a project version. Accepts any key-value pairs — all fields are optional.

```typescript
await sdk.versions.update("version-id", {
  state: "upload_complete",
  target: "Shipping",
  unrealEngineVersion: "5.2.1",
  levelName: "MyLevel",
});
```

| Parameter   | Type                  | Description              |
| ----------- | --------------------- | ------------------------ |
| `versionId` | `string`              | The version's unique ID. |
| `data`      | `Record<string, any>` | Fields to update.        |

**Returns:** `Promise<UnrealProjectVersion>`

---

#### `sdk.versions.delete(versionId)`

Delete a single project version by ID.

```typescript
await sdk.versions.delete("version-id");
```

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

---

#### `sdk.versions.deleteByAsset(assetId)`

Delete all versions associated with an asset.

```typescript
await sdk.versions.deleteByAsset("asset-id");
```

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

---

### Users

#### `sdk.users.getAll()`

Fetch all users.

```typescript
const users = await sdk.users.getAll();
```

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

---

#### `sdk.users.getById(userId)`

Fetch a single user by ID.

```typescript
const user = await sdk.users.getById("user-id");
```

**Returns:** `Promise<User>`

---

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

Create a new user.

```typescript
const user = await sdk.users.create({ id: "custom-user-id" });
```

| Field | Type     | Required | Description                                         |
| ----- | -------- | -------- | --------------------------------------------------- |
| `id`  | `string` | No       | Optional custom ID. Auto-generated if not provided. |

**Returns:** `Promise<User>`

---

#### `sdk.users.update(userId, data)`

Update a user.

```typescript
const user = await sdk.users.update("user-id", { id: "new-id" });
```

**Returns:** `Promise<User>`

---

#### `sdk.users.delete(userId)`

Delete a user by ID.

```typescript
await sdk.users.delete("user-id");
```

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

---

### Upload

The upload module manages the full multipart upload lifecycle. The typical flow is:

```
initiate → batchGetSignedUrls → upload parts to GCS → complete
                                                     ↘ abort + deleteRecords (on failure)
```

---

#### `sdk.upload.initiate(data)`

Start a new multipart upload session. Returns the `uploadId` and `objectName` needed for all subsequent upload calls.

```typescript
const session = await sdk.upload.initiate({
  orgId: "org-id",
  projectId: "project-id",
  userId: "user-id",
  assetType: "UNREAL_PROJECT",
  assetFilename: "MyProject.zip",
  assetDisplayName: "My Project",
  unrealEngineVersion: "5.2.1",
  target: "Development",
  selfPackaged: true,
  volumeRegions: ["ORD1", "LGA1", "LAS1"],
  buildRegion: "ORD1",
});
```

**`InitiateUploadRequest` fields:**

| Field                      | Type                           | Required | Description                              |
| -------------------------- | ------------------------------ | -------- | ---------------------------------------- |
| `orgId`                    | `string`                       | Yes      | Organization ID.                         |
| `projectId`                | `string`                       | Yes      | Project ID.                              |
| `userId`                   | `string`                       | Yes      | Uploading user's ID.                     |
| `assetType`                | `AssetType`                    | Yes      | `"UNREAL_PROJECT"` or `"OTHER_3D"`.      |
| `assetFilename`            | `string`                       | Yes      | Original filename including extension.   |
| `assetDisplayName`         | `string`                       | No       | Human-readable display name.             |
| `unrealProjectId`          | `string`                       | No       | Existing Unreal project ID to attach to. |
| `unrealProjectDisplayName` | `string`                       | No       | Unreal project display name.             |
| `unrealEngineVersion`      | `SupportedUnrealEngineVersion` | No       | `"5.0.3"` or `"5.2.1"`.                  |
| `selfPackaged`             | `boolean`                      | No       | Whether the project was self-packaged.   |
| `target`                   | `UnrealProjectVersionTarget`   | No       | `"Development"` or `"Shipping"`.         |
| `other_3dId`               | `string`                       | No       | Existing Other3D ID to attach to.        |
| `other_3dDisplayName`      | `string`                       | No       | Other3D display name.                    |
| `buildRegion`              | `string`                       | No       | Preferred build region.                  |
| `volumeRegions`            | `string[]`                     | No       | Volume replication regions.              |

**Returns:** `Promise<InitiateUploadResponse>`

```typescript
{
  orgId: "org-id",
  projectId: "project-id",
  assetId: "generated-asset-id",
  assetVersionId: "generated-version-id",
  uploadId: "gcs-upload-id",
  objectName: "path/to/object.zip"
}
```

---

#### `sdk.upload.batchGetSignedUrls(orgId, projectId, assetId, assetVersionId, uploadId, objectName, partNumbers)`

Fetch signed GCS upload URLs for multiple parts in a single request. Always prefer this over `getSignedUrl` for initial URL fetching — it eliminates per-part backend round trips.

```typescript
const signedUrls = await sdk.upload.batchGetSignedUrls(
  "org-id",
  "project-id",
  "asset-id",
  "asset-version-id",
  "upload-id",
  "path/to/object.zip",
  [1, 2, 3, 4, 5],
);
// { 1: 'https://...', 2: 'https://...', ... }
```

**Returns:** `Promise<Record<number, string>>` — map of part number to signed URL.

---

#### `sdk.upload.getSignedUrl(orgId, projectId, assetId, assetVersionId, uploadId, objectName, partNumber)`

Fetch a signed URL for a single part. Use this when retrying a failed part — pre-fetched URLs may have expired.

```typescript
const url = await sdk.upload.getSignedUrl(
  "org-id",
  "project-id",
  "asset-id",
  "asset-version-id",
  "upload-id",
  "path/to/object.zip",
  3,
);
```

**Returns:** `Promise<string>` — the signed GCS URL.

---

#### `sdk.upload.complete(data)`

Complete the multipart upload. Signals GCS to assemble all uploaded parts into the final object.

```typescript
await sdk.upload.complete({
  orgId: "org-id",
  projectId: "project-id",
  assetId: "asset-id",
  assetType: "UNREAL_PROJECT",
  assetVersionId: "asset-version-id",
  uploadId: "upload-id",
  objectName: "path/to/object.zip",
  parts: [
    { partNumber: 1, etag: "abc123" },
    { partNumber: 2, etag: "def456" },
  ],
  sha256Sum: "optional-checksum",
});
```

**`CompleteUploadRequest` fields:**

| Field            | Type                                     | Required | Description                                      |
| ---------------- | ---------------------------------------- | -------- | ------------------------------------------------ |
| `orgId`          | `string`                                 | Yes      | Organization ID.                                 |
| `projectId`      | `string`                                 | Yes      | Project ID.                                      |
| `assetId`        | `string`                                 | Yes      | Asset ID from `initiate`.                        |
| `assetType`      | `AssetType`                              | Yes      | `"UNREAL_PROJECT"` or `"OTHER_3D"`.              |
| `assetVersionId` | `string`                                 | Yes      | Version ID from `initiate`.                      |
| `uploadId`       | `string`                                 | Yes      | Upload ID from `initiate`.                       |
| `objectName`     | `string`                                 | Yes      | Object name from `initiate`.                     |
| `parts`          | `{ partNumber: number; etag: string }[]` | Yes      | All uploaded parts with their ETags.             |
| `failed`         | `boolean`                                | No       | Mark the upload as failed instead of completing. |
| `sha256Sum`      | `string`                                 | No       | SHA-256 checksum of the complete file.           |

**Returns:** `Promise<AssetWithRelations>`

---

#### `sdk.upload.abort(data)`

Abort an in-progress multipart upload and clean up all parts from GCS.

```typescript
await sdk.upload.abort({
  orgId: "org-id",
  projectId: "project-id",
  assetId: "asset-id",
  assetType: "UNREAL_PROJECT",
  assetVersionId: "asset-version-id",
  uploadId: "upload-id",
  objectName: "path/to/object.zip",
});
```

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

---

#### `sdk.upload.deleteRecords(data)`

Delete all database records for an upload session. Use after `abort` to fully clean up.

```typescript
await sdk.upload.deleteRecords({
  orgId: "org-id",
  projectId: "project-id",
  assetId: "asset-id",
  assetType: "UNREAL_PROJECT",
  assetVersionId: "asset-version-id",
  uploadId: "upload-id",
  objectName: "path/to/object.zip",
});
```

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

---

#### `sdk.upload.getSession(filename)`

Check the backend for an existing in-progress upload session matching a filename. Returns `null` if none found. Use on file select to enable upload resumption without any client-side storage.

```typescript
const session = await sdk.upload.getSession("MyProject.zip");

if (session) {
  const uploadedParts = await sdk.upload.listParts(
    session.objectName,
    session.uploadId,
  );
}
```

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

```typescript
{
  orgId: "org-id",
  projectId: "project-id",
  assetId: "asset-id",
  assetVersionId: "asset-version-id",
  uploadId: "upload-id",
  objectName: "path/to/object.zip"
}
```

---

#### `sdk.upload.listParts(objectName, uploadId)`

List all parts already uploaded to GCS for a given upload session. Use after `getSession` to determine which parts still need to be uploaded.

```typescript
const parts = await sdk.upload.listParts("path/to/object.zip", "upload-id");
// [{ partNumber: 1, etag: 'abc123' }, { partNumber: 2, etag: 'def456' }]
```

**Returns:** `Promise<UploadPart[]>` — returns empty array if no parts found or on error.

---

### Sanitization

#### `sdk.sanitization.deleteProjectAndFootprint(assetId)`

Fully and irreversibly delete an asset and its entire footprint — GCS files, Kubernetes PVCs, and all database records. This is a destructive operation.

```typescript
const report = await sdk.sanitization.deleteProjectAndFootprint("asset-id");
```

**Returns:** `Promise<DeleteProjectReport>`

```typescript
{
  assetId: "asset-id",
  assetType: "UNREAL_PROJECT",
  orgId: "org-id",
  projectId: "project-id",
  fullyDeleted: true,
  steps: [
    { step: "GCS.unrealProject", success: true },
    { step: "K8s.PVC", success: true, skipped: false },
    { step: "DB.deleteAll", success: true }
  ]
}
```

If some steps fail, `fullyDeleted` will be `false` and the HTTP status will be `207 Partial Content`. Always check `steps` to see which resources could not be removed.

---

## Types Reference

All types are exported directly from the package root:

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

---

### Config

```typescript
type AssetManagerSDKConfig = {
  apiKey: string; // Required. API key issued after SDK purchase.
  timeout?: number; // Optional. Request timeout in ms. Default: 30000.
};
```

---

### Common

Shared enums and constants used across all modules.

```typescript
type AssetType = "UNREAL_PROJECT" | "OTHER_3D";

type UploadStatus = "INITIATED" | "UPLOADING" | "COMPLETED" | "FAILED";

type BuildStatus = "NOT_APPLICABLE" | "BUILDING" | "COMPLETED" | "FAILED";

type ValidationStatus = "PENDING" | "VALID" | "INVALID";

type SourceType = "UNIVERSAL_SCENE";

type SupportedUnrealEngineVersion = "5.0.3" | "5.2.1";

type UnrealProjectVersionTarget = "Development" | "Shipping";

type ProjectVersionState =
  | "new"
  | "odyssey_plugin_version_invalid"
  | "failed_missing_unreal_plugin_version"
  | "failed_missing_unreal_project"
  | "failed_missing_package_archive_url"
  | "failed_missing_package_archive_checksum"
  | "upload_complete"
  | "upload_invalid"
  | "upload_failed"
  | "upload_validating"
  | "builder_pod_creating"
  | "builder_pod_failed_to_create"
  | "builder_pod_timed_out_creating"
  | "builder_pod_waiting_for_ready"
  | "builder_pod_failed"
  | "builder_pod_ready"
  | "builder_downloading_project_version"
  | "builder_downloading_project_version_failed"
  | "builder_finding_project_file_failed"
  | "builder_copying_plugin_version"
  | "builder_copying_plugin_version_failed"
  | "builder_downloading_plugin_version"
  | "builder_downloading_plugin_version_failed"
  | "builder_validating"
  | "builder_validation_failed"
  | "builder_update_unreal_project_name"
  | "builder_settings_uploaded"
  | "builder_building"
  | "builder_failed"
  | "builder_retrying"
  | "builder_uploading"
  | "builder_upload_failed"
  | "builder_upload_complete"
  | "package_validator_required"
  | "package_validator_pod_creating"
  | "package_validator_pod_failed_to_create"
  | "package_validator_pod_waiting_for_ready"
  | "package_validator_pod_timed_out"
  | "package_validator_pod_ready"
  | "package_validator_failed"
  | "package_validator_retrying"
  | "package_validator_validating"
  | "package_validator_updating_unreal_project_name"
  | "package_validator_updating_project_path"
  | "package_validator_complete"
  | "volume_copy_pvcs_creating"
  | "volume_copy_pvcs_bound"
  | "volume_copy_pvcs_failed"
  | "volume_copy_pods_creating"
  | "volume_copy_pods_failed_to_create"
  | "volume_copy_pods_timed_out_creating"
  | "volume_copy_pods_waiting_for_ready"
  | "volume_copy_pods_failed"
  | "volume_copy_pods_ready"
  | "volume_copy_region_copying"
  | "volume_copy_region_failed"
  | "volume_copy_region_complete"
  | "volume_copy_failed"
  | "volume_copy_retrying"
  | "volume_copy_complete"
  | "volume_copy_expiring"
  | "volume_copy_expired"
  | "expiring"
  | "expired";
```

---

### Asset Types

```typescript
type AssetResponse = {
  id: string;
  name: string;
  orgId: string;
  projectId: string;
  assetType: AssetType;
  sourceType: SourceType;
  uploadStatus: UploadStatus;
  validationStatus: ValidationStatus;
  buildStatus: BuildStatus;
  storageBucket: string;
  storagePath: string;
  uploadId: string | null;
  objectName: string | null;
  downloadUrl: string | null;
  thumb: string | null;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
};

type UnrealProjectResponse = {
  assetId: string;
  orgId: string;
  projectId: string;
  displayName: string;
  unrealProjectVersion: string;
  unrealPluginVersion: string;
  versions?: UnrealProjectVersion[];
  createdAt: Date;
  updatedAt: Date;
};

type Other3dResponse = {
  assetId: string;
  orgId: string;
  projectId: string;
  displayName: string;
  unrealPluginVersion: string;
  createdAt: Date;
  updatedAt: Date;
};

type AssetWithRelations = AssetResponse & {
  unrealProjects?: UnrealProjectResponse[];
  other3d?: Other3dResponse[];
};

type CreateAssetInput = {
  name: string;
  orgId: string;
  projectId: string;
  assetType: AssetType;
  sourceType: SourceType;
  storageBucket: string;
  storagePath: string;
};

type UpdateUnrealProjectInput = {
  displayName?: string;
  unrealProjectVersion?: string;
  unrealPluginVersion?: string;
};

type UpdateOther3dInput = {
  displayName?: string;
  unrealPluginVersion?: string;
};

type UpdateAssetInput = {
  name?: string;
  uploadStatus?: UploadStatus;
  validationStatus?: ValidationStatus;
  buildStatus?: BuildStatus;
  storageBucket?: string;
  storagePath?: string;
  uploadId?: string;
  objectName?: string;
  downloadUrl?: string;
  thumb?: string;
  description?: string;
  unrealProject?: UpdateUnrealProjectInput;
  other3d?: UpdateOther3dInput;
};

type UpdateAssetStatusInput = {
  uploadStatus?: UploadStatus;
  validationStatus?: ValidationStatus;
  buildStatus?: BuildStatus;
};

type CreateAssetWithUnrealProjectInput = {
  assetData: CreateAssetInput;
  unrealProjectData: {
    displayName: string;
    unrealProjectVersion: string;
    unrealPluginVersion: string;
  };
};

type CreateAssetWithOther3dInput = {
  assetData: CreateAssetInput;
  other3dData: {
    displayName: string;
    unrealPluginVersion: string;
  };
};
```

---

### Version Types

```typescript
type UnrealProjectVersion = {
  id: string;
  orgId: string;
  projectId: string;
  authorUserId: string;
  appType: string;
  unrealProjectId: string;
  buildRegion: string | null;
  levelFilePath: string | null;
  levelName: string | null;
  name: string;
  packageArchiveSha256Sum: string;
  packageArchiveUrl: string;
  pluginVersionId: string;
  selfPackaged: boolean;
  state: ProjectVersionState;
  symbolsArchiveSha256Sum: string;
  symbolsArchiveUrl: string;
  bridgeToolkitFileSettings: Record<string, any>;
  stateChanges: Record<string, any>;
  target: UnrealProjectVersionTarget;
  uploader: string;
  uploadId: string | null;
  objectName: string | null;
  downloadUrl: string | null;
  uploadSha256Sum: string | null;
  unrealEngineVersion: SupportedUnrealEngineVersion | null;
  volumeCopyRegionsComplete: string[];
  volumeRegions: string[];
  volumeSizeGb: number;
  unrealProjectDirectoryPath: string | null;
  disableMultiplayer: boolean | null;
  lastPingFromBuilder: Date | null;
  lastPingFromVolumeCopyRegion: Date | null;
  createdAt: Date;
  updatedAt: Date;
};

// See CreateUnrealProjectVersionInput fields in the Versions API section above.
type UpdateUnrealProjectVersionInput = Record<string, any>;
```

---

### User Types

```typescript
type User = {
  id: string;
  createdAt: Date;
  updatedAt: Date;
};

type CreateUserInput = {
  id?: string;
};

type UpdateUserInput = {
  id?: string;
};
```

---

### Upload Types

```typescript
type InitiateUploadRequest = {
  orgId: string;
  projectId: string;
  userId: string;
  assetType: AssetType;
  assetFilename: string;
  assetDisplayName?: string;
  unrealProjectId?: string;
  unrealProjectDisplayName?: string;
  unrealEngineVersion?: SupportedUnrealEngineVersion;
  selfPackaged?: boolean;
  target?: UnrealProjectVersionTarget;
  other_3dId?: string;
  other_3dDisplayName?: string;
  buildRegion?: string;
  volumeRegions?: string[];
};

type InitiateUploadResponse = {
  orgId: string;
  projectId: string;
  assetId: string;
  assetVersionId: string;
  uploadId: string;
  objectName: string;
};

type CompleteUploadRequest = {
  orgId: string;
  projectId: string;
  assetId: string;
  assetType: AssetType;
  assetVersionId: string;
  uploadId: string;
  objectName: string;
  parts: { partNumber: number; etag: string }[];
  failed?: boolean;
  sha256Sum?: string;
};

type AbortUploadRequest = {
  orgId: string;
  projectId: string;
  assetId: string;
  assetType: AssetType;
  assetVersionId: string;
  uploadId: string;
  objectName: string;
};

type UploadSession = {
  orgId: string;
  projectId: string;
  assetId: string;
  assetVersionId: string;
  uploadId: string;
  objectName: string;
};

type UploadPart = {
  partNumber: number;
  etag: string;
};
```

---

### Health Types

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

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

---

### Sanitization Types

```typescript
type DeletionStepResult = {
  step: string;
  success: boolean;
  skipped?: boolean;
  error?: string;
};

type DeleteProjectReport = {
  assetId: string;
  assetType: "UNREAL_PROJECT" | "OTHER_3D";
  orgId: string;
  projectId: string;
  steps: DeletionStepResult[];
  fullyDeleted: boolean;
};
```

---

### Error Types

```typescript
interface OdysseyError extends Error {
  readonly code: string; // Machine-readable error code
  readonly name: string; // Human-readable error name
  readonly message: string; // Description of what went wrong
}

// Type guard — use in catch blocks to narrow the error type
function isOdysseyError(err: unknown): err is OdysseyError;
```

---

## Complete Upload Example

A full end-to-end multipart upload with session recovery and error handling:

```typescript
import {
  createAssetManagerSDK,
  isOdysseyError,
} from "@newgameplusinc/odyssey-asset-manager-sdk";

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

async function uploadAsset(
  file: File,
  orgId: string,
  projectId: string,
  userId: string,
) {
  const PART_SIZE = 50 * 1024 * 1024; // 50MB per part
  const totalParts = Math.ceil(file.size / PART_SIZE);

  let session;
  let uploadedParts: { partNumber: number; etag: string }[] = [];

  try {
    // 1. Check for an existing session (resume support)
    const existing = await sdk.upload.getSession(file.name);

    if (existing) {
      session = existing;
      uploadedParts = await sdk.upload.listParts(
        session.objectName,
        session.uploadId,
      );
      console.log(
        `Resuming upload — ${uploadedParts.length} parts already done`,
      );
    } else {
      // 2. Initiate a fresh upload
      session = await sdk.upload.initiate({
        orgId,
        projectId,
        userId,
        assetType: "UNREAL_PROJECT",
        assetFilename: file.name,
        unrealEngineVersion: "5.2.1",
        target: "Development",
        selfPackaged: true,
      });
    }

    const { assetId, assetVersionId, uploadId, objectName } = session;
    const uploadedPartNumbers = new Set(uploadedParts.map((p) => p.partNumber));
    const remainingParts = Array.from(
      { length: totalParts },
      (_, i) => i + 1,
    ).filter((n) => !uploadedPartNumbers.has(n));

    // 3. Fetch signed URLs for remaining parts in one request
    const signedUrls = await sdk.upload.batchGetSignedUrls(
      orgId,
      projectId,
      assetId,
      assetVersionId,
      uploadId,
      objectName,
      remainingParts,
    );

    // 4. Upload each remaining part
    for (const partNumber of remainingParts) {
      const start = (partNumber - 1) * PART_SIZE;
      const chunk = file.slice(start, start + PART_SIZE);

      const response = await fetch(signedUrls[partNumber], {
        method: "PUT",
        body: chunk,
      });

      const etag = response.headers.get("ETag")!;
      uploadedParts.push({ partNumber, etag });
    }

    // 5. Complete the upload
    await sdk.upload.complete({
      orgId,
      projectId,
      assetId,
      assetType: "UNREAL_PROJECT",
      assetVersionId,
      uploadId,
      objectName,
      parts: uploadedParts.sort((a, b) => a.partNumber - b.partNumber),
    });

    console.log("Upload complete:", assetId);
    return assetId;
  } catch (err) {
    if (isOdysseyError(err)) {
      console.error(`[${err.name}] ${err.message}`);

      // Clean up on upload-specific failures
      if (err.code === "UPLOAD_ERROR" && session) {
        await sdk.upload.abort({
          orgId,
          projectId,
          assetId: session.assetId,
          assetType: "UNREAL_PROJECT",
          assetVersionId: session.assetVersionId,
          uploadId: session.uploadId,
          objectName: session.objectName,
        });
        await sdk.upload.deleteRecords({
          orgId,
          projectId,
          assetId: session.assetId,
          assetType: "UNREAL_PROJECT",
          assetVersionId: session.assetVersionId,
          uploadId: session.uploadId,
          objectName: session.objectName,
        });
      }
    }
    throw err;
  }
}
```

---

## Versioning

This SDK follows semantic versioning:

| Bump              | When                                                     |
| ----------------- | -------------------------------------------------------- |
| `patch` — `x.x.1` | Bug fixes, no API changes.                               |
| `minor` — `x.1.0` | New methods or fields added, fully backwards compatible. |
| `major` — `2.0.0` | Breaking changes to existing method signatures or types. |

---

## License

Private — Odyssey Internal Use Only