npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@newgameplusinc/odyssey-asset-manager-sdk

v3.1.1

Published

Official SDK for the Odyssey Asset Manager backend

Readme

@newgameplusinc/odyssey-asset-manager-sdk

npm version npm downloads

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

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

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:

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

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.

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:

ODYSSEY_DEBUG=true node your-script.js

In the browser:

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.

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

Returns: Promise<HealthResponse>

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

sdk.health.getRedisHealth()

Check Redis connection health and measure latency.

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

Returns: Promise<RedisHealthResponse>

{
  status: "ok",
  latencyMs: 2
}

sdk.health.testRedis()

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

await sdk.health.testRedis();

Returns: Promise<void>


Assets

sdk.assets.create(data)

Create a new asset record.

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.

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

Returns: Promise<AssetWithRelations[]>


sdk.assets.getById(assetId)

Fetch a single asset by its ID.

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.

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.

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.

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.

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".

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

Returns: Promise<AssetWithRelations>


sdk.assets.markAsValidated(assetId)

Shorthand to set validationStatus to "VALID".

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.

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.

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.

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

Returns: Promise<void>


Versions

sdk.versions.create(data)

Create a new Unreal project version record.

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.

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

Returns: Promise<UnrealProjectVersion>


sdk.versions.getByAsset(assetId)

Fetch all versions associated with an asset.

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.

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.

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

Returns: Promise<void>


sdk.versions.deleteByAsset(assetId)

Delete all versions associated with an asset.

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

Returns: Promise<void>


Users

sdk.users.getAll()

Fetch all users.

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

Returns: Promise<User[]>


sdk.users.getById(userId)

Fetch a single user by ID.

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

Returns: Promise<User>


sdk.users.create(data)

Create a new user.

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.

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

Returns: Promise<User>


sdk.users.delete(userId)

Delete a user by ID.

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.

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>

{
  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.

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.

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.

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.

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.

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.

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

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

Returns: Promise<UploadSession | null>

{
  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.

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.

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

Returns: Promise<DeleteProjectReport>

{
  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:

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

Config

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.

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

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

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

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

type CreateUserInput = {
  id?: string;
};

type UpdateUserInput = {
  id?: string;
};

Upload Types

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

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

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

Sanitization Types

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

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:

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 | | ----------------- | -------------------------------------------------------- | | patchx.x.1 | Bug fixes, no API changes. | | minorx.1.0 | New methods or fields added, fully backwards compatible. | | major2.0.0 | Breaking changes to existing method signatures or types. |


License

Private — Odyssey Internal Use Only