@alliumcloud/asset-manager 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1540 @@
1
+ # @alliumcloud/asset-manager
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@alliumcloud/asset-manager)](https://www.npmjs.com/package/@alliumcloud/asset-manager)
4
+ [![npm downloads](https://img.shields.io/npm/dt/@alliumcloud/asset-manager)](https://www.npmjs.com/package/@alliumcloud/asset-manager)
5
+
6
+ Allium Asset Manager SDK - maintains 3D asset uploads (unreal or other 3D files). Manages extraction, build, validation, volume copy creation of unreal projects.
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Architecture](#architecture)
15
+ - [Configuration](#configuration)
16
+ - [Error Handling](#error-handling)
17
+ - [Debug Logging](#debug-logging)
18
+ - [API Reference](#api-reference)
19
+ - [Health](#health)
20
+ - [Assets](#assets)
21
+ - [Versions](#versions)
22
+ - [Users](#users)
23
+ - [Upload](#upload)
24
+ - [Sanitization](#sanitization)
25
+ - [Types Reference](#types-reference)
26
+ - [Config](#config)
27
+ - [Common](#common)
28
+ - [Asset Types](#asset-types)
29
+ - [Version Types](#version-types)
30
+ - [User Types](#user-types)
31
+ - [Upload Types](#upload-types)
32
+ - [Health Types](#health-types)
33
+ - [Sanitization Types](#sanitization-types)
34
+ - [Error Types](#error-types)
35
+ - [Versioning](#versioning)
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @alliumcloud/asset-manager
43
+ # or
44
+ yarn add @alliumcloud/asset-manager
45
+ # or
46
+ pnpm add @alliumcloud/asset-manager
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Quick Start
52
+
53
+ ```typescript
54
+ import { createAssetManagerSDK } from "@alliumcloud/asset-manager";
55
+
56
+ // Initialize once
57
+ const sdk = createAssetManagerSDK({
58
+ apiKey: "your-api-key",
59
+ jwksUri: "https://your-auth-domain/.well-known/jwks.json", // JWKS is optional here
60
+ accessTokenProvider: async () => await yourAuthService.getToken(),
61
+ });
62
+
63
+ // Check server health
64
+ const health = await sdk.health.getHealth();
65
+ console.log(health.status); // 'ok'
66
+
67
+ // Fetch all assets for an org
68
+ const assets = await sdk.assets.getByOrg("your-org-id");
69
+
70
+ // Start a multipart upload
71
+ const session = await sdk.upload.initiate({
72
+ orgId: "your-org-id",
73
+ projectId: "your-project-id",
74
+ userId: "your-user-id",
75
+ assetType: "UNREAL_PROJECT",
76
+ assetFilename: "MyProject.zip",
77
+ unrealEngineVersion: "5.2.1",
78
+ target: "Development",
79
+ selfPackaged: true,
80
+ });
81
+ ```
82
+
83
+ You can also use the namespace default export:
84
+
85
+ ```typescript
86
+ import AssetManagerSDK from "@alliumcloud/asset-manager";
87
+
88
+ const sdk = AssetManagerSDK.create({
89
+ apiKey: "your-api-key",
90
+ jwksUri: "https://your-auth-domain/.well-known/jwks.json", //optional
91
+ accessTokenProvider: async () => await yourAuthService.getToken(),
92
+ });
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Architecture
98
+
99
+ 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.
100
+
101
+ ```
102
+ createAssetManagerSDK({ apiKey, jwksUri, accessTokenProvider })
103
+
104
+ ├── sdk.health → /health
105
+ ├── sdk.assets → /assets
106
+ ├── sdk.versions → /unrealProjectVersion
107
+ ├── sdk.users → /users
108
+ ├── sdk.upload → /uploader
109
+ └── sdk.sanitization → /deepSanitization
110
+ ```
111
+
112
+ **Key design decisions:**
113
+
114
+ - The backend base URL is baked into the SDK at build time. You never pass a URL — it is not configurable by consumers.
115
+ - Your API key is passed once at initialization and is automatically injected as `x-sdk-key` on every request.
116
+ - All errors are normalized into typed `AlliumError` objects with a `code` and `name` so you can catch them reliably.
117
+ - Debug logging is fully opt-in and zero-cost in production.
118
+
119
+ ---
120
+
121
+ ## Configuration
122
+
123
+ ```typescript
124
+ import {
125
+ createAssetManagerSDK,
126
+ AssetManagerSDKConfig,
127
+ } from "@alliumcloud/asset-manager";
128
+
129
+ const sdk = createAssetManagerSDK({
130
+ apiKey: "your-api-key",
131
+ jwksUri: "https://your-auth-domain/.well-known/jwks.json",
132
+ accessTokenProvider: async () => await yourAuthService.getToken(),
133
+ timeout: 60_000,
134
+ });
135
+ ```
136
+
137
+ | Option | Type | Required | Default | Description |
138
+ | --------------------- | --------------------------------- | -------- | ------- | ------------------------------------------------------------------------------- |
139
+ | `apiKey` | `string` | Yes | — | API key issued after SDK purchase. Sent as `x-sdk-key` header on every request. |
140
+ | `jwksUri` | `string` | No | — | JWKS endpoint URI. Sent as `x-jwks-uri` on every request. |
141
+ | `accessTokenProvider` | `() => string \| Promise<string>` | Yes | — | Called before every request to retrieve a Bearer token. |
142
+ | `timeout` | `number` | No | `30000` | Request timeout in milliseconds. |
143
+
144
+ ---
145
+
146
+ ## Error Handling
147
+
148
+ All SDK errors are instances of `AlliumError`. You can use `isAlliumError()` to safely narrow the type in catch blocks.
149
+
150
+ ```typescript
151
+ import { isAlliumError } from "@alliumcloud/asset-manager";
152
+
153
+ try {
154
+ const asset = await sdk.assets.getById("some-id");
155
+ } catch (err) {
156
+ if (isAlliumError(err)) {
157
+ console.log(err.name); // e.g. 'AlliumAuthError'
158
+ console.log(err.code); // e.g. 'AUTH_ERROR'
159
+ console.log(err.message); // human-readable description
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Error Types
165
+
166
+ | `err.name` | `err.code` | When it is thrown |
167
+ | ----------------------- | ------------------ | ----------------------------------------------------------- |
168
+ | `AlliumConfigError` | `INVALID_CONFIG` | API key is missing or base URL not configured in the build. |
169
+ | `AlliumAuthError` | `AUTH_ERROR` | Server returns `401` or `403` — key is invalid or expired. |
170
+ | `AlliumValidationError` | `VALIDATION_ERROR` | Server returns `400` or `422` — bad input data. |
171
+ | `AlliumUploadError` | `UPLOAD_ERROR` | Any failure on `/uploader/*` routes. |
172
+ | `AlliumRequestError` | `REQUEST_ERROR` | Network failure, timeout, or any other `5xx` response. |
173
+ | `AlliumSDKError` | `SDK_ERROR` | Generic fallback for uncategorized errors. |
174
+
175
+ ---
176
+
177
+ ## Debug Logging
178
+
179
+ 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.
180
+
181
+ **In Node.js:**
182
+
183
+ ```bash
184
+ Allium_DEBUG=true node your-script.js
185
+ ```
186
+
187
+ **In the browser:**
188
+
189
+ ```javascript
190
+ window.Allium_DEBUG = true;
191
+ ```
192
+
193
+ All log lines are prefixed with a timestamp and category:
194
+
195
+ ```
196
+ [14:23:01.452][SDK:HTTP] → POST /uploader/initiate
197
+ [14:23:01.891][SDK:HTTP] ← 200 /uploader/initiate
198
+ [14:23:01.892][SDK:Upload] initiate { orgId: '...', assetId: '...' }
199
+ ```
200
+
201
+ ---
202
+
203
+ ## API Reference
204
+
205
+ ### Health
206
+
207
+ #### `sdk.health.getHealth()`
208
+
209
+ Check if the backend service is running and healthy.
210
+
211
+ ```typescript
212
+ const health = await sdk.health.getHealth();
213
+ ```
214
+
215
+ **Returns:** `Promise<HealthResponse>`
216
+
217
+ ```typescript
218
+ {
219
+ status: "ok",
220
+ timestamp: "2024-01-01T00:00:00.000Z"
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ #### `sdk.health.getRedisHealth()`
227
+
228
+ Check Redis connection health and measure latency.
229
+
230
+ ```typescript
231
+ const redis = await sdk.health.getRedisHealth();
232
+ ```
233
+
234
+ **Returns:** `Promise<RedisHealthResponse>`
235
+
236
+ ```typescript
237
+ {
238
+ status: "ok",
239
+ latencyMs: 2
240
+ }
241
+ ```
242
+
243
+ ---
244
+
245
+ #### `sdk.health.testRedis()`
246
+
247
+ Run a test write/read cycle on Redis. Useful for smoke-testing after deployment.
248
+
249
+ ```typescript
250
+ await sdk.health.testRedis();
251
+ ```
252
+
253
+ **Returns:** `Promise<void>`
254
+
255
+ ---
256
+
257
+ ### Assets
258
+
259
+ #### `sdk.assets.create(data)`
260
+
261
+ Create a new asset record.
262
+
263
+ ```typescript
264
+ const asset = await sdk.assets.create({
265
+ name: "My Project",
266
+ orgId: "org-id",
267
+ projectId: "project-id",
268
+ assetType: "UNREAL_PROJECT",
269
+ sourceType: "UNIVERSAL_SCENE",
270
+ storageBucket: "my-bucket",
271
+ storagePath: "path/to/asset",
272
+ });
273
+ ```
274
+
275
+ | Field | Type | Required | Description |
276
+ | --------------- | ------------ | -------- | ----------------------------------- |
277
+ | `name` | `string` | Yes | Display name of the asset. |
278
+ | `orgId` | `string` | Yes | Organization ID (UUID). |
279
+ | `projectId` | `string` | Yes | Project ID. |
280
+ | `assetType` | `AssetType` | Yes | `"UNREAL_PROJECT"` or `"OTHER_3D"`. |
281
+ | `sourceType` | `SourceType` | Yes | `"UNIVERSAL_SCENE"`. |
282
+ | `storageBucket` | `string` | Yes | GCS bucket name. |
283
+ | `storagePath` | `string` | Yes | GCS object path. |
284
+
285
+ **Returns:** `Promise<AssetWithRelations>`
286
+
287
+ ---
288
+
289
+ #### `sdk.assets.getAll()`
290
+
291
+ Fetch all assets across all organizations.
292
+
293
+ ```typescript
294
+ const assets = await sdk.assets.getAll();
295
+ ```
296
+
297
+ **Returns:** `Promise<AssetWithRelations[]>`
298
+
299
+ ---
300
+
301
+ #### `sdk.assets.getById(assetId)`
302
+
303
+ Fetch a single asset by its ID.
304
+
305
+ ```typescript
306
+ const asset = await sdk.assets.getById("asset-id");
307
+ ```
308
+
309
+ | Parameter | Type | Description |
310
+ | --------- | -------- | ---------------------- |
311
+ | `assetId` | `string` | The asset's unique ID. |
312
+
313
+ **Returns:** `Promise<AssetWithRelations>`
314
+
315
+ ---
316
+
317
+ #### `sdk.assets.getByOrg(orgId)`
318
+
319
+ Fetch all assets belonging to a specific organization.
320
+
321
+ ```typescript
322
+ const assets = await sdk.assets.getByOrg("org-id");
323
+ ```
324
+
325
+ | Parameter | Type | Description |
326
+ | --------- | -------- | ----------------------------- |
327
+ | `orgId` | `string` | The organization's unique ID. |
328
+
329
+ **Returns:** `Promise<AssetWithRelations[]>`
330
+
331
+ ---
332
+
333
+ #### `sdk.assets.getByProject(projectId)`
334
+
335
+ Fetch all assets belonging to a specific project.
336
+
337
+ ```typescript
338
+ const assets = await sdk.assets.getByProject("project-id");
339
+ ```
340
+
341
+ | Parameter | Type | Description |
342
+ | ----------- | -------- | ------------------------ |
343
+ | `projectId` | `string` | The project's unique ID. |
344
+
345
+ **Returns:** `Promise<AssetWithRelations[]>`
346
+
347
+ ---
348
+
349
+ #### `sdk.assets.update(assetId, data)`
350
+
351
+ Update one or more fields on an asset.
352
+
353
+ ```typescript
354
+ const updated = await sdk.assets.update("asset-id", {
355
+ name: "New Asset Name",
356
+ buildStatus: "COMPLETED",
357
+ uploadId: "gcs-upload-id",
358
+ objectName: "path/to/object.zip",
359
+ unrealProject: {
360
+ displayName: "Updated Display Name",
361
+ unrealProjectVersion: "1.0.1",
362
+ },
363
+ });
364
+ ```
365
+
366
+ | Parameter | Type | Description |
367
+ | --------- | ------------------ | ------------------------------------------ |
368
+ | `assetId` | `string` | The asset's unique ID. |
369
+ | `data` | `UpdateAssetInput` | Fields to update. All fields are optional. |
370
+
371
+ **`UpdateAssetInput` fields:**
372
+
373
+ | Field | Type | Description |
374
+ | ------------------ | -------------------------- | ----------------------------------------------------------------- |
375
+ | `name` | `string` | New display name. |
376
+ | `uploadStatus` | `UploadStatus` | `"INITIATED"` \| `"UPLOADING"` \| `"COMPLETED"` \| `"FAILED"` |
377
+ | `validationStatus` | `ValidationStatus` | `"PENDING"` \| `"VALID"` \| `"INVALID"` |
378
+ | `buildStatus` | `BuildStatus` | `"NOT_APPLICABLE"` \| `"BUILDING"` \| `"COMPLETED"` \| `"FAILED"` |
379
+ | `uploadId` | `string` | GCS multipart upload ID. |
380
+ | `objectName` | `string` | GCS object name. |
381
+ | `downloadUrl` | `string` | Public download URL. |
382
+ | `unrealProject` | `UpdateUnrealProjectInput` | Nested Unreal project fields to update. |
383
+ | `other3d` | `UpdateOther3dInput` | Nested Other3D fields to update. |
384
+ | `thumb` | `string` | Thumbnail URL. |
385
+ | `description` | `string` | Asset description. |
386
+
387
+ **Returns:** `Promise<AssetWithRelations>`
388
+
389
+ ---
390
+
391
+ #### `sdk.assets.updateStatus(assetId, data)`
392
+
393
+ Update only the status fields of an asset.
394
+
395
+ ```typescript
396
+ const updated = await sdk.assets.updateStatus("asset-id", {
397
+ uploadStatus: "COMPLETED",
398
+ validationStatus: "VALID",
399
+ buildStatus: "BUILDING",
400
+ });
401
+ ```
402
+
403
+ | Parameter | Type | Description |
404
+ | --------- | ------------------------ | ------------------------------------------------- |
405
+ | `assetId` | `string` | The asset's unique ID. |
406
+ | `data` | `UpdateAssetStatusInput` | Status fields to update. All fields are optional. |
407
+
408
+ **`UpdateAssetStatusInput` fields:**
409
+
410
+ | Field | Type | Description |
411
+ | ------------------ | ------------------ | ----------------------------------------------------------------- |
412
+ | `uploadStatus` | `UploadStatus` | `"INITIATED"` \| `"UPLOADING"` \| `"COMPLETED"` \| `"FAILED"` |
413
+ | `validationStatus` | `ValidationStatus` | `"PENDING"` \| `"VALID"` \| `"INVALID"` |
414
+ | `buildStatus` | `BuildStatus` | `"NOT_APPLICABLE"` \| `"BUILDING"` \| `"COMPLETED"` \| `"FAILED"` |
415
+
416
+ **Returns:** `Promise<AssetWithRelations>`
417
+
418
+ ---
419
+
420
+ #### `sdk.assets.markAsUploaded(assetId)`
421
+
422
+ Shorthand to set `uploadStatus` to `"COMPLETED"`.
423
+
424
+ ```typescript
425
+ await sdk.assets.markAsUploaded("asset-id");
426
+ ```
427
+
428
+ **Returns:** `Promise<AssetWithRelations>`
429
+
430
+ ---
431
+
432
+ #### `sdk.assets.markAsValidated(assetId)`
433
+
434
+ Shorthand to set `validationStatus` to `"VALID"`.
435
+
436
+ ```typescript
437
+ await sdk.assets.markAsValidated("asset-id");
438
+ ```
439
+
440
+ **Returns:** `Promise<AssetWithRelations>`
441
+
442
+ ---
443
+
444
+ #### `sdk.assets.markAsFailed(assetId, stage)`
445
+
446
+ Shorthand to mark an asset as failed at a specific pipeline stage.
447
+
448
+ ```typescript
449
+ await sdk.assets.markAsFailed("asset-id", "upload");
450
+ ```
451
+
452
+ | Parameter | Type | Description |
453
+ | --------- | ------------------------------------- | ------------------------------------ |
454
+ | `assetId` | `string` | The asset's unique ID. |
455
+ | `stage` | `"upload" \| "validation" \| "build"` | The stage at which the asset failed. |
456
+
457
+ **Returns:** `Promise<AssetWithRelations>`
458
+
459
+ ---
460
+
461
+ #### `sdk.assets.uploadThumb(assetId, userId, file)`
462
+
463
+ Upload a thumbnail image for an asset.
464
+
465
+ ```typescript
466
+ const updated = await sdk.assets.uploadThumb(
467
+ "asset-id",
468
+ "user-id",
469
+ file, // File | Blob
470
+ );
471
+ ```
472
+
473
+ | Parameter | Type | Description |
474
+ | --------- | -------------- | --------------------------------------- |
475
+ | `assetId` | `string` | The asset's unique ID. |
476
+ | `userId` | `string` | ID of the user uploading the thumbnail. |
477
+ | `file` | `File \| Blob` | The thumbnail image file. |
478
+
479
+ **Returns:** `Promise<AssetWithRelations>`
480
+
481
+ ---
482
+
483
+ #### `sdk.assets.delete(assetId)`
484
+
485
+ Permanently delete an asset record. For full deletion including GCS files and database footprint, use `sdk.sanitization.deleteProjectAndFootprint()` instead.
486
+
487
+ ```typescript
488
+ await sdk.assets.delete("asset-id");
489
+ ```
490
+
491
+ **Returns:** `Promise<void>`
492
+
493
+ ---
494
+
495
+ ### Versions
496
+
497
+ #### `sdk.versions.create(data)`
498
+
499
+ Create a new Unreal project version record.
500
+
501
+ ```typescript
502
+ const version = await sdk.versions.create({
503
+ orgId: "org-id",
504
+ projectId: "project-id",
505
+ authorUserId: "user-id",
506
+ appType: "unreal",
507
+ unrealProjectId: "unreal-project-id",
508
+ name: "v1.0.0",
509
+ pluginVersionId: "plugin-version-id",
510
+ selfPackaged: true,
511
+ state: "new",
512
+ target: "Development",
513
+ uploader: "uploader-id",
514
+ packageArchiveSha256Sum: "abc123",
515
+ packageArchiveUrl: "https://...",
516
+ symbolsArchiveSha256Sum: "def456",
517
+ symbolsArchiveUrl: "https://...",
518
+ bridgeToolkitFileSettings: {},
519
+ stateChanges: {},
520
+ volumeCopyRegionsComplete: [],
521
+ volumeRegions: ["ORD1"],
522
+ volumeSizeGb: 10,
523
+ unrealEngineVersion: "5.2.1",
524
+ });
525
+ ```
526
+
527
+ **Required fields:**
528
+
529
+ | Field | Type | Description |
530
+ | --------------------------- | ---------------------------- | ------------------------------------------ |
531
+ | `orgId` | `string` | Organization ID (UUID). |
532
+ | `projectId` | `string` | Project ID. |
533
+ | `authorUserId` | `string` | ID of the user creating the version. |
534
+ | `appType` | `string` | Application type identifier. |
535
+ | `unrealProjectId` | `string` | Unreal project ID (CUID). |
536
+ | `name` | `string` | Version name. |
537
+ | `pluginVersionId` | `string` | Plugin version ID. |
538
+ | `selfPackaged` | `boolean` | Whether the project was self-packaged. |
539
+ | `state` | `ProjectVersionState` | Initial lifecycle state. See states below. |
540
+ | `target` | `UnrealProjectVersionTarget` | `"Development"` or `"Shipping"`. |
541
+ | `uploader` | `string` | Uploader identifier. |
542
+ | `packageArchiveSha256Sum` | `string` | SHA-256 checksum of the package archive. |
543
+ | `packageArchiveUrl` | `string` | URL of the package archive. |
544
+ | `symbolsArchiveSha256Sum` | `string` | SHA-256 checksum of the symbols archive. |
545
+ | `symbolsArchiveUrl` | `string` | URL of the symbols archive. |
546
+ | `bridgeToolkitFileSettings` | `Record<string, any>` | Bridge toolkit file settings map. |
547
+ | `stateChanges` | `Record<string, any>` | State change history map. |
548
+ | `volumeCopyRegionsComplete` | `string[]` | Regions where volume copy is complete. |
549
+ | `volumeRegions` | `string[]` | Target volume regions. |
550
+ | `volumeSizeGb` | `number` | Volume size in GB. |
551
+
552
+ **Optional fields:**
553
+
554
+ | Field | Type | Description |
555
+ | ------------------------------ | ------------------------------ | -------------------------------------- |
556
+ | `buildRegion` | `string` | Preferred build region (max 10 chars). |
557
+ | `unrealEngineVersion` | `SupportedUnrealEngineVersion` | `"5.0.3"` or `"5.2.1"`. |
558
+ | `uploadId` | `string` | GCS upload ID. |
559
+ | `objectName` | `string` | GCS object name. |
560
+ | `downloadUrl` | `string` | Public download URL. |
561
+ | `uploadSha256Sum` | `string` | SHA-256 checksum of the upload. |
562
+ | `unrealProjectDirectoryPath` | `string` | Path to the Unreal project directory. |
563
+ | `disableMultiplayer` | `boolean` | Whether to disable multiplayer. |
564
+ | `lastPingFromBuilder` | `Date` | Last ping timestamp from builder. |
565
+ | `lastPingFromVolumeCopyRegion` | `Date` | Last ping from volume copy region. |
566
+ | `levelFilePath` | `string` | Path to the level file. |
567
+ | `levelName` | `string` | Unreal level name. |
568
+
569
+ **Returns:** `Promise<UnrealProjectVersion>`
570
+
571
+ ---
572
+
573
+ #### `sdk.versions.getById(versionId)`
574
+
575
+ Fetch a single Unreal project version by ID.
576
+
577
+ ```typescript
578
+ const version = await sdk.versions.getById("version-id");
579
+ ```
580
+
581
+ **Returns:** `Promise<UnrealProjectVersion>`
582
+
583
+ ---
584
+
585
+ #### `sdk.versions.getByAsset(assetId)`
586
+
587
+ Fetch all versions associated with an asset.
588
+
589
+ ```typescript
590
+ const versions = await sdk.versions.getByAsset("asset-id");
591
+ ```
592
+
593
+ **Returns:** `Promise<UnrealProjectVersion[]>`
594
+
595
+ ---
596
+
597
+ #### `sdk.versions.update(versionId, data)`
598
+
599
+ Update any fields on a project version. Accepts any key-value pairs — all fields are optional.
600
+
601
+ ```typescript
602
+ await sdk.versions.update("version-id", {
603
+ state: "upload_complete",
604
+ target: "Shipping",
605
+ unrealEngineVersion: "5.2.1",
606
+ levelName: "MyLevel",
607
+ });
608
+ ```
609
+
610
+ | Parameter | Type | Description |
611
+ | ----------- | --------------------- | ------------------------ |
612
+ | `versionId` | `string` | The version's unique ID. |
613
+ | `data` | `Record<string, any>` | Fields to update. |
614
+
615
+ **Returns:** `Promise<UnrealProjectVersion>`
616
+
617
+ ---
618
+
619
+ #### `sdk.versions.delete(versionId)`
620
+
621
+ Delete a single project version by ID.
622
+
623
+ ```typescript
624
+ await sdk.versions.delete("version-id");
625
+ ```
626
+
627
+ **Returns:** `Promise<void>`
628
+
629
+ ---
630
+
631
+ #### `sdk.versions.deleteByAsset(assetId)`
632
+
633
+ Delete all versions associated with an asset.
634
+
635
+ ```typescript
636
+ await sdk.versions.deleteByAsset("asset-id");
637
+ ```
638
+
639
+ **Returns:** `Promise<void>`
640
+
641
+ ---
642
+
643
+ ### Users
644
+
645
+ #### `sdk.users.getAll()`
646
+
647
+ Fetch all users.
648
+
649
+ ```typescript
650
+ const users = await sdk.users.getAll();
651
+ ```
652
+
653
+ **Returns:** `Promise<User[]>`
654
+
655
+ ---
656
+
657
+ #### `sdk.users.getById(userId)`
658
+
659
+ Fetch a single user by ID.
660
+
661
+ ```typescript
662
+ const user = await sdk.users.getById("user-id");
663
+ ```
664
+
665
+ **Returns:** `Promise<User>`
666
+
667
+ ---
668
+
669
+ #### `sdk.users.create(data)`
670
+
671
+ Create a new user.
672
+
673
+ ```typescript
674
+ const user = await sdk.users.create({ id: "custom-user-id" });
675
+ ```
676
+
677
+ | Field | Type | Required | Description |
678
+ | ----- | -------- | -------- | --------------------------------------------------- |
679
+ | `id` | `string` | No | Optional custom ID. Auto-generated if not provided. |
680
+
681
+ **Returns:** `Promise<User>`
682
+
683
+ ---
684
+
685
+ #### `sdk.users.update(userId, data)`
686
+
687
+ Update a user.
688
+
689
+ ```typescript
690
+ const user = await sdk.users.update("user-id", { id: "new-id" });
691
+ ```
692
+
693
+ **Returns:** `Promise<User>`
694
+
695
+ ---
696
+
697
+ #### `sdk.users.delete(userId)`
698
+
699
+ Delete a user by ID.
700
+
701
+ ```typescript
702
+ await sdk.users.delete("user-id");
703
+ ```
704
+
705
+ **Returns:** `Promise<void>`
706
+
707
+ ---
708
+
709
+ ### Upload
710
+
711
+ The upload module manages the full multipart upload lifecycle. The typical flow is:
712
+
713
+ ```
714
+ initiate → batchGetSignedUrls → upload parts to GCS → complete
715
+ ↘ abort + deleteRecords (on failure)
716
+ ```
717
+
718
+ ---
719
+
720
+ #### `sdk.upload.initiate(data)`
721
+
722
+ Start a new multipart upload session. Returns the `uploadId` and `objectName` needed for all subsequent upload calls.
723
+
724
+ ```typescript
725
+ const session = await sdk.upload.initiate({
726
+ orgId: "org-id",
727
+ projectId: "project-id",
728
+ userId: "user-id",
729
+ assetType: "UNREAL_PROJECT",
730
+ assetFilename: "MyProject.zip",
731
+ assetDisplayName: "My Project",
732
+ unrealEngineVersion: "5.2.1",
733
+ target: "Development",
734
+ selfPackaged: true,
735
+ volumeRegions: ["ORD1", "LGA1", "LAS1"],
736
+ buildRegion: "ORD1",
737
+ });
738
+ ```
739
+
740
+ **`InitiateUploadRequest` fields:**
741
+
742
+ | Field | Type | Required | Description |
743
+ | -------------------------- | ------------------------------ | -------- | ---------------------------------------- |
744
+ | `orgId` | `string` | Yes | Organization ID. |
745
+ | `projectId` | `string` | Yes | Project ID. |
746
+ | `userId` | `string` | Yes | Uploading user's ID. |
747
+ | `assetType` | `AssetType` | Yes | `"UNREAL_PROJECT"` or `"OTHER_3D"`. |
748
+ | `assetFilename` | `string` | Yes | Original filename including extension. |
749
+ | `assetDisplayName` | `string` | No | Human-readable display name. |
750
+ | `unrealProjectId` | `string` | No | Existing Unreal project ID to attach to. |
751
+ | `unrealProjectDisplayName` | `string` | No | Unreal project display name. |
752
+ | `unrealEngineVersion` | `SupportedUnrealEngineVersion` | No | `"5.0.3"` or `"5.2.1"`. |
753
+ | `selfPackaged` | `boolean` | No | Whether the project was self-packaged. |
754
+ | `target` | `UnrealProjectVersionTarget` | No | `"Development"` or `"Shipping"`. |
755
+ | `other_3dId` | `string` | No | Existing Other3D ID to attach to. |
756
+ | `other_3dDisplayName` | `string` | No | Other3D display name. |
757
+ | `buildRegion` | `string` | No | Preferred build region. |
758
+ | `volumeRegions` | `string[]` | No | Volume replication regions. |
759
+
760
+ **Returns:** `Promise<InitiateUploadResponse>`
761
+
762
+ ```typescript
763
+ {
764
+ orgId: "org-id",
765
+ projectId: "project-id",
766
+ assetId: "generated-asset-id",
767
+ assetVersionId: "generated-version-id",
768
+ uploadId: "gcs-upload-id",
769
+ objectName: "path/to/object.zip"
770
+ }
771
+ ```
772
+
773
+ ---
774
+
775
+ #### `sdk.upload.batchGetSignedUrls(orgId, projectId, assetId, assetVersionId, uploadId, objectName, partNumbers)`
776
+
777
+ 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.
778
+
779
+ ```typescript
780
+ const signedUrls = await sdk.upload.batchGetSignedUrls(
781
+ "org-id",
782
+ "project-id",
783
+ "asset-id",
784
+ "asset-version-id",
785
+ "upload-id",
786
+ "path/to/object.zip",
787
+ [1, 2, 3, 4, 5],
788
+ );
789
+ // { 1: 'https://...', 2: 'https://...', ... }
790
+ ```
791
+
792
+ **Returns:** `Promise<Record<number, string>>` — map of part number to signed URL.
793
+
794
+ ---
795
+
796
+ #### `sdk.upload.getSignedUrl(orgId, projectId, assetId, assetVersionId, uploadId, objectName, partNumber)`
797
+
798
+ Fetch a signed URL for a single part. Use this when retrying a failed part — pre-fetched URLs may have expired.
799
+
800
+ ```typescript
801
+ const url = await sdk.upload.getSignedUrl(
802
+ "org-id",
803
+ "project-id",
804
+ "asset-id",
805
+ "asset-version-id",
806
+ "upload-id",
807
+ "path/to/object.zip",
808
+ 3,
809
+ );
810
+ ```
811
+
812
+ **Returns:** `Promise<string>` — the signed GCS URL.
813
+
814
+ ---
815
+
816
+ #### `sdk.upload.complete(data)`
817
+
818
+ Complete the multipart upload. Signals GCS to assemble all uploaded parts into the final object.
819
+
820
+ ```typescript
821
+ await sdk.upload.complete({
822
+ orgId: "org-id",
823
+ projectId: "project-id",
824
+ assetId: "asset-id",
825
+ assetType: "UNREAL_PROJECT",
826
+ assetVersionId: "asset-version-id",
827
+ uploadId: "upload-id",
828
+ objectName: "path/to/object.zip",
829
+ parts: [
830
+ { partNumber: 1, etag: "abc123" },
831
+ { partNumber: 2, etag: "def456" },
832
+ ],
833
+ sha256Sum: "optional-checksum",
834
+ });
835
+ ```
836
+
837
+ **`CompleteUploadRequest` fields:**
838
+
839
+ | Field | Type | Required | Description |
840
+ | ---------------- | ---------------------------------------- | -------- | ------------------------------------------------ |
841
+ | `orgId` | `string` | Yes | Organization ID. |
842
+ | `projectId` | `string` | Yes | Project ID. |
843
+ | `assetId` | `string` | Yes | Asset ID from `initiate`. |
844
+ | `assetType` | `AssetType` | Yes | `"UNREAL_PROJECT"` or `"OTHER_3D"`. |
845
+ | `assetVersionId` | `string` | Yes | Version ID from `initiate`. |
846
+ | `uploadId` | `string` | Yes | Upload ID from `initiate`. |
847
+ | `objectName` | `string` | Yes | Object name from `initiate`. |
848
+ | `parts` | `{ partNumber: number; etag: string }[]` | Yes | All uploaded parts with their ETags. |
849
+ | `failed` | `boolean` | No | Mark the upload as failed instead of completing. |
850
+ | `sha256Sum` | `string` | No | SHA-256 checksum of the complete file. |
851
+
852
+ **Returns:** `Promise<AssetWithRelations>`
853
+
854
+ ---
855
+
856
+ #### `sdk.upload.abort(data)`
857
+
858
+ Abort an in-progress multipart upload and clean up all parts from GCS.
859
+
860
+ ```typescript
861
+ await sdk.upload.abort({
862
+ orgId: "org-id",
863
+ projectId: "project-id",
864
+ assetId: "asset-id",
865
+ assetType: "UNREAL_PROJECT",
866
+ assetVersionId: "asset-version-id",
867
+ uploadId: "upload-id",
868
+ objectName: "path/to/object.zip",
869
+ });
870
+ ```
871
+
872
+ **Returns:** `Promise<void>`
873
+
874
+ ---
875
+
876
+ #### `sdk.upload.deleteRecords(data)`
877
+
878
+ Delete all database records for an upload session. Use after `abort` to fully clean up.
879
+
880
+ ```typescript
881
+ await sdk.upload.deleteRecords({
882
+ orgId: "org-id",
883
+ projectId: "project-id",
884
+ assetId: "asset-id",
885
+ assetType: "UNREAL_PROJECT",
886
+ assetVersionId: "asset-version-id",
887
+ uploadId: "upload-id",
888
+ objectName: "path/to/object.zip",
889
+ });
890
+ ```
891
+
892
+ **Returns:** `Promise<void>`
893
+
894
+ ---
895
+
896
+ #### `sdk.upload.getSession(filename)`
897
+
898
+ 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.
899
+
900
+ ```typescript
901
+ const session = await sdk.upload.getSession("MyProject.zip");
902
+
903
+ if (session) {
904
+ const uploadedParts = await sdk.upload.listParts(
905
+ session.objectName,
906
+ session.uploadId,
907
+ );
908
+ }
909
+ ```
910
+
911
+ **Returns:** `Promise<UploadSession | null>`
912
+
913
+ ```typescript
914
+ {
915
+ orgId: "org-id",
916
+ projectId: "project-id",
917
+ assetId: "asset-id",
918
+ assetVersionId: "asset-version-id",
919
+ uploadId: "upload-id",
920
+ objectName: "path/to/object.zip"
921
+ }
922
+ ```
923
+
924
+ ---
925
+
926
+ #### `sdk.upload.listParts(objectName, uploadId)`
927
+
928
+ List all parts already uploaded to GCS for a given upload session. Use after `getSession` to determine which parts still need to be uploaded.
929
+
930
+ ```typescript
931
+ const parts = await sdk.upload.listParts("path/to/object.zip", "upload-id");
932
+ // [{ partNumber: 1, etag: 'abc123' }, { partNumber: 2, etag: 'def456' }]
933
+ ```
934
+
935
+ **Returns:** `Promise<UploadPart[]>` — returns empty array if no parts found or on error.
936
+
937
+ ---
938
+
939
+ ### Sanitization
940
+
941
+ #### `sdk.sanitization.deleteProjectAndFootprint(assetId)`
942
+
943
+ Fully and irreversibly delete an asset and its entire footprint — GCS files, Kubernetes PVCs, and all database records. This is a destructive operation.
944
+
945
+ ```typescript
946
+ const report = await sdk.sanitization.deleteProjectAndFootprint("asset-id");
947
+ ```
948
+
949
+ **Returns:** `Promise<DeleteProjectReport>`
950
+
951
+ ```typescript
952
+ {
953
+ assetId: "asset-id",
954
+ assetType: "UNREAL_PROJECT",
955
+ orgId: "org-id",
956
+ projectId: "project-id",
957
+ fullyDeleted: true,
958
+ steps: [
959
+ { step: "GCS.unrealProject", success: true },
960
+ { step: "K8s.PVC", success: true, skipped: false },
961
+ { step: "DB.deleteAll", success: true }
962
+ ]
963
+ }
964
+ ```
965
+
966
+ 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.
967
+
968
+ ---
969
+
970
+ ## Types Reference
971
+
972
+ All types are exported directly from the package root:
973
+
974
+ ```typescript
975
+ import type { ... } from "@alliumcloud/odyssey-asset-manager-sdk";
976
+ ```
977
+
978
+ ---
979
+
980
+ ### Config
981
+
982
+ ```typescript
983
+ type AssetManagerSDKConfig = {
984
+ apiKey: string;
985
+ jwksUri: string;
986
+ accessTokenProvider: () => string | Promise<string>;
987
+ timeout?: number;
988
+ };
989
+ ```
990
+
991
+ ---
992
+
993
+ ### Common
994
+
995
+ Shared enums and constants used across all modules.
996
+
997
+ ```typescript
998
+ type AssetType = "UNREAL_PROJECT" | "OTHER_3D";
999
+
1000
+ type UploadStatus = "INITIATED" | "UPLOADING" | "COMPLETED" | "FAILED";
1001
+
1002
+ type BuildStatus = "NOT_APPLICABLE" | "BUILDING" | "COMPLETED" | "FAILED";
1003
+
1004
+ type ValidationStatus = "PENDING" | "VALID" | "INVALID";
1005
+
1006
+ type SourceType = "UNIVERSAL_SCENE";
1007
+
1008
+ type SupportedUnrealEngineVersion = "5.0.3" | "5.2.1";
1009
+
1010
+ type UnrealProjectVersionTarget = "Development" | "Shipping";
1011
+
1012
+ type ProjectVersionState =
1013
+ | "new"
1014
+ | "odyssey_plugin_version_invalid"
1015
+ | "failed_missing_unreal_plugin_version"
1016
+ | "failed_missing_unreal_project"
1017
+ | "failed_missing_package_archive_url"
1018
+ | "failed_missing_package_archive_checksum"
1019
+ | "upload_complete"
1020
+ | "upload_invalid"
1021
+ | "upload_failed"
1022
+ | "upload_validating"
1023
+ | "builder_pod_creating"
1024
+ | "builder_pod_failed_to_create"
1025
+ | "builder_pod_timed_out_creating"
1026
+ | "builder_pod_waiting_for_ready"
1027
+ | "builder_pod_failed"
1028
+ | "builder_pod_ready"
1029
+ | "builder_downloading_project_version"
1030
+ | "builder_downloading_project_version_failed"
1031
+ | "builder_finding_project_file_failed"
1032
+ | "builder_copying_plugin_version"
1033
+ | "builder_copying_plugin_version_failed"
1034
+ | "builder_downloading_plugin_version"
1035
+ | "builder_downloading_plugin_version_failed"
1036
+ | "builder_validating"
1037
+ | "builder_validation_failed"
1038
+ | "builder_update_unreal_project_name"
1039
+ | "builder_settings_uploaded"
1040
+ | "builder_building"
1041
+ | "builder_failed"
1042
+ | "builder_retrying"
1043
+ | "builder_uploading"
1044
+ | "builder_upload_failed"
1045
+ | "builder_upload_complete"
1046
+ | "package_validator_required"
1047
+ | "package_validator_pod_creating"
1048
+ | "package_validator_pod_failed_to_create"
1049
+ | "package_validator_pod_waiting_for_ready"
1050
+ | "package_validator_pod_timed_out"
1051
+ | "package_validator_pod_ready"
1052
+ | "package_validator_failed"
1053
+ | "package_validator_retrying"
1054
+ | "package_validator_validating"
1055
+ | "package_validator_updating_unreal_project_name"
1056
+ | "package_validator_updating_project_path"
1057
+ | "package_validator_complete"
1058
+ | "volume_copy_pvcs_creating"
1059
+ | "volume_copy_pvcs_bound"
1060
+ | "volume_copy_pvcs_failed"
1061
+ | "volume_copy_pods_creating"
1062
+ | "volume_copy_pods_failed_to_create"
1063
+ | "volume_copy_pods_timed_out_creating"
1064
+ | "volume_copy_pods_waiting_for_ready"
1065
+ | "volume_copy_pods_failed"
1066
+ | "volume_copy_pods_ready"
1067
+ | "volume_copy_region_copying"
1068
+ | "volume_copy_region_failed"
1069
+ | "volume_copy_region_complete"
1070
+ | "volume_copy_failed"
1071
+ | "volume_copy_retrying"
1072
+ | "volume_copy_complete"
1073
+ | "volume_copy_expiring"
1074
+ | "volume_copy_expired"
1075
+ | "expiring"
1076
+ | "expired";
1077
+
1078
+ type ProjectVersionStateGroup =
1079
+ | "uploading"
1080
+ | "validating"
1081
+ | "building"
1082
+ | "deploying"
1083
+ | "complete"
1084
+ | "failed"
1085
+ | "uploaded"
1086
+ | "expired";
1087
+ ```
1088
+
1089
+ ---
1090
+
1091
+ ### Asset Types
1092
+
1093
+ ```typescript
1094
+ type AssetResponse = {
1095
+ id: string;
1096
+ name: string;
1097
+ orgId: string;
1098
+ projectId: string;
1099
+ assetType: AssetType;
1100
+ sourceType: SourceType;
1101
+ uploadStatus: UploadStatus;
1102
+ validationStatus: ValidationStatus;
1103
+ buildStatus: BuildStatus;
1104
+ uploadId: string | null;
1105
+ objectName: string | null;
1106
+ downloadUrl: string | null;
1107
+ thumb: string | null;
1108
+ description: string | null;
1109
+ createdAt: Date;
1110
+ updatedAt: Date;
1111
+ };
1112
+
1113
+ type UnrealProjectResponse = {
1114
+ assetId: string;
1115
+ orgId: string;
1116
+ projectId: string;
1117
+ displayName: string;
1118
+ unrealProjectVersion: string;
1119
+ unrealPluginVersion: string;
1120
+ versions?: UnrealProjectVersion[];
1121
+ createdAt: Date;
1122
+ updatedAt: Date;
1123
+ };
1124
+
1125
+ type Other3dResponse = {
1126
+ assetId: string;
1127
+ orgId: string;
1128
+ projectId: string;
1129
+ displayName: string;
1130
+ unrealPluginVersion: string;
1131
+ createdAt: Date;
1132
+ updatedAt: Date;
1133
+ };
1134
+
1135
+ type AssetWithRelations = AssetResponse & {
1136
+ unrealProjects?: UnrealProjectResponse[];
1137
+ other3d?: Other3dResponse[];
1138
+ };
1139
+
1140
+ type CreateAssetInput = {
1141
+ name: string;
1142
+ orgId: string;
1143
+ projectId: string;
1144
+ assetType: AssetType;
1145
+ sourceType: SourceType;
1146
+ storageBucket: string;
1147
+ storagePath: string;
1148
+ };
1149
+
1150
+ type UpdateUnrealProjectInput = {
1151
+ displayName?: string;
1152
+ unrealProjectVersion?: string;
1153
+ unrealPluginVersion?: string;
1154
+ };
1155
+
1156
+ type UpdateOther3dInput = {
1157
+ displayName?: string;
1158
+ unrealPluginVersion?: string;
1159
+ };
1160
+
1161
+ type UpdateAssetInput = {
1162
+ name?: string;
1163
+ uploadStatus?: UploadStatus;
1164
+ validationStatus?: ValidationStatus;
1165
+ buildStatus?: BuildStatus;
1166
+ uploadId?: string;
1167
+ objectName?: string;
1168
+ downloadUrl?: string;
1169
+ thumb?: string;
1170
+ description?: string;
1171
+ unrealProject?: UpdateUnrealProjectInput;
1172
+ other3d?: UpdateOther3dInput;
1173
+ };
1174
+
1175
+ type UpdateAssetStatusInput = {
1176
+ uploadStatus?: UploadStatus;
1177
+ validationStatus?: ValidationStatus;
1178
+ buildStatus?: BuildStatus;
1179
+ };
1180
+
1181
+ type CreateAssetWithUnrealProjectInput = {
1182
+ assetData: CreateAssetInput;
1183
+ unrealProjectData: {
1184
+ displayName: string;
1185
+ unrealProjectVersion: string;
1186
+ unrealPluginVersion: string;
1187
+ };
1188
+ };
1189
+
1190
+ type CreateAssetWithOther3dInput = {
1191
+ assetData: CreateAssetInput;
1192
+ other3dData: {
1193
+ displayName: string;
1194
+ unrealPluginVersion: string;
1195
+ };
1196
+ };
1197
+ ```
1198
+
1199
+ ---
1200
+
1201
+ ### Version Types
1202
+
1203
+ ```typescript
1204
+ type UnrealProjectVersion = {
1205
+ id: string;
1206
+ orgId: string;
1207
+ projectId: string;
1208
+ authorUserId: string;
1209
+ appType: string;
1210
+ unrealProjectId: string;
1211
+ buildRegion: string | null;
1212
+ levelFilePath: string | null;
1213
+ levelName: string | null;
1214
+ name: string;
1215
+ packageArchiveSha256Sum: string;
1216
+ packageArchiveUrl: string;
1217
+ pluginVersionId: string;
1218
+ selfPackaged: boolean;
1219
+ state: ProjectVersionState;
1220
+ stateGroup: ProjectVersionStateGroup;
1221
+ symbolsArchiveSha256Sum: string;
1222
+ symbolsArchiveUrl: string;
1223
+ bridgeToolkitFileSettings: Record<string, any>;
1224
+ stateChanges: Record<string, any>;
1225
+ target: UnrealProjectVersionTarget;
1226
+ uploader: string;
1227
+ uploadId: string | null;
1228
+ objectName: string | null;
1229
+ downloadUrl: string | null;
1230
+ uploadSha256Sum: string | null;
1231
+ unrealEngineVersion: SupportedUnrealEngineVersion | null;
1232
+ volumeCopyRegionsComplete: string[];
1233
+ volumeRegions: string[];
1234
+ volumeSizeGb: number;
1235
+ unrealProjectDirectoryPath: string | null;
1236
+ disableMultiplayer: boolean | null;
1237
+ lastPingFromBuilder: Date | null;
1238
+ lastPingFromVolumeCopyRegion: Date | null;
1239
+ createdAt: Date;
1240
+ updatedAt: Date;
1241
+ };
1242
+
1243
+ // See CreateUnrealProjectVersionInput fields in the Versions API section above.
1244
+ type UpdateUnrealProjectVersionInput = Record<string, any>;
1245
+ ```
1246
+
1247
+ ---
1248
+
1249
+ ### User Types
1250
+
1251
+ ```typescript
1252
+ type User = {
1253
+ id: string;
1254
+ createdAt: Date;
1255
+ updatedAt: Date;
1256
+ };
1257
+
1258
+ type CreateUserInput = {
1259
+ id?: string;
1260
+ };
1261
+
1262
+ type UpdateUserInput = {
1263
+ id?: string;
1264
+ };
1265
+ ```
1266
+
1267
+ ---
1268
+
1269
+ ### Upload Types
1270
+
1271
+ ```typescript
1272
+ type InitiateUploadRequest = {
1273
+ orgId: string;
1274
+ projectId: string;
1275
+ userId: string;
1276
+ assetType: AssetType;
1277
+ assetFilename: string;
1278
+ assetDisplayName?: string;
1279
+ unrealProjectId?: string;
1280
+ unrealProjectDisplayName?: string;
1281
+ unrealEngineVersion?: SupportedUnrealEngineVersion;
1282
+ selfPackaged?: boolean;
1283
+ target?: UnrealProjectVersionTarget;
1284
+ other_3dId?: string;
1285
+ other_3dDisplayName?: string;
1286
+ buildRegion?: string;
1287
+ volumeRegions?: string[];
1288
+ };
1289
+
1290
+ type InitiateUploadResponse = {
1291
+ orgId: string;
1292
+ projectId: string;
1293
+ assetId: string;
1294
+ assetVersionId: string;
1295
+ uploadId: string;
1296
+ objectName: string;
1297
+ };
1298
+
1299
+ type CompleteUploadRequest = {
1300
+ orgId: string;
1301
+ projectId: string;
1302
+ assetId: string;
1303
+ assetType: AssetType;
1304
+ assetVersionId: string;
1305
+ uploadId: string;
1306
+ objectName: string;
1307
+ parts: { partNumber: number; etag: string }[];
1308
+ failed?: boolean;
1309
+ sha256Sum?: string;
1310
+ };
1311
+
1312
+ type AbortUploadRequest = {
1313
+ orgId: string;
1314
+ projectId: string;
1315
+ assetId: string;
1316
+ assetType: AssetType;
1317
+ assetVersionId: string;
1318
+ uploadId: string;
1319
+ objectName: string;
1320
+ };
1321
+
1322
+ type UploadSession = {
1323
+ orgId: string;
1324
+ projectId: string;
1325
+ assetId: string;
1326
+ assetVersionId: string;
1327
+ uploadId: string;
1328
+ objectName: string;
1329
+ };
1330
+
1331
+ type UploadPart = {
1332
+ partNumber: number;
1333
+ etag: string;
1334
+ };
1335
+ ```
1336
+
1337
+ ---
1338
+
1339
+ ### Health Types
1340
+
1341
+ ```typescript
1342
+ type HealthResponse = {
1343
+ status: string;
1344
+ timestamp: string;
1345
+ };
1346
+
1347
+ type RedisHealthResponse = {
1348
+ status: string;
1349
+ latencyMs?: number;
1350
+ };
1351
+ ```
1352
+
1353
+ ---
1354
+
1355
+ ### Sanitization Types
1356
+
1357
+ ```typescript
1358
+ type DeletionStepResult = {
1359
+ step: string;
1360
+ success: boolean;
1361
+ skipped?: boolean;
1362
+ error?: string;
1363
+ };
1364
+
1365
+ type DeleteProjectReport = {
1366
+ assetId: string;
1367
+ assetType: "UNREAL_PROJECT" | "OTHER_3D";
1368
+ orgId: string;
1369
+ projectId: string;
1370
+ steps: DeletionStepResult[];
1371
+ fullyDeleted: boolean;
1372
+ };
1373
+ ```
1374
+
1375
+ ---
1376
+
1377
+ ### Error Types
1378
+
1379
+ ```typescript
1380
+ interface OdysseyError extends Error {
1381
+ readonly code: string; // Machine-readable error code
1382
+ readonly name: string; // Human-readable error name
1383
+ readonly message: string; // Description of what went wrong
1384
+ }
1385
+
1386
+ // Type guard — use in catch blocks to narrow the error type
1387
+ function isOdysseyError(err: unknown): err is OdysseyError;
1388
+ ```
1389
+
1390
+ ---
1391
+
1392
+ ## Complete Upload Example
1393
+
1394
+ A full end-to-end multipart upload with session recovery and error handling:
1395
+
1396
+ ```typescript
1397
+ import {
1398
+ createAssetManagerSDK,
1399
+ isOdysseyError,
1400
+ } from "@alliumcloud/odyssey-asset-manager-sdk";
1401
+
1402
+ const sdk = createAssetManagerSDK({
1403
+ apiKey: "your-api-key",
1404
+ jwksUri: "https://your-auth-domain/.well-known/jwks.json",
1405
+ accessTokenProvider: async () => await yourAuthService.getToken(),
1406
+ });
1407
+
1408
+ async function uploadAsset(
1409
+ file: File,
1410
+ orgId: string,
1411
+ projectId: string,
1412
+ userId: string,
1413
+ ) {
1414
+ const PART_SIZE = 50 * 1024 * 1024; // 50MB per part
1415
+ const totalParts = Math.ceil(file.size / PART_SIZE);
1416
+
1417
+ let session;
1418
+ let uploadedParts: { partNumber: number; etag: string }[] = [];
1419
+
1420
+ try {
1421
+ // 1. Check for an existing session (resume support)
1422
+ const existing = await sdk.upload.getSession(file.name);
1423
+
1424
+ if (existing) {
1425
+ session = existing;
1426
+ uploadedParts = await sdk.upload.listParts(
1427
+ session.objectName,
1428
+ session.uploadId,
1429
+ );
1430
+ console.log(
1431
+ `Resuming upload — ${uploadedParts.length} parts already done`,
1432
+ );
1433
+ } else {
1434
+ // 2. Initiate a fresh upload
1435
+ session = await sdk.upload.initiate({
1436
+ orgId,
1437
+ projectId,
1438
+ userId,
1439
+ assetType: "UNREAL_PROJECT",
1440
+ assetFilename: file.name,
1441
+ unrealEngineVersion: "5.2.1",
1442
+ target: "Development",
1443
+ selfPackaged: true,
1444
+ });
1445
+ }
1446
+
1447
+ const { assetId, assetVersionId, uploadId, objectName } = session;
1448
+ const uploadedPartNumbers = new Set(uploadedParts.map((p) => p.partNumber));
1449
+ const remainingParts = Array.from(
1450
+ { length: totalParts },
1451
+ (_, i) => i + 1,
1452
+ ).filter((n) => !uploadedPartNumbers.has(n));
1453
+
1454
+ // 3. Fetch signed URLs for remaining parts in one request
1455
+ const signedUrls = await sdk.upload.batchGetSignedUrls(
1456
+ orgId,
1457
+ projectId,
1458
+ assetId,
1459
+ assetVersionId,
1460
+ uploadId,
1461
+ objectName,
1462
+ remainingParts,
1463
+ );
1464
+
1465
+ // 4. Upload each remaining part
1466
+ for (const partNumber of remainingParts) {
1467
+ const start = (partNumber - 1) * PART_SIZE;
1468
+ const chunk = file.slice(start, start + PART_SIZE);
1469
+
1470
+ const response = await fetch(signedUrls[partNumber], {
1471
+ method: "PUT",
1472
+ body: chunk,
1473
+ });
1474
+
1475
+ const etag = response.headers.get("ETag")!;
1476
+ uploadedParts.push({ partNumber, etag });
1477
+ }
1478
+
1479
+ // 5. Complete the upload
1480
+ await sdk.upload.complete({
1481
+ orgId,
1482
+ projectId,
1483
+ assetId,
1484
+ assetType: "UNREAL_PROJECT",
1485
+ assetVersionId,
1486
+ uploadId,
1487
+ objectName,
1488
+ parts: uploadedParts.sort((a, b) => a.partNumber - b.partNumber),
1489
+ });
1490
+
1491
+ console.log("Upload complete:", assetId);
1492
+ return assetId;
1493
+ } catch (err) {
1494
+ if (isOdysseyError(err)) {
1495
+ console.error(`[${err.name}] ${err.message}`);
1496
+
1497
+ // Clean up on upload-specific failures
1498
+ if (err.code === "UPLOAD_ERROR" && session) {
1499
+ await sdk.upload.abort({
1500
+ orgId,
1501
+ projectId,
1502
+ assetId: session.assetId,
1503
+ assetType: "UNREAL_PROJECT",
1504
+ assetVersionId: session.assetVersionId,
1505
+ uploadId: session.uploadId,
1506
+ objectName: session.objectName,
1507
+ });
1508
+ await sdk.upload.deleteRecords({
1509
+ orgId,
1510
+ projectId,
1511
+ assetId: session.assetId,
1512
+ assetType: "UNREAL_PROJECT",
1513
+ assetVersionId: session.assetVersionId,
1514
+ uploadId: session.uploadId,
1515
+ objectName: session.objectName,
1516
+ });
1517
+ }
1518
+ }
1519
+ throw err;
1520
+ }
1521
+ }
1522
+ ```
1523
+
1524
+ ---
1525
+
1526
+ ## Versioning
1527
+
1528
+ This SDK follows semantic versioning:
1529
+
1530
+ | Bump | When |
1531
+ | ----------------- | -------------------------------------------------------- |
1532
+ | `patch` — `x.x.1` | Bug fixes, no API changes. |
1533
+ | `minor` — `x.1.0` | New methods or fields added, fully backwards compatible. |
1534
+ | `major` — `2.0.0` | Breaking changes to existing method signatures or types. |
1535
+
1536
+ ---
1537
+
1538
+ ## License
1539
+
1540
+ Private — Odyssey Internal Use Only