@newgameplusinc/odyssey-asset-manager-sdk
v3.1.1
Published
Official SDK for the Odyssey Asset Manager backend
Readme
@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
- Quick Start
- Architecture
- Configuration
- Error Handling
- Debug Logging
- API Reference
- Types Reference
- Versioning
Installation
npm install @newgameplusinc/odyssey-asset-manager-sdk
# or
yarn add @newgameplusinc/odyssey-asset-manager-sdk
# or
pnpm add @newgameplusinc/odyssey-asset-manager-sdkQuick 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 → /deepSanitizationKey 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-keyon every request. - All errors are normalized into typed
OdysseyErrorobjects with acodeandnameso 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.jsIn 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 |
| ----------------- | -------------------------------------------------------- |
| 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
